1
0
mirror of https://github.com/Toxblh/MTMR.git synced 2026-01-10 17:08:39 +00:00

Compare commits

...

59 Commits

Author SHA1 Message Date
AoMe · 青目
dd99e9d73d
fix: discord broken logo url (#466) 2024-03-13 19:07:03 +00:00
_lordBucket (Antoniuk Orest)
58beb5a213
Add PLN and UAH signs to CurrencyBarItem.swift (#463)
I just added them, to give more flexibility for some people from eastern Europe. Currently it looks like: "PLN9.53"
2023-12-24 17:23:38 +00:00
Marcus Förster
7da9ca2c68
add visual link for keycode (#462) 2023-12-21 18:27:38 +00:00
Zaheer
88a4ce82db
Currency update (#446)
* Added new crypto icons

* Added new crypto icons

* Create swift.yml

* Added new crypto icons

* Delete swift.yml

* Delete publish.yml

* Revert publish.yml

---------

Co-authored-by: Anton Palgunov <Toxblh@users.noreply.github.com>
2023-07-28 20:02:18 +01:00
Vladimir Tolstikov
d39b4c0c31
YandexWeatherBarItem: fixes in parsing (#454) 2023-02-28 17:27:06 +00:00
ckfear
5e609c2446
fix incorrect value of audio slider when audio output is switched (#444) 2022-08-15 22:47:24 +01:00
ak0nst
14301c4dbd
update of Sparkle framework for fix apple m1 local builds (#441)
* added speed units for network

* update of Sparkle framework for fix apple m1 local builds

Co-authored-by: akonst <akonst@cqg.com>
2022-06-22 17:37:36 +01:00
ak0nst
a879498e4c
added speed units for network (#440)
Co-authored-by: akonst <akonst@cqg.com>
2022-05-11 10:57:17 +01:00
Wiktor Latanowicz
36bf749a46
App id matching (#432)
* Add app id matching for buttons

* Hide MT button from  control strip when touch bar is empty

Co-authored-by: Wiktor Latanowicz <wiktor@latanowicz.com>
Co-authored-by: Anton Palgunov <Toxblh@users.noreply.github.com>
2022-02-16 19:01:09 +00:00
Piss Man
ac0e44db4d
Modified the way shell script are called (#430)
* Added shell detection

* Replaced deprecated object to supported version

* Updated haptic feedback.swift to support for newer mac

Added support for m1 mac
2022-02-16 19:00:28 +00:00
Dennis Wurster
26ad83be70
fixed grammar and clarity of README (#427)
* fixed grammar and clarity of README

* Update README.md

Co-authored-by: Anton Palgunov <Toxblh@users.noreply.github.com>
2022-02-16 18:59:41 +00:00
Vladimir Tolstikov
d199bbd852
Add new "cpu" widget (#415) 2021-09-16 16:49:39 +01:00
ash14
352bf4887c
Parse title/image from a script's output (#418) 2021-09-16 16:49:09 +01:00
Vladimir Tolstikov
211ca4be32
YandexWeather: update matching array with missing forecast ("Drizzle") (#414) 2021-08-17 15:01:07 +01:00
Sergey Ryazanov
d270a7bbcd
Fix HapticFeedback; Add MBP 13, M1 2020 (#408)
* Fix HapticFeedback; Add MBP 13, M1 2020

* Fix warnings

Co-authored-by: Sergey Ryazanov <sergey.ryazanov@rightperception.company>
2021-06-23 10:58:18 +01:00
Taymuraz Kaytmazov
44732e8ad6
chore: better grammar (#409) 2021-06-21 12:23:45 +01:00
侑夕
8c57342070
add missing punctuation (#400) 2021-06-21 12:23:36 +01:00
Harsh Anand
3add660d72
fix brew install cmd (#385) 2021-01-26 10:32:41 +00:00
ShanHui
eb617ff31b
update logo (#375)
* update logo

* fix the unauthorized use of the Apple Logo problem
2020-11-24 11:01:20 +00:00
3e82676008 upd build.sh 2020-11-20 02:24:31 +00:00
a2ad47c7ba build counter 2020-11-20 02:21:05 +00:00
bbe901a572 version 0.27 2020-11-20 01:42:50 +00:00
54eaa3fd9f Change brightness for 3 finger gesture. Close #370, #372 2020-11-19 21:32:35 +00:00
Matteo Piccina
6660bb2d8f
Fix brightness keys (#367) 2020-11-19 21:27:30 +00:00
Kaibin Yang
7a1800252c
Fix error. (#325)
Colon expected.
2020-08-13 12:32:55 +01:00
Matteo Piccina
588e6ae09b
Implement multiple actions (double tap, triple tap) (#349)
* Implement double tap and new actions array in config

* Update native widgets to use new actions parameter

* Refactor new actions parameter moving it to main definition
Renamed old action and longAction to legacy

* Fix tests

* Remove unused code

* Readd test for legacyAction

* Implement triple tap

* Add new actions explanation

* Add support for multiple actions and same trigger
2020-08-03 11:53:39 +01:00
Connor G Meehan
87141e381b
Up next calendar widget (#348)
* WIP Implementation of up next widget

* Seperated button view and event source logic

* Adjusted default parameters

* Added the ability to view multiple events

* Added ability to click touchbar item and go to calendar

* renamed nthEvent to maxToShow and changed default

* Updated CFBundleVersion

* Renamed UpNext class and fix ups

* Added "autoResize" property (same functionality as dock)

* Added EKEventStore listener to reduce perfomance impact

* Log cleanup

* Made button blue for current/past events

* Added handling of unauthorised access to calendar
2020-07-27 00:41:05 +01:00
Vladimir Tolstikov
14282b86a9
YandexWeather: fix race condition which randomly leads to displaying wrong data (#345) 2020-07-16 19:11:37 +01:00
Vladimir Tolstikov
810cdeed36
YandexWeather: fix issue with wrong URL which leads to random incorrect location detection and wrong weather display (#344) 2020-07-15 21:04:11 +01:00
Giuseppe Petrosino
a65613acaf
Allow change of of a group button width (#336) 2020-06-17 22:40:58 +01:00
Anton Palgunov
aa69d5f592
Update TECHNICAL_DEBT.md 2020-05-26 17:46:15 +01:00
Anton Palgunov
2f00c9ffb3
Update README.md 2020-05-26 17:45:49 +01:00
Vladimir Tolstikov
1def53878d
YandexWeather: update matching array with missing forecast ("Thunderstorm with rain") (#316) 2020-05-21 11:27:50 +01:00
3e5fa14494 v0.26.1 2020-05-17 13:42:02 +01:00
Fedor Zaitsev
2e2f556daf
Fixed lost margin (#313)
* Updated README

Added explanation for missing parameters (background, title and image)

* Implemented changable icons for AppleScriptTouchBarItem

AppleScriptTouchBarItem now allow to specify any number of icons which can be changed from the script. You cannot change icon from touch event. To change icon, you need to return array from your script with 2 values - title and icn name. More info in readme

* Fixed #312

Fixed bug that margin is lost on all elements

Co-authored-by: Fedor Zaitsev <lobster@Fedors-MacBook-Pro.local>
2020-05-17 13:39:21 +01:00
1e1ae2af61 Release version 0.26 2020-05-14 00:49:53 +01:00
bc11728c2e Standard english date for build script 2020-05-14 00:44:10 +01:00
445584bb1b Added xcpretty for test and build scripts 2020-05-14 00:42:08 +01:00
b6721f0274 return normal trigggers in workflows 2020-05-14 00:05:17 +01:00
75df82a567 ignore sign error 2020-05-13 23:34:45 +01:00
a1f64028cc Correct file for release 2020-05-13 23:23:03 +01:00
3fc75ca8f0 Updated workflows. For delivery unsign app in gihub 2020-05-13 23:22:31 +01:00
6d266394a4 Revert "More Productive Gestures (#289)"
This reverts commit f61550e510.
2020-05-13 23:11:45 +01:00
Fedor Zaytsev
52758f947d
Fix shell crash (#297)
* Updated README

Added explanation for missing parameters (background, title and image)

* Implemented changable icons for AppleScriptTouchBarItem

AppleScriptTouchBarItem now allow to specify any number of icons which can be changed from the script. You cannot change icon from touch event. To change icon, you need to return array from your script with 2 values - title and icn name. More info in readme

* Fixed error related to ShellButton

When you execute Process from swift you cannot relay solely on pipe.fileHandleForReading.readDataToEndOfFile()
Sometimes when I close notebook I get exception saying that you cannot access process.terminationStatus variable while process is running.
Apparently it seems that this call can be finished when OS X put disks into sleep mode(?)

What I did:
1. Added Process.waitUntilExit() call
2. Added timeout (equal to the update interval)

Co-authored-by: Fedor Zaitsev <lobster@Fedors-MacBook-Pro.local>
2020-04-21 13:34:03 +01:00
Simon Rogers
502f989417
Currency bar update (#293)
* Updated CurrenyBarItem to display second currency (TO FROM>RATE) and also correct number of decimal places.
Added INR to list of currencies.

* Update CurrencyBarItem.swift

* Updated else statements

Co-authored-by: medden <si@medden.co.uk>
2020-04-09 18:49:39 +01:00
Jæy
f61550e510
More Productive Gestures (#289)
* Scroll Functionality Works

* updated md

* Update README.md

* Update README.md

* attempted to fix readme

* fixed readme

* fixed readme
2020-04-09 00:24:43 +01:00
Fedor Zaytsev
a0fc0b33c5
Custom gestures support (#288)
* Updated README

Added explanation for missing parameters (background, title and image)

* Implemented changable icons for AppleScriptTouchBarItem

AppleScriptTouchBarItem now allow to specify any number of icons which can be changed from the script. You cannot change icon from touch event. To change icon, you need to return array from your script with 2 values - title and icn name. More info in readme

* Implemented custom swipe actions

Co-authored-by: Fedor Zaitsev <lobster@Fedors-MacBook-Pro.local>
2020-04-01 22:11:35 +01:00
Peter Hrvola
42ce95b72e
Add Music app (#279) 2020-03-16 12:06:20 +00:00
Fedor Zaytsev
642f0807bb
Changeable icons for AppleScriptTouchBarItem & tests (#276)
* Implemented changable icons for AppleScriptTouchBarItem

AppleScriptTouchBarItem now allow to specify any number of icons which can be changed from the script. You cannot change icon from touch event. To change icon, you need to return array from your script with 2 values - title and icn name. More info in readme

* Fixed tests

Some tests started to fail after implementing alternativeIcons field for appleScriptBarItem
2020-02-29 11:23:40 +00:00
Fedor Zaytsev
dbb2f16222
Implemented changable icons for AppleScriptTouchBarItem (#275)
AppleScriptTouchBarItem now allow to specify any number of icons which can be changed from the script. You cannot change icon from touch event. To change icon, you need to return array from your script with 2 values - title and icn name. More info in readme
2020-02-28 21:59:14 +00:00
Anton Palgunov
3864591777
Update build-test.yml 2020-02-28 21:27:09 +00:00
Fedor Zaytsev
7e0db70fab
Updated README (#274)
Added explanation for missing parameters (background, title and image)
2020-02-28 19:35:09 +00:00
Anton Palgunov
40c684f528
GitHub actions (#271)
* build xcode
* final build test
* delete travisCI
2020-02-11 23:44:19 +00:00
Anton Palgunov
093faa2c02
Create swift.yml 2020-01-14 10:24:24 +00:00
Anton Palgunov
aeee9983d1
Merge pull request #260 from samflattery/master
fix pomodoro icon not showing up on catalina
2020-01-08 14:57:23 +00:00
Sam Flattery
bdfb7118c9 fix pomodoro icon not showing up on catalina 2020-01-06 19:17:29 +00:00
Anton Palgunov
878cd346c1
Merge pull request #256 from bobrosoft/master
YandexWeather: update matching array with English translations as some users getting English version instead of Russian
2019-12-30 09:18:09 +00:00
bobrosoft
712448f207 YandexWeather: update matching array with English translations as some users getting English version instead of Russian 2019-12-30 11:51:27 +04:00
e9e5a6f739 fix yandex wheather, for correct lang 2019-11-26 17:33:18 +03:00
348 changed files with 8553 additions and 969 deletions

2
.github/FUNDING.yml vendored
View File

@ -3,4 +3,4 @@
issuehunt: Toxblh
patreon: toxblh
ko_fi: toxblh
custom: https://www.paypal.me/toxblh
custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=WUAAG2HH58WE4

22
.github/workflows/build-test.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: Build-and-test
on: [push, pull_request]
jobs:
test:
runs-on: macOS-latest
steps:
- uses: actions/checkout@v1
- name: Run tests
run: xcodebuild test -project MTMR.xcodeproj -scheme 'UnitTests' | xcpretty -c && exit ${PIPESTATUS[0]}
build:
runs-on: macOS-latest
steps:
- uses: actions/checkout@v1
- name: Build
run: xcodebuild archive -project "MTMR.xcodeproj" -scheme "MTMR" -archivePath Release/App.xcarchive DEVELOPMENT_TEAM="" CODE_SIGN_IDENTITY="" | xcpretty -c && exit ${PIPESTATUS[0]}

41
.github/workflows/publish.yml vendored Normal file
View File

@ -0,0 +1,41 @@
name: Publish unsign version
on:
push:
branches:
- master
tags:
- "v*"
jobs:
Build-and-release:
runs-on: macOS-latest
steps:
- uses: actions/checkout@v1
- name: Set up Node.js
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Install create-dmg
run: npm i -g create-dmg
- name: Build Archive
run: xcodebuild archive -project "MTMR.xcodeproj" -scheme "MTMR" -archivePath Release/App.xcarchive DEVELOPMENT_TEAM="" CODE_SIGN_IDENTITY="" | xcpretty -c && exit ${PIPESTATUS[0]}
- name: Build App
run: xcodebuild -project "MTMR.xcodeproj" -exportArchive -archivePath Release/App.xcarchive -exportOptionsPlist export-options.plist -exportPath Release | xcpretty -c && exit ${PIPESTATUS[0]}
- name: Create DMG
run: |
cd Release
create-dmg MTMR.app || true
- name: GitHub Release
uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
prerelease: false
files: Release/MTMR*.dmg

View File

@ -1,6 +0,0 @@
language: swift
xcode_project: MTMR.xcodeproj
xcode_scheme: UnitTests
osx_image: xcode11.1
install: gem install xcpretty
script: "xcodebuild test -project MTMR.xcodeproj -scheme 'UnitTests' | xcpretty -c && exit ${PIPESTATUS[0]}"

View File

@ -21,7 +21,11 @@
36FEF872235A1CFC00A0ABCE /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36FEF871235A1CFC00A0ABCE /* AppSettings.swift */; };
4CC9FEDC22FDEA65001512EB /* AMR_ANSIEscapeHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CC9FEDB22FDEA65001512EB /* AMR_ANSIEscapeHelper.m */; };
4CDC6E5022FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDC6E4F22FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift */; };
4CF222EB26CC00470025BDA7 /* CPUBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF222EA26CC00470025BDA7 /* CPUBarItem.swift */; };
4CF222ED26CC43D50025BDA7 /* CPU.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF222EC26CC43D40025BDA7 /* CPU.swift */; };
4CFF5E5C22E623DD00BFB1EE /* YandexWeatherBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF5E5B22E623DD00BFB1EE /* YandexWeatherBarItem.swift */; };
5DC6CA02241F92CB005CD5E8 /* Music.nowPlaying.scpt in Resources */ = {isa = PBXBuildFile; fileRef = 5DC6CA00241F92CB005CD5E8 /* Music.nowPlaying.scpt */; };
5DC6CA03241F92CB005CD5E8 /* Music.next.scpt in Resources */ = {isa = PBXBuildFile; fileRef = 5DC6CA01241F92CB005CD5E8 /* Music.next.scpt */; };
60173D3E20C0031B002C305F /* LaunchAtLoginController.m in Sources */ = {isa = PBXBuildFile; fileRef = 60173D3D20C0031B002C305F /* LaunchAtLoginController.m */; };
6027D1B92080E52A004FFDC7 /* BrightnessViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6027D1B72080E52A004FFDC7 /* BrightnessViewController.swift */; };
6027D1BA2080E52A004FFDC7 /* VolumeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6027D1B82080E52A004FFDC7 /* VolumeViewController.swift */; };
@ -67,6 +71,9 @@
B0F3112520C9E35F0076BB88 /* SupportNSTouchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0F3112420C9E35F0076BB88 /* SupportNSTouchBar.swift */; };
B0F54A7A2295AC7D00B4C509 /* DarkModeBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0F54A792295AC7D00B4C509 /* DarkModeBarItem.swift */; };
B0F8771A207AC1EA00D6E430 /* TouchBarSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = B0F87719207AC1EA00D6E430 /* TouchBarSupport.m */; };
BAF5AB5724317B4300B04904 /* BasicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF5AB5624317B4300B04904 /* BasicView.swift */; };
BAF5AB5924317CAF00B04904 /* SwipeItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF5AB5824317CAF00B04904 /* SwipeItem.swift */; };
F29F6A2524BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F29F6A2424BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@ -99,7 +106,11 @@
4CC9FEDA22FDEA65001512EB /* AMR_ANSIEscapeHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AMR_ANSIEscapeHelper.h; sourceTree = "<group>"; };
4CC9FEDB22FDEA65001512EB /* AMR_ANSIEscapeHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AMR_ANSIEscapeHelper.m; sourceTree = "<group>"; };
4CDC6E4F22FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShellScriptTouchBarItem.swift; sourceTree = "<group>"; };
4CF222EA26CC00470025BDA7 /* CPUBarItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CPUBarItem.swift; sourceTree = "<group>"; };
4CF222EC26CC43D40025BDA7 /* CPU.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CPU.swift; sourceTree = "<group>"; };
4CFF5E5B22E623DD00BFB1EE /* YandexWeatherBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YandexWeatherBarItem.swift; sourceTree = "<group>"; };
5DC6CA00241F92CB005CD5E8 /* Music.nowPlaying.scpt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Music.nowPlaying.scpt; sourceTree = "<group>"; };
5DC6CA01241F92CB005CD5E8 /* Music.next.scpt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Music.next.scpt; sourceTree = "<group>"; };
60173D3C20C0031B002C305F /* LaunchAtLoginController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LaunchAtLoginController.h; sourceTree = "<group>"; };
60173D3D20C0031B002C305F /* LaunchAtLoginController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LaunchAtLoginController.m; sourceTree = "<group>"; };
6027D1B72080E52A004FFDC7 /* BrightnessViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrightnessViewController.swift; sourceTree = "<group>"; };
@ -155,6 +166,9 @@
B0F54A792295AC7D00B4C509 /* DarkModeBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DarkModeBarItem.swift; sourceTree = "<group>"; };
B0F87719207AC1EA00D6E430 /* TouchBarSupport.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TouchBarSupport.m; sourceTree = "<group>"; };
B0F8771B207AC92700D6E430 /* TouchBarSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TouchBarSupport.h; sourceTree = "<group>"; };
BAF5AB5624317B4300B04904 /* BasicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicView.swift; sourceTree = "<group>"; };
BAF5AB5824317CAF00B04904 /* SwipeItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeItem.swift; sourceTree = "<group>"; };
F29F6A2424BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpNextScrubberTouchBarItem.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -229,9 +243,12 @@
B0A7E9A9205D6AA400EEF070 /* KeyPress.swift */,
36FEF871235A1CFC00A0ABCE /* AppSettings.swift */,
B059D623205E04F3006E6B86 /* CustomButtonTouchBarItem.swift */,
4CF222EC26CC43D40025BDA7 /* CPU.swift */,
36C2ECD8207B74B4003CDA33 /* AppleScriptTouchBarItem.swift */,
BAF5AB5824317CAF00B04904 /* SwipeItem.swift */,
4CDC6E4F22FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift */,
B059D621205E03F5006E6B86 /* TouchBarController.swift */,
BAF5AB5624317B4300B04904 /* BasicView.swift */,
36C2ECDA207C3FE7003CDA33 /* ItemsParsing.swift */,
B0008E542080286C003AD4DD /* SupportHelpers.swift */,
B09EB1E3207C082000D5C1E0 /* HapticFeedback.swift */,
@ -258,6 +275,8 @@
B0B17426207D6AFE0004B740 /* AppleScripts */ = {
isa = PBXGroup;
children = (
5DC6CA01241F92CB005CD5E8 /* Music.next.scpt */,
5DC6CA00241F92CB005CD5E8 /* Music.nowPlaying.scpt */,
B0B1742A207D6B580004B740 /* Battery.scpt */,
B0B17429207D6B580004B740 /* Finder.scpt */,
B0B1742C207D6B590004B740 /* iTunes.next.scpt */,
@ -296,6 +315,7 @@
6042B6A62083E03A00C525C8 /* AppScrubberTouchBarItem.swift */,
B04B7BB62087398C00C835D0 /* BatteryBarItem.swift */,
6027D1B72080E52A004FFDC7 /* BrightnessViewController.swift */,
4CF222EA26CC00470025BDA7 /* CPUBarItem.swift */,
607EEA4C2087A8DA009DA5F0 /* CurrencyBarItem.swift */,
B081732B213739FE005D4908 /* DnDBarItem.swift */,
60669B4220AD8FA80074E817 /* GroupBarItem.swift */,
@ -310,6 +330,7 @@
4CFF5E5B22E623DD00BFB1EE /* YandexWeatherBarItem.swift */,
B08126F0217BE19000A98970 /* WidgetProtocol.swift */,
B0F54A792295AC7D00B4C509 /* DarkModeBarItem.swift */,
F29F6A2424BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift */,
);
path = Widgets;
sourceTree = "<group>";
@ -407,6 +428,8 @@
B0B1743A207D6B590004B740 /* iTunes.nowPlaying.scpt in Resources */,
B082B257205C7D8000BC04DC /* Assets.xcassets in Resources */,
B0B17437207D6B590004B740 /* Spotify.nowPlaying.scpt in Resources */,
5DC6CA02241F92CB005CD5E8 /* Music.nowPlaying.scpt in Resources */,
5DC6CA03241F92CB005CD5E8 /* Music.next.scpt in Resources */,
B0B17436207D6B590004B740 /* iTunes.next.scpt in Resources */,
B082B25A205C7D8000BC04DC /* Main.storyboard in Resources */,
B0B17435207D6B590004B740 /* Spotify.next.scpt in Resources */,
@ -457,14 +480,18 @@
B059D622205E03F5006E6B86 /* TouchBarController.swift in Sources */,
B05600D32083E9BB00EB218D /* CustomSlider.swift in Sources */,
36C2ECD9207B74B4003CDA33 /* AppleScriptTouchBarItem.swift in Sources */,
BAF5AB5724317B4300B04904 /* BasicView.swift in Sources */,
B0846A752220C968000288A7 /* NetworkBarItem.swift in Sources */,
B0F8771A207AC1EA00D6E430 /* TouchBarSupport.m in Sources */,
B08126F1217BE19000A98970 /* WidgetProtocol.swift in Sources */,
4CF222EB26CC00470025BDA7 /* CPUBarItem.swift in Sources */,
B0008E552080286C003AD4DD /* SupportHelpers.swift in Sources */,
6042B6A72083E03A00C525C8 /* AppScrubberTouchBarItem.swift in Sources */,
BAF5AB5924317CAF00B04904 /* SwipeItem.swift in Sources */,
B082B253205C7D8000BC04DC /* AppDelegate.swift in Sources */,
B0F54A7A2295AC7D00B4C509 /* DarkModeBarItem.swift in Sources */,
B08126EF217BD0B900A98970 /* PomodoroBarItem.swift in Sources */,
4CF222ED26CC43D50025BDA7 /* CPU.swift in Sources */,
B081732C213739FE005D4908 /* DnDBarItem.swift in Sources */,
60C44AFD20A373A100C0EC91 /* MusicBarItem.swift in Sources */,
B059D624205E04F3006E6B86 /* CustomButtonTouchBarItem.swift in Sources */,
@ -472,6 +499,7 @@
6027D1BA2080E52A004FFDC7 /* VolumeViewController.swift in Sources */,
4CDC6E5022FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift in Sources */,
607EEA4B2087835F009DA5F0 /* WeatherBarItem.swift in Sources */,
F29F6A2524BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift in Sources */,
B0F3112520C9E35F0076BB88 /* SupportNSTouchBar.swift in Sources */,
4CFF5E5C22E623DD00BFB1EE /* YandexWeatherBarItem.swift in Sources */,
6042B6AA2083E27000C525C8 /* DeprecatedCarbonAPI.c in Sources */,

View File

@ -25,7 +25,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true] as NSDictionary)
TouchBarController.shared.setupControlStripPresence()
HapticFeedbackUpdate()
if let button = statusItem.button {
button.image = #imageLiteral(resourceName: "StatusImage")
@ -41,10 +40,6 @@ class AppDelegate: NSObject, NSApplicationDelegate {
func applicationWillTerminate(_: Notification) {}
func HapticFeedbackUpdate() {
HapticFeedback.shared = AppSettings.hapticFeedbackState ? HapticFeedback() : nil
}
@objc func updateIsBlockedApp() {
if let frontmostAppId = TouchBarController.shared.frontmostApplicationIdentifier {
isBlockedApp = AppSettings.blacklistedAppIds.firstIndex(of: frontmostAppId) != nil
@ -86,13 +81,12 @@ class AppDelegate: NSObject, NSApplicationDelegate {
@objc func toggleHapticFeedback(_ item: NSMenuItem) {
item.state = item.state == .on ? .off : .on
AppSettings.hapticFeedbackState = item.state == .on
HapticFeedbackUpdate()
}
@objc func toggleMultitouch(_ item: NSMenuItem) {
item.state = item.state == .on ? .off : .on
AppSettings.multitouchGestures = item.state == .on
TouchBarController.shared.scrollArea?.gesturesEnabled = item.state == .on
TouchBarController.shared.basicView?.legacyGesturesEnabled = item.state == .on
}
@objc func openPreset(_: Any?) {

View File

@ -4,9 +4,11 @@ class AppleScriptTouchBarItem: CustomButtonTouchBarItem {
private var script: NSAppleScript!
private let interval: TimeInterval
private var forceHideConstraint: NSLayoutConstraint!
private let alternativeImages: [String: SourceProtocol]
init?(identifier: NSTouchBarItem.Identifier, source: SourceProtocol, interval: TimeInterval) {
init?(identifier: NSTouchBarItem.Identifier, source: SourceProtocol, interval: TimeInterval, alternativeImages: [String: SourceProtocol]) {
self.interval = interval
self.alternativeImages = alternativeImages
super.init(identifier: identifier, title: "")
forceHideConstraint = view.widthAnchor.constraint(equalToConstant: 0)
title = "scheduled"
@ -57,6 +59,16 @@ class AppleScriptTouchBarItem: CustomButtonTouchBarItem {
}
}
func updateIcon(iconLabel: String) {
if alternativeImages[iconLabel] != nil {
DispatchQueue.main.async {
self.image = self.alternativeImages[iconLabel]!.image
}
} else {
print("Cannot find icon with label \"\(iconLabel)\"")
}
}
func execute() -> String {
var error: NSDictionary?
let output = script.executeAndReturnError(&error)
@ -64,6 +76,18 @@ class AppleScriptTouchBarItem: CustomButtonTouchBarItem {
print(error)
return "error"
}
if output.descriptorType == typeAEList {
let arr = Array(1...output.numberOfItems).compactMap({ output.atIndex($0)!.stringValue ?? "" })
if arr.count <= 0 {
return ""
} else if arr.count == 1 {
return arr[0]
} else {
updateIcon(iconLabel: arr[1])
return arr[0]
}
}
return output.stringValue ?? ""
}
}

View File

@ -0,0 +1,7 @@
if application "Music" is running then
tell application "Music"
if player state is playing then
next track
end if
end tell
end if

View File

@ -0,0 +1,10 @@
if application "Music" is running then
tell application "Music"
if player state is playing then
return (get artist of current track) & " " & (get name of current track)
else
return ""
end if
end tell
end if
return ""

View File

@ -2,6 +2,10 @@ if application "iTunes" is running then
tell application "iTunes" to playpause
end if
if application "Music" is running then
tell application "Music" to playpause
end if
if application "Spotify" is running then
tell application "Spotify" to playpause
end if

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 667 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,21 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "cpu.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

107
MTMR/BasicView.swift Normal file
View File

@ -0,0 +1,107 @@
//
// BasicView.swift
// MTMR
//
// Created by Fedor Zaitsev on 3/29/20.
// Copyright © 2020 Anton Palgunov. All rights reserved.
//
import Foundation
class BasicView: NSCustomTouchBarItem, NSGestureRecognizerDelegate {
var twofingers: NSPanGestureRecognizer!
var threefingers: NSPanGestureRecognizer!
var fourfingers: NSPanGestureRecognizer!
var swipeItems: [SwipeItem] = []
var prevPositions: [Int: CGFloat] = [2:0, 3:0, 4:0]
// legacy gesture positions
// by legacy I mean gestures to increse/decrease volume/brigtness which can be checked from app menu
var legacyPrevPositions: [Int: CGFloat] = [2:0, 3:0, 4:0]
var legacyGesturesEnabled = false
init(identifier: NSTouchBarItem.Identifier, items: [NSTouchBarItem], swipeItems: [SwipeItem]) {
super.init(identifier: identifier)
self.swipeItems = swipeItems
let views = items.compactMap { $0.view }
let stackView = NSStackView(views: views)
stackView.spacing = 8
stackView.orientation = .horizontal
view = stackView
twofingers = NSPanGestureRecognizer(target: self, action: #selector(twofingersHandler(_:)))
twofingers.numberOfTouchesRequired = 2
twofingers.allowedTouchTypes = .direct
view.addGestureRecognizer(twofingers)
threefingers = NSPanGestureRecognizer(target: self, action: #selector(threefingersHandler(_:)))
threefingers.numberOfTouchesRequired = 3
threefingers.allowedTouchTypes = .direct
view.addGestureRecognizer(threefingers)
fourfingers = NSPanGestureRecognizer(target: self, action: #selector(fourfingersHandler(_:)))
fourfingers.numberOfTouchesRequired = 4
fourfingers.allowedTouchTypes = .direct
view.addGestureRecognizer(fourfingers)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func gestureHandler(position: CGFloat, fingers: Int, state: NSGestureRecognizer.State) {
switch state {
case .began:
prevPositions[fingers] = position
legacyPrevPositions[fingers] = position
case .changed:
if self.legacyGesturesEnabled {
if fingers == 2 {
let prevPos = legacyPrevPositions[fingers]!
if ((position - prevPos) > 10) || ((prevPos - position) > 10) {
if position > prevPos {
HIDPostAuxKey(NX_KEYTYPE_SOUND_UP)
} else if position < prevPos {
HIDPostAuxKey(NX_KEYTYPE_SOUND_DOWN)
}
legacyPrevPositions[fingers] = position
}
}
if fingers == 3 {
let prevPos = legacyPrevPositions[fingers]!
if ((position - prevPos) > 15) || ((prevPos - position) > 15) {
if position > prevPos {
HIDPostAuxKey(NX_KEYTYPE_BRIGHTNESS_UP)
} else if position < prevPos {
HIDPostAuxKey(NX_KEYTYPE_BRIGHTNESS_DOWN)
}
legacyPrevPositions[fingers] = position
}
}
}
case .ended:
print("gesture ended \(position - prevPositions[fingers]!) \(fingers)")
for item in swipeItems {
item.processEvent(offset: position - prevPositions[fingers]!, fingers: fingers)
}
default:
break
}
}
@objc func twofingersHandler(_ sender: NSGestureRecognizer?) {
let position = (sender?.location(in: sender?.view).x)!
self.gestureHandler(position: position, fingers: 2, state: sender!.state)
}
@objc func threefingersHandler(_ sender: NSGestureRecognizer?) {
let position = (sender?.location(in: sender?.view).x)!
self.gestureHandler(position: position, fingers: 3, state: sender!.state)
}
@objc func fourfingersHandler(_ sender: NSGestureRecognizer?) {
let position = (sender?.location(in: sender?.view).x)!
self.gestureHandler(position: position, fingers: 4, state: sender!.state)
}
}

191
MTMR/CPU.swift Normal file
View File

@ -0,0 +1,191 @@
//
// CPU.swift
// Pods
//
// Created by zixun on 2016/12/5.
// https://github.com/zixun/SystemEye
// MIT License
//
//
import Foundation
private let HOST_CPU_LOAD_INFO_COUNT : mach_msg_type_number_t =
UInt32(MemoryLayout<host_cpu_load_info_data_t>.size / MemoryLayout<integer_t>.size)
/// CPU Class
public class CPU: NSObject {
//--------------------------------------------------------------------------
// MARK: OPEN PROPERTY
//--------------------------------------------------------------------------
// /// Number of physical cores on this machine.
// public static var physicalCores: Int {
// get {
// return Int(System.hostBasicInfo.physical_cpu)
// }
// }
//
// /// Number of logical cores on this machine. Will be equal to physicalCores
// /// unless it has hyper-threading, in which case it will be double.
// public static var logicalCores: Int {
// get {
// return Int(System.hostBasicInfo.logical_cpu)
// }
// }
//--------------------------------------------------------------------------
// MARK: OPEN FUNCTIONS
//--------------------------------------------------------------------------
/// Get CPU usage of hole system (system, user, idle, nice). Determined by the delta between
/// the current and last call.
public static func systemUsage() -> (system: Double,
user: Double,
idle: Double,
nice: Double) {
let load = self.hostCPULoadInfo
let userDiff = Double(load.cpu_ticks.0 - loadPrevious.cpu_ticks.0)
let sysDiff = Double(load.cpu_ticks.1 - loadPrevious.cpu_ticks.1)
let idleDiff = Double(load.cpu_ticks.2 - loadPrevious.cpu_ticks.2)
let niceDiff = Double(load.cpu_ticks.3 - loadPrevious.cpu_ticks.3)
let totalTicks = sysDiff + userDiff + niceDiff + idleDiff
let sys = sysDiff / totalTicks * 100.0
let user = userDiff / totalTicks * 100.0
let idle = idleDiff / totalTicks * 100.0
let nice = niceDiff / totalTicks * 100.0
loadPrevious = load
return (sys, user, idle, nice)
}
/// Get CPU usage of application,get from all thread
open class func applicationUsage() -> Double {
let threads = self.threadBasicInfos()
var result : Double = 0.0
threads.forEach { (thread:thread_basic_info) in
if self.flag(thread) {
result += Double.init(thread.cpu_usage) / Double.init(TH_USAGE_SCALE);
}
}
return result * 100
}
//--------------------------------------------------------------------------
// MARK: PRIVATE PROPERTY
//--------------------------------------------------------------------------
/// previous load of cpu
private static var loadPrevious = host_cpu_load_info()
static var hostCPULoadInfo: host_cpu_load_info {
get {
var size = HOST_CPU_LOAD_INFO_COUNT
var hostInfo = host_cpu_load_info()
let result = withUnsafeMutablePointer(to: &hostInfo) {
$0.withMemoryRebound(to: integer_t.self, capacity: Int(size)) {
host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, $0, &size)
}
}
#if DEBUG
if result != KERN_SUCCESS {
fatalError("ERROR - \(#file):\(#function) - kern_result_t = "
+ "\(result)")
}
#endif
return hostInfo
}
}
//--------------------------------------------------------------------------
// MARK: PRIVATE FUNCTION
//--------------------------------------------------------------------------
private class func flag(_ thread:thread_basic_info) -> Bool {
let foo = thread.flags & TH_FLAGS_IDLE
let number = NSNumber.init(value: foo)
return !Bool.init(truncating: number)
}
private class func threadActPointers() -> [thread_act_t] {
var threads_act = [thread_act_t]()
var threads_array: thread_act_array_t? = nil
var count = mach_msg_type_number_t()
let result = task_threads(mach_task_self_, &(threads_array), &count)
guard result == KERN_SUCCESS else {
return threads_act
}
guard let array = threads_array else {
return threads_act
}
for i in 0..<count {
threads_act.append(array[Int(i)])
}
let krsize = count * UInt32.init(MemoryLayout<thread_t>.size)
_ = vm_deallocate(mach_task_self_, vm_address_t(array.pointee), vm_size_t(krsize));
return threads_act
}
private class func threadBasicInfos() -> [thread_basic_info] {
var result = [thread_basic_info]()
let thinfo : thread_info_t = thread_info_t.allocate(capacity: Int(THREAD_INFO_MAX))
let thread_info_count = UnsafeMutablePointer<mach_msg_type_number_t>.allocate(capacity: 128)
var basic_info_th: thread_basic_info_t? = nil
for act_t in self.threadActPointers() {
thread_info_count.pointee = UInt32(THREAD_INFO_MAX);
let kr = thread_info(act_t ,thread_flavor_t(THREAD_BASIC_INFO),thinfo, thread_info_count);
if (kr != KERN_SUCCESS) {
return [thread_basic_info]();
}
basic_info_th = withUnsafePointer(to: &thinfo.pointee, { (ptr) -> thread_basic_info_t in
let int8Ptr = unsafeBitCast(ptr, to: thread_basic_info_t.self)
return int8Ptr
})
if basic_info_th != nil {
result.append(basic_info_th!.pointee)
}
}
return result
}
//TODO: this function is used for get cpu usage of all thread,and this is in developing
private class func threadIdentifierInfos() -> [thread_identifier_info] {
var result = [thread_identifier_info]()
let thinfo : thread_info_t = thread_info_t.allocate(capacity: Int(THREAD_INFO_MAX))
let thread_info_count = UnsafeMutablePointer<mach_msg_type_number_t>.allocate(capacity: 128)
var identifier_info_th: thread_identifier_info_t? = nil
for act_t in self.threadActPointers() {
thread_info_count.pointee = UInt32(THREAD_INFO_MAX);
let kr = thread_info(act_t ,thread_flavor_t(THREAD_IDENTIFIER_INFO),thinfo, thread_info_count);
if (kr != KERN_SUCCESS) {
return [thread_identifier_info]();
}
identifier_info_th = withUnsafePointer(to: &thinfo.pointee, { (ptr) -> thread_identifier_info_t in
let int8Ptr = unsafeBitCast(ptr, to: thread_identifier_info_t.self)
return int8Ptr
})
if identifier_info_th != nil {
result.append(identifier_info_th!.pointee)
}
}
return result
}
}

View File

@ -8,18 +8,32 @@
import Cocoa
struct ItemAction {
typealias TriggerClosure = (() -> Void)?
let trigger: Action.Trigger
let closure: TriggerClosure
init(trigger: Action.Trigger, _ closure: TriggerClosure) {
self.trigger = trigger
self.closure = closure
}
}
class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegate {
var tapClosure: (() -> Void)?
var longTapClosure: (() -> Void)? {
var actions: [ItemAction] = [] {
didSet {
longClick.isEnabled = longTapClosure != nil
multiClick.isDoubleClickEnabled = actions.filter({ $0.trigger == .doubleTap }).count > 0
multiClick.isTripleClickEnabled = actions.filter({ $0.trigger == .tripleTap }).count > 0
longClick.isEnabled = actions.filter({ $0.trigger == .longTap }).count > 0
}
}
var finishViewConfiguration: ()->() = {}
private var button: NSButton!
private var singleClick: HapticClickGestureRecognizer!
private var longClick: LongPressGestureRecognizer!
private var multiClick: MultiClickGestureRecognizer!
init(identifier: NSTouchBarItem.Identifier, title: String) {
attributedTitle = title.defaultTouchbarAttributedString
@ -32,9 +46,16 @@ class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegat
longClick.allowedTouchTypes = .direct
longClick.delegate = self
singleClick = HapticClickGestureRecognizer(target: self, action: #selector(handleGestureSingle))
singleClick.allowedTouchTypes = .direct
singleClick.delegate = self
multiClick = MultiClickGestureRecognizer(
target: self,
action: #selector(handleGestureSingleTap),
doubleAction: #selector(handleGestureDoubleTap),
tripleAction: #selector(handleGestureTripleTap)
)
multiClick.allowedTouchTypes = .direct
multiClick.delegate = self
multiClick.isDoubleClickEnabled = false
multiClick.isTripleClickEnabled = false
reinstallButton()
button.attributedTitle = attributedTitle
@ -100,33 +121,43 @@ class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegat
view = button
view.addGestureRecognizer(longClick)
view.addGestureRecognizer(singleClick)
// view.addGestureRecognizer(singleClick)
view.addGestureRecognizer(multiClick)
finishViewConfiguration()
}
func gestureRecognizer(_ gestureRecognizer: NSGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: NSGestureRecognizer) -> Bool {
if gestureRecognizer == singleClick && otherGestureRecognizer == longClick
|| gestureRecognizer == longClick && otherGestureRecognizer == singleClick // need it
if gestureRecognizer == multiClick && otherGestureRecognizer == longClick
|| gestureRecognizer == longClick && otherGestureRecognizer == multiClick // need it
{
return false
}
return true
}
@objc func handleGestureSingle(gr: NSClickGestureRecognizer) {
switch gr.state {
case .ended:
tapClosure?()
break
default:
break
func callActions(for trigger: Action.Trigger) {
let itemActions = self.actions.filter { $0.trigger == trigger }
for itemAction in itemActions {
itemAction.closure?()
}
}
@objc func handleGestureSingleTap() {
callActions(for: .singleTap)
}
@objc func handleGestureDoubleTap() {
callActions(for: .doubleTap)
}
@objc func handleGestureTripleTap() {
callActions(for: .tripleTap)
}
@objc func handleGestureLong(gr: NSPressGestureRecognizer) {
switch gr.state {
case .possible: // tiny hack because we're calling action manually
(self.longTapClosure ?? self.tapClosure)?()
callActions(for: .longTap)
break
default:
break
@ -176,15 +207,78 @@ class CustomButtonCell: NSButtonCell {
}
}
class HapticClickGestureRecognizer: NSClickGestureRecognizer {
// Thanks to https://stackoverflow.com/a/49843893
final class MultiClickGestureRecognizer: NSClickGestureRecognizer {
private let _action: Selector
private let _doubleAction: Selector
private let _tripleAction: Selector
private var _clickCount: Int = 0
public var isDoubleClickEnabled = true
public var isTripleClickEnabled = true
override var action: Selector? {
get {
return nil /// prevent base class from performing any actions
} set {
if newValue != nil { // if they are trying to assign an actual action
fatalError("Only use init(target:action:doubleAction) for assigning actions")
}
}
}
required init(target: AnyObject, action: Selector, doubleAction: Selector, tripleAction: Selector) {
_action = action
_doubleAction = doubleAction
_tripleAction = tripleAction
super.init(target: target, action: nil)
}
required init?(coder: NSCoder) {
fatalError("init(target:action:doubleAction:tripleAction) is only support atm")
}
override func touchesBegan(with event: NSEvent) {
HapticFeedback.shared?.tap(strong: 2)
HapticFeedback.instance.tap(type: .click)
super.touchesBegan(with: event)
}
override func touchesEnded(with event: NSEvent) {
HapticFeedback.shared?.tap(strong: 1)
HapticFeedback.instance.tap(type: .back)
super.touchesEnded(with: event)
_clickCount += 1
var delayThreshold: TimeInterval // fine tune this as needed
guard isDoubleClickEnabled || isTripleClickEnabled else {
_ = target?.perform(_action)
return
}
if (isTripleClickEnabled) {
delayThreshold = 0.4
perform(#selector(_resetAndPerformActionIfNecessary), with: nil, afterDelay: delayThreshold)
if _clickCount == 3 {
_ = target?.perform(_tripleAction)
}
} else {
delayThreshold = 0.3
perform(#selector(_resetAndPerformActionIfNecessary), with: nil, afterDelay: delayThreshold)
if _clickCount == 2 {
_ = target?.perform(_doubleAction)
}
}
}
@objc private func _resetAndPerformActionIfNecessary() {
if _clickCount == 1 {
_ = target?.perform(_action)
}
if isTripleClickEnabled && _clickCount == 2 {
_ = target?.perform(_doubleAction)
}
_clickCount = 0
}
}
@ -228,7 +322,7 @@ class LongPressGestureRecognizer: NSPressGestureRecognizer {
@objc private func onTimer() {
if let target = self.target, let action = self.action {
target.performSelector(onMainThread: action, with: self, waitUntilDone: false)
HapticFeedback.shared?.tap(strong: 6)
HapticFeedback.instance.tap(type: .strong)
}
}

View File

@ -9,81 +9,92 @@
import IOKit
class HapticFeedback {
static var shared: HapticFeedback?
// Here we have list of possible IDs for Haptic Generator Device. They are not constant
// To find deviceID, you will need IORegistryExplorer app from Additional Tools for Xcode dmg
// which you can download from https://developer.apple.com/download/more/?=Additional%20Tools
// Open IORegistryExplorer app, search for AppleMultitouchDevice and get "Multitouch ID"
// Open IORegistryExplorer app, search for "AppleMultitouchDevice" and get "Multitouch ID"
// or "AppleMultitouchTrackpadHIDEventDriver" and get "mt-device-id"
// There should be programmatic way to get it but I can't find, no docs for macOS :(
private let possibleDeviceIDs: [UInt64] = [
0x200_0000_0100_0000, // MacBook Pro 2016/2017
0x300000080500000 // MacBook Pro 2019 (possibly 2018 as well)
0x300_0000_8050_0000, // MacBook Pro 2019/2018
0x200_0000_0000_0024, // MacBook Pro (13-inch, M1, 2020)
0x200_0000_0000_0023 // MacBook Pro M1 13-Inch 2020 with 1tb
// 0x300000080500000,
]
private var correctDeviceID: UInt64?
// you can get a plist `otool -s __TEXT __tpad_act_plist /System/Library/PrivateFrameworks/MultitouchSupport.framework/Versions/Current/MultitouchSupport|tail -n +3|awk -F'\t' '{print $2}'|xxd -r -p`
enum HapticType: Int32, CaseIterable {
case back = 1
case click = 2
case weak = 3
case medium = 4
case weakMedium = 5
case strong = 6
case reserved1 = 15
case reserved2 = 16
}
private var actuatorRef: CFTypeRef?
init() {
recreateDevice()
}
static var instance = HapticFeedback()
// Don't know how to do strong is enum one of
// 1 like back Click
// 2 like Click
// 3 week
// 4 medium
// 5 week medium
// 6 strong
// 15 nothing
// 16 nothing
// you can get a plist `otool -s __TEXT __tpad_act_plist /System/Library/PrivateFrameworks/MultitouchSupport.framework/Versions/Current/MultitouchSupport|tail -n +3|awk -F'\t' '{print $2}'|xxd -r -p`
// MARK: - Init
func tap(strong: Int32) {
guard correctDeviceID != nil, actuatorRef != nil else {
print("guard actuatorRef == nil (no haptic device found?)")
return
}
var result: IOReturn
result = MTActuatorOpen(actuatorRef!)
guard result == kIOReturnSuccess else {
print("guard MTActuatorOpen")
recreateDevice()
return
}
result = MTActuatorActuate(actuatorRef!, strong, 0, 0, 0)
guard result == kIOReturnSuccess else {
print("guard MTActuatorActuate")
return
}
result = MTActuatorClose(actuatorRef!)
guard result == kIOReturnSuccess else {
print("guard MTActuatorClose")
return
}
private init() {
self.recreateDevice()
}
private func recreateDevice() {
if let actuatorRef = actuatorRef {
if let actuatorRef = self.actuatorRef {
MTActuatorClose(actuatorRef)
self.actuatorRef = nil // just in case %)
}
if let correctDeviceID = correctDeviceID {
actuatorRef = MTActuatorCreateFromDeviceID(correctDeviceID).takeRetainedValue()
} else {
guard self.actuatorRef == nil else {
return
}
// Let's find our Haptic device
possibleDeviceIDs.forEach {(deviceID) in
guard correctDeviceID == nil else {return}
actuatorRef = MTActuatorCreateFromDeviceID(deviceID).takeRetainedValue()
self.possibleDeviceIDs.forEach {(deviceID) in
let actuatorRef = MTActuatorCreateFromDeviceID(deviceID).takeRetainedValue()
if actuatorRef != nil {
correctDeviceID = deviceID
self.actuatorRef = actuatorRef
}
}
}
// MARK: - Tap action
private func getActuatorIfPosible() -> CFTypeRef? {
guard AppSettings.hapticFeedbackState else { return nil }
guard let actuatorRef = self.actuatorRef else {
print("guard actuatorRef == nil (no haptic device found?)")
return nil
}
guard MTActuatorOpen(actuatorRef) == kIOReturnSuccess else {
print("guard MTActuatorOpen")
self.recreateDevice()
return nil
}
return actuatorRef
}
func tap(type: HapticType) {
guard let actuator = getActuatorIfPosible() else { return }
guard MTActuatorActuate(actuator, type.rawValue, 0, 0, 0) == kIOReturnSuccess else {
print("guard MTActuatorActuate")
return
}
guard MTActuatorClose(actuator) == kIOReturnSuccess else {
print("guard MTActuatorClose")
return
}
}
}

View File

@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.25</string>
<string>0.27</string>
<key>CFBundleVersion</key>
<string>297</string>
<string>448</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSMinimumSystemVersion</key>
@ -39,7 +39,7 @@
<key>NSHomeKitUsageDescription</key>
<string>MTMR needs access to HomeKit for work</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2018 Anton Palgunov. All rights reserved.</string>
<string>Copyright © 2018 - 2020 Anton Palgunov. All rights reserved.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Weather widget need your location for correct work</string>
<key>NSLocationAlwaysUsageDescription</key>

View File

@ -3,47 +3,52 @@ import Foundation
extension Data {
func barItemDefinitions() -> [BarItemDefinition]? {
return try? JSONDecoder().decode([BarItemDefinition].self, from: utf8string!.stripComments().data(using: .utf8)!)
return try! JSONDecoder().decode([BarItemDefinition].self, from: utf8string!.stripComments().data(using: .utf8)!)
}
}
struct BarItemDefinition: Decodable {
let type: ItemType
let action: ActionType
let longAction: LongActionType
let actions: [Action]
let legacyAction: LegacyActionType
let legacyLongAction: LegacyLongActionType
let additionalParameters: [GeneralParameters.CodingKeys: GeneralParameter]
private enum CodingKeys: String, CodingKey {
case type
case actions
}
init(type: ItemType, action: ActionType, longAction: LongActionType, additionalParameters: [GeneralParameters.CodingKeys: GeneralParameter]) {
init(type: ItemType, actions: [Action], action: LegacyActionType, legacyLongAction: LegacyLongActionType, additionalParameters: [GeneralParameters.CodingKeys: GeneralParameter]) {
self.type = type
self.action = action
self.longAction = longAction
self.actions = actions
self.legacyAction = action
self.legacyLongAction = legacyLongAction
self.additionalParameters = additionalParameters
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
let parametersDecoder = SupportedTypesHolder.sharedInstance.lookup(by: type)
let actions = try container.decodeIfPresent([Action].self, forKey: .actions)
let parametersDecoder = SupportedTypesHolder.sharedInstance.lookup(by: type, actions: actions ?? [])
var additionalParameters = try GeneralParameters(from: decoder).parameters
if let result = try? parametersDecoder(decoder),
case let (itemType, action, longAction, parameters) = result {
case let (itemType, actions, action, longAction, parameters) = result {
parameters.forEach { additionalParameters[$0] = $1 }
self.init(type: itemType, action: action, longAction: longAction, additionalParameters: additionalParameters)
self.init(type: itemType, actions: actions, action: action, legacyLongAction: longAction, additionalParameters: additionalParameters)
} else {
self.init(type: .staticButton(title: "unknown"), action: .none, longAction: .none, additionalParameters: additionalParameters)
self.init(type: .staticButton(title: "unknown"), actions: [], action: .none, legacyLongAction: .none, additionalParameters: additionalParameters)
}
}
}
typealias ParametersDecoder = (Decoder) throws -> (
item: ItemType,
action: ActionType,
longAction: LongActionType,
actions: [Action],
legacyAction: LegacyActionType,
legacyLongAction: LegacyLongActionType,
parameters: [GeneralParameters.CodingKeys: GeneralParameter]
)
@ -51,15 +56,21 @@ class SupportedTypesHolder {
private var supportedTypes: [String: ParametersDecoder] = [
"escape": { _ in (
item: .staticButton(title: "esc"),
action: .keyPress(keycode: 53),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .keyPress(keycode: 53))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.align: .align(.left)]
) },
"delete": { _ in (
item: .staticButton(title: "del"),
action: .keyPress(keycode: 117),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .keyPress(keycode: 117))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [:]
) },
@ -67,8 +78,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "brightnessUp"))
return (
item: .staticButton(title: ""),
action: .keyPress(keycode: 144),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_BRIGHTNESS_UP))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter]
)
},
@ -77,8 +91,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "brightnessDown"))
return (
item: .staticButton(title: ""),
action: .keyPress(keycode: 145),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_BRIGHTNESS_DOWN))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter]
)
},
@ -87,8 +104,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "ill_up"))
return (
item: .staticButton(title: ""),
action: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_UP),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_UP))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter]
)
},
@ -97,8 +117,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "ill_down"))
return (
item: .staticButton(title: ""),
action: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_DOWN),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_DOWN))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter]
)
},
@ -107,8 +130,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarVolumeDownTemplateName)!)
return (
item: .staticButton(title: ""),
action: .hidKey(keycode: NX_KEYTYPE_SOUND_DOWN),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_SOUND_DOWN))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter]
)
},
@ -117,8 +143,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarVolumeUpTemplateName)!)
return (
item: .staticButton(title: ""),
action: .hidKey(keycode: NX_KEYTYPE_SOUND_UP),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_SOUND_UP))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter]
)
},
@ -127,8 +156,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarAudioOutputMuteTemplateName)!)
return (
item: .staticButton(title: ""),
action: .hidKey(keycode: NX_KEYTYPE_MUTE),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_MUTE))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter]
)
},
@ -137,8 +169,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarRewindTemplateName)!)
return (
item: .staticButton(title: ""),
action: .hidKey(keycode: NX_KEYTYPE_PREVIOUS),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_PREVIOUS))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter]
)
},
@ -147,8 +182,11 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarPlayPauseTemplateName)!)
return (
item: .staticButton(title: ""),
action: .hidKey(keycode: NX_KEYTYPE_PLAY),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_PLAY))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter]
)
},
@ -157,23 +195,32 @@ class SupportedTypesHolder {
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarFastForwardTemplateName)!)
return (
item: .staticButton(title: ""),
action: .hidKey(keycode: NX_KEYTYPE_NEXT),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_NEXT))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.image: imageParameter]
)
},
"sleep": { _ in (
item: .staticButton(title: "☕️"),
action: .shellScript(executable: "/usr/bin/pmset", parameters: ["sleepnow"]),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .shellScript(executable: "/usr/bin/pmset", parameters: ["sleepnow"]))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [:]
) },
"displaySleep": { _ in (
item: .staticButton(title: "☕️"),
action: .shellScript(executable: "/usr/bin/pmset", parameters: ["displaysleepnow"]),
longAction: .none,
actions: [
Action(trigger: .singleTap, value: .shellScript(executable: "/usr/bin/pmset", parameters: ["displaysleepnow"]))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [:]
) },
@ -181,11 +228,12 @@ class SupportedTypesHolder {
static let sharedInstance = SupportedTypesHolder()
func lookup(by type: String) -> ParametersDecoder {
func lookup(by type: String, actions: [Action]) -> ParametersDecoder {
return supportedTypes[type] ?? { decoder in (
item: try ItemType(from: decoder),
action: try ActionType(from: decoder),
longAction: try LongActionType(from: decoder),
actions: actions,
legacyAction: try LegacyActionType(from: decoder),
legacyLongAction: try LegacyLongActionType(from: decoder),
parameters: [:]
) }
}
@ -194,12 +242,13 @@ class SupportedTypesHolder {
supportedTypes[typename] = decoder
}
func register(typename: String, item: ItemType, action: ActionType, longAction: LongActionType) {
func register(typename: String, item: ItemType, actions: [Action], legacyAction: LegacyActionType, legacyLongAction: LegacyLongActionType) {
register(typename: typename) { _ in
(
item: item,
action,
longAction,
actions,
legacyAction,
legacyLongAction,
parameters: [:]
)
}
@ -208,10 +257,11 @@ class SupportedTypesHolder {
enum ItemType: Decodable {
case staticButton(title: String)
case appleScriptTitledButton(source: SourceProtocol, refreshInterval: Double)
case appleScriptTitledButton(source: SourceProtocol, refreshInterval: Double, alternativeImages: [String: SourceProtocol])
case shellScriptTitledButton(source: SourceProtocol, refreshInterval: Double)
case timeButton(formatTemplate: String, timeZone: String?, locale: String?)
case battery
case cpu(refreshInterval: Double)
case dock(autoResize: Bool, filter: String?)
case volume
case brightness(refreshInterval: Double)
@ -224,8 +274,10 @@ enum ItemType: Decodable {
case nightShift
case dnd
case pomodoro(workTime: Double, restTime: Double)
case network(flip: Bool)
case network(flip: Bool, units: String)
case darkMode
case swipe(direction: String, fingers: Int, minOffset: Float, sourceApple: SourceProtocol?, sourceBash: SourceProtocol?)
case upnext(from: Double, to: Double, maxToShow: Int, autoResize: Bool)
private enum CodingKeys: String, CodingKey {
case type
@ -251,6 +303,13 @@ enum ItemType: Decodable {
case autoResize
case filter
case disableMarquee
case alternativeImages
case sourceApple
case sourceBash
case direction
case fingers
case minOffset
case maxToShow
}
enum ItemTypeRaw: String, Decodable {
@ -259,6 +318,7 @@ enum ItemType: Decodable {
case shellScriptTitledButton
case timeButton
case battery
case cpu
case dock
case volume
case brightness
@ -273,6 +333,8 @@ enum ItemType: Decodable {
case pomodoro
case network
case darkMode
case swipe
case upnext
}
init(from decoder: Decoder) throws {
@ -282,7 +344,8 @@ enum ItemType: Decodable {
case .appleScriptTitledButton:
let source = try container.decode(Source.self, forKey: .source)
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 1800.0
self = .appleScriptTitledButton(source: source, refreshInterval: interval)
let alternativeImages = try container.decodeIfPresent([String: Source].self, forKey: .alternativeImages) ?? [:]
self = .appleScriptTitledButton(source: source, refreshInterval: interval, alternativeImages: alternativeImages)
case .shellScriptTitledButton:
let source = try container.decode(Source.self, forKey: .source)
@ -302,6 +365,10 @@ enum ItemType: Decodable {
case .battery:
self = .battery
case .cpu:
let refreshInterval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 5.0
self = .cpu(refreshInterval: refreshInterval)
case .dock:
let autoResize = try container.decodeIfPresent(Bool.self, forKey: .autoResize) ?? false
let filterRegexString = try container.decodeIfPresent(String.self, forKey: .filter)
@ -357,15 +424,119 @@ enum ItemType: Decodable {
case .network:
let flip = try container.decodeIfPresent(Bool.self, forKey: .flip) ?? false
self = .network(flip: flip)
let units = try container.decodeIfPresent(String.self, forKey: .units) ?? "dynamic"
self = .network(flip: flip, units: units)
case .darkMode:
self = .darkMode
case .swipe:
let sourceApple = try container.decodeIfPresent(Source.self, forKey: .sourceApple)
let sourceBash = try container.decodeIfPresent(Source.self, forKey: .sourceBash)
let direction = try container.decode(String.self, forKey: .direction)
let fingers = try container.decode(Int.self, forKey: .fingers)
let minOffset = try container.decodeIfPresent(Float.self, forKey: .minOffset) ?? 0.0
self = .swipe(direction: direction, fingers: fingers, minOffset: minOffset, sourceApple: sourceApple, sourceBash: sourceBash)
case .upnext:
let from = try container.decodeIfPresent(Double.self, forKey: .from) ?? 0 // Lower bounds of period of time in hours to search for events
let to = try container.decodeIfPresent(Double.self, forKey: .to) ?? 12 // Upper bounds of period of time in hours to search for events
let maxToShow = try container.decodeIfPresent(Int.self, forKey: .maxToShow) ?? 3 // 1 indexed array. Get the 1st, 2nd, 3rd event to display multiple notifications
let autoResize = try container.decodeIfPresent(Bool.self, forKey: .autoResize) ?? false
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 60.0
self = .upnext(from: from, to: to, maxToShow: maxToShow, autoResize: autoResize)
}
}
}
enum ActionType: Decodable {
struct FailableDecodable<Base : Decodable> : Decodable {
let base: Base?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.base = try? container.decode(Base.self)
}
}
struct Action: Decodable {
enum Trigger: String, Decodable {
case singleTap
case doubleTap
case tripleTap
case longTap
}
enum Value {
case none
case hidKey(keycode: Int32)
case keyPress(keycode: Int)
case appleScript(source: SourceProtocol)
case shellScript(executable: String, parameters: [String])
case custom(closure: () -> Void)
case openUrl(url: String)
}
private enum ActionTypeRaw: String, Decodable {
case hidKey
case keyPress
case appleScript
case shellScript
case openUrl
}
enum CodingKeys: String, CodingKey {
case trigger
case action
case keycode
case actionAppleScript
case executablePath
case shellArguments
case url
}
let trigger: Trigger
let value: Value
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
trigger = try container.decode(Trigger.self, forKey: .trigger)
let type = try container.decodeIfPresent(ActionTypeRaw.self, forKey: .action)
switch type {
case .some(.hidKey):
let keycode = try container.decode(Int32.self, forKey: .keycode)
value = .hidKey(keycode: keycode)
case .some(.keyPress):
let keycode = try container.decode(Int.self, forKey: .keycode)
value = .keyPress(keycode: keycode)
case .some(.appleScript):
let source = try container.decode(Source.self, forKey: .actionAppleScript)
value = .appleScript(source: source)
case .some(.shellScript):
let executable = try container.decode(String.self, forKey: .executablePath)
let parameters = try container.decodeIfPresent([String].self, forKey: .shellArguments) ?? []
value = .shellScript(executable: executable, parameters: parameters)
case .some(.openUrl):
let url = try container.decode(String.self, forKey: .url)
value = .openUrl(url: url)
case .none:
value = .none
}
}
init(trigger: Trigger, value: Value) {
self.trigger = trigger
self.value = value
}
}
enum LegacyActionType: Decodable {
case none
case hidKey(keycode: Int32)
case keyPress(keycode: Int)
@ -423,7 +594,7 @@ enum ActionType: Decodable {
}
}
enum LongActionType: Decodable {
enum LegacyLongActionType: Decodable {
case none
case hidKey(keycode: Int32)
case keyPress(keycode: Int)
@ -488,6 +659,7 @@ enum GeneralParameter {
case bordered(_: Bool)
case background(_: NSColor)
case title(_: String)
case matchAppId(_: String)
}
struct GeneralParameters: Decodable {
@ -500,6 +672,7 @@ struct GeneralParameters: Decodable {
case bordered
case background
case title
case matchAppId
}
init(from decoder: Decoder) throws {
@ -529,6 +702,10 @@ struct GeneralParameters: Decodable {
result[.title] = .title(title)
}
if let matchAppId = try container.decodeIfPresent(String.self, forKey: .matchAppId) {
result[.matchAppId] = .matchAppId(matchAppId)
}
parameters = result
}
}

View File

@ -1,10 +1,6 @@
import Foundation
class ScrollViewItem: NSCustomTouchBarItem, NSGestureRecognizerDelegate {
var twofingersPrev: CGFloat = 0.0
var threefingersPrev: CGFloat = 0.0
var twofingers: NSPanGestureRecognizer!
var threefingers: NSPanGestureRecognizer!
class ScrollViewItem: NSCustomTouchBarItem/*, NSGestureRecognizerDelegate*/ {
init(identifier: NSTouchBarItem.Identifier, items: [NSTouchBarItem]) {
super.init(identifier: identifier)
@ -15,70 +11,10 @@ class ScrollViewItem: NSCustomTouchBarItem, NSGestureRecognizerDelegate {
let scrollView = NSScrollView(frame: CGRect(origin: .zero, size: stackView.fittingSize))
scrollView.documentView = stackView
view = scrollView
twofingers = NSPanGestureRecognizer(target: self, action: #selector(twofingersHandler(_:)))
twofingers.allowedTouchTypes = .direct
twofingers.numberOfTouchesRequired = 2
view.addGestureRecognizer(twofingers)
threefingers = NSPanGestureRecognizer(target: self, action: #selector(threefingersHandler(_:)))
threefingers.allowedTouchTypes = .direct
threefingers.numberOfTouchesRequired = 3
view.addGestureRecognizer(threefingers)
}
var gesturesEnabled = true {
didSet {
twofingers.isEnabled = gesturesEnabled
threefingers.isEnabled = gesturesEnabled
}
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func twofingersHandler(_ sender: NSGestureRecognizer?) { // Volume
let position = (sender?.location(in: sender?.view).x)!
switch sender!.state {
case .began:
twofingersPrev = position
case .changed:
if ((position - twofingersPrev) > 10) || ((twofingersPrev - position) > 10) {
if position > twofingersPrev {
HIDPostAuxKey(NX_KEYTYPE_SOUND_UP)
} else if position < twofingersPrev {
HIDPostAuxKey(NX_KEYTYPE_SOUND_DOWN)
}
twofingersPrev = position
}
case .ended:
twofingersPrev = 0.0
default:
break
}
}
@objc func threefingersHandler(_ sender: NSGestureRecognizer?) { // Brightness
let position = (sender?.location(in: sender?.view).x)!
switch sender!.state {
case .began:
threefingersPrev = position
case .changed:
if ((position - threefingersPrev) > 15) || ((threefingersPrev - position) > 15) {
if position > threefingersPrev {
GenericKeyPress(keyCode: CGKeyCode(144)).send()
} else if position < threefingersPrev {
GenericKeyPress(keyCode: CGKeyCode(145)).send()
}
threefingersPrev = position
}
case .ended:
threefingersPrev = 0.0
default:
break
}
}
}

View File

@ -12,6 +12,11 @@ class ShellScriptTouchBarItem: CustomButtonTouchBarItem {
private let source: String
private var forceHideConstraint: NSLayoutConstraint!
struct ScriptResult: Decodable {
var title: String?
var image: Source?
}
init?(identifier: NSTouchBarItem.Identifier, source: SourceProtocol, interval: TimeInterval) {
self.interval = interval
self.source = source.string ?? "echo No \"source\""
@ -31,12 +36,25 @@ class ShellScriptTouchBarItem: CustomButtonTouchBarItem {
func refreshAndSchedule() {
// Execute script and get result
let scriptResult = execute(source)
var rawTitle: String, image: NSImage?
var json: Bool
do {
let decoder = JSONDecoder()
let result = try decoder.decode(ScriptResult.self, from: scriptResult.data(using: .utf8)!)
json = true
rawTitle = result.title ?? ""
image = result.image?.image
} catch {
json = false
rawTitle = scriptResult
}
// Apply returned text attributes (if they were returned) to our result string
let helper = AMR_ANSIEscapeHelper.init()
helper.defaultStringColor = NSColor.white
helper.font = "1".defaultTouchbarAttributedString.attribute(.font, at: 0, effectiveRange: nil) as? NSFont
let title = NSMutableAttributedString.init(attributedString: helper.attributedString(withANSIEscapedString: scriptResult) ?? NSAttributedString(string: ""))
let title = NSMutableAttributedString.init(attributedString: helper.attributedString(withANSIEscapedString: rawTitle) ?? NSAttributedString(string: ""))
title.addAttributes([.baselineOffset: 1], range: NSRange(location: 0, length: title.length))
let newBackgoundColor: NSColor? = title.length != 0 ? title.attribute(.backgroundColor, at: 0, effectiveRange: nil) as? NSColor : nil
@ -46,6 +64,9 @@ class ShellScriptTouchBarItem: CustomButtonTouchBarItem {
self?.backgroundColor = newBackgoundColor
}
self?.attributedTitle = title
if json {
self?.image = image
}
self?.forceHideConstraint.isActive = scriptResult == ""
}
@ -57,16 +78,28 @@ class ShellScriptTouchBarItem: CustomButtonTouchBarItem {
func execute(_ command: String) -> String {
let task = Process()
if let shell = getenv("SHELL") {
task.launchPath = String.init(cString: shell)
} else {
task.launchPath = "/bin/bash"
}
task.arguments = ["-c", command]
let pipe = Pipe()
task.standardOutput = pipe
// kill process if it is over update interval
DispatchQueue.main.asyncAfter(deadline: .now() + interval) { [weak task] in
task?.terminate()
}
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
var output: String = NSString(data: data, encoding: String.Encoding.utf8.rawValue) as String? ?? ""
//always wait until task end or you can catch "task still running" error while accessing task.terminationStatus variable
task.waitUntilExit()
if (output == "" && task.terminationStatus != 0) {
output = "error"
}

78
MTMR/SwipeItem.swift Normal file
View File

@ -0,0 +1,78 @@
//
// SwipeItem.swift
// MTMR
//
// Created by Fedor Zaitsev on 3/29/20.
// Copyright © 2020 Anton Palgunov. All rights reserved.
//
import Foundation
import Foundation
class SwipeItem: NSCustomTouchBarItem {
private var scriptApple: NSAppleScript?
private var scriptBash: String?
private var direction: String
private var fingers: Int
private var minOffset: Float
init?(identifier: NSTouchBarItem.Identifier, direction: String, fingers: Int, minOffset: Float, sourceApple: SourceProtocol?, sourceBash: SourceProtocol?) {
self.direction = direction
self.fingers = fingers
self.scriptBash = sourceBash?.string
self.scriptApple = sourceApple?.appleScript
self.minOffset = minOffset
super.init(identifier: identifier)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func processEvent(offset: CGFloat, fingers: Int) {
if direction == "right" && Float(offset) > self.minOffset && self.fingers == fingers {
self.execute()
}
if direction == "left" && Float(offset) < -self.minOffset && self.fingers == fingers {
self.execute()
}
}
func execute() {
if scriptApple != nil {
DispatchQueue.appleScriptQueue.async {
var error: NSDictionary?
self.scriptApple?.executeAndReturnError(&error)
if let error = error {
print("SwipeItem apple script error: \(error)")
return
}
}
}
if scriptBash != nil {
DispatchQueue.shellScriptQueue.async {
let task = Process()
if let shell = getenv("SHELL") {
task.launchPath = String.init(cString: shell)
} else {
task.launchPath = "/bin/bash"
}
task.arguments = ["-c", self.scriptBash!]
task.launch()
task.waitUntilExit()
if (task.terminationStatus != 0) {
print("SwipeItem bash script error. Status: \(task.terminationStatus)")
}
}
}
}
func isEqual(_ object: AnyObject?) -> Bool {
if let object = object as? SwipeItem {
return self.scriptApple?.source as String? == object.scriptApple?.source as String? && self.scriptBash == object.scriptBash && self.direction == object.direction && self.fingers == object.fingers && self.minOffset == object.minOffset
} else {
return false
}
}
}

View File

@ -29,6 +29,8 @@ extension ItemType {
return "com.toxblh.mtmr.timeButton."
case .battery:
return "com.toxblh.mtmr.battery."
case .cpu(refreshInterval: _):
return "com.toxblh.mtmr.cpu."
case .dock(autoResize: _, filter: _):
return "com.toxblh.mtmr.dock"
case .volume:
@ -57,6 +59,10 @@ extension ItemType {
return NetworkBarItem.identifier
case .darkMode:
return DarkModeBarItem.identifier
case .swipe(direction: _, fingers: _, minOffset: _, sourceApple: _, sourceBash: _):
return "com.toxblh.mtmr.swipe."
case .upnext(from: _, to: _, maxToShow: _, autoResize: _):
return "com.connorgmeehan.mtmrup.next."
}
}
}
@ -76,10 +82,10 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
var items: [NSTouchBarItem.Identifier: NSTouchBarItem] = [:]
var leftIdentifiers: [NSTouchBarItem.Identifier] = []
var centerIdentifiers: [NSTouchBarItem.Identifier] = []
var centerItems: [NSTouchBarItem] = []
var rightIdentifiers: [NSTouchBarItem.Identifier] = []
var scrollArea: ScrollViewItem?
var centerScrollArea = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollArea.".appending(UUID().uuidString))
var basicViewIdentifier = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollView.".appending(UUID().uuidString))
var basicView: BasicView?
var swipeItems: [SwipeItem] = []
var blacklistAppIdentifiers: [String] = []
var frontmostApplicationIdentifier: String? {
@ -88,13 +94,28 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
private override init() {
super.init()
SupportedTypesHolder.sharedInstance.register(typename: "exitTouchbar", item: .staticButton(title: "exit"), action: .custom(closure: { [weak self] in self?.dismissTouchBar() }), longAction: .none)
SupportedTypesHolder.sharedInstance.register(
typename: "exitTouchbar",
item: .staticButton(title: "exit"),
actions: [
Action(trigger: .singleTap, value: .custom(closure: { [weak self] in self?.dismissTouchBar() }))
],
legacyAction: .none,
legacyLongAction: .none
)
SupportedTypesHolder.sharedInstance.register(typename: "close") { _ in
(item: .staticButton(title: ""), action: .custom(closure: { [weak self] in
(
item: .staticButton(title: ""),
actions: [
Action(trigger: .singleTap, value: .custom(closure: { [weak self] in
guard let `self` = self else { return }
self.reloadPreset(path: self.lastPresetPath)
}), longAction: .none, parameters: [.width: .width(30), .image: .image(source: (NSImage(named: NSImage.stopProgressFreestandingTemplateName))!)])
}))
],
legacyAction: .none,
legacyLongAction: .none,
parameters: [.width: .width(30), .image: .image(source: (NSImage(named: NSImage.stopProgressFreestandingTemplateName))!)])
}
blacklistAppIdentifiers = AppSettings.blacklistedAppIds
@ -113,27 +134,69 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
touchBar = NSTouchBar()
jsonItems = newJsonItems
itemDefinitions = [:]
items = [:]
leftIdentifiers = []
centerItems = []
rightIdentifiers = []
loadItemDefinitions(jsonItems: jsonItems)
updateActiveApp()
}
func didItemsChange(prevItems: [NSTouchBarItem.Identifier: NSTouchBarItem], prevSwipeItems: [SwipeItem]) -> Bool {
var changed = items.count != prevItems.count || swipeItems.count != prevSwipeItems.count
if !changed {
for (item, prevItem) in zip(items, prevItems) {
if item.key != prevItem.key {
changed = true
break
}
}
}
if !changed {
for (swipeItem, prevSwipeItem) in zip(swipeItems, prevSwipeItems) {
if !swipeItem.isEqual(prevSwipeItem) {
changed = true
break
}
}
}
return changed
}
func prepareTouchBar() {
let prevItems = items
let prevSwipeItems = swipeItems
createItems()
centerItems = centerIdentifiers.compactMap({ (identifier) -> NSTouchBarItem? in
let changed = didItemsChange(prevItems: prevItems, prevSwipeItems: prevSwipeItems)
if !changed {
return
}
let centerItems = centerIdentifiers.compactMap({ (identifier) -> NSTouchBarItem? in
items[identifier]
})
centerScrollArea = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollArea.".appending(UUID().uuidString))
scrollArea = ScrollViewItem(identifier: centerScrollArea, items: centerItems)
scrollArea?.gesturesEnabled = AppSettings.multitouchGestures
let centerScrollArea = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollArea.".appending(UUID().uuidString))
let scrollArea = ScrollViewItem(identifier: centerScrollArea, items: centerItems)
basicViewIdentifier = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollView.".appending(UUID().uuidString))
touchBar.delegate = self
touchBar.defaultItemIdentifiers = []
touchBar.defaultItemIdentifiers = leftIdentifiers + [centerScrollArea] + rightIdentifiers
touchBar.defaultItemIdentifiers = [basicViewIdentifier]
updateActiveApp()
let leftItems = leftIdentifiers.compactMap({ (identifier) -> NSTouchBarItem? in
items[identifier]
})
let rightItems = rightIdentifiers.compactMap({ (identifier) -> NSTouchBarItem? in
items[identifier]
})
basicView = BasicView(identifier: basicViewIdentifier, items:leftItems + [scrollArea] + rightItems, swipeItems: swipeItems)
basicView?.legacyGesturesEnabled = AppSettings.multitouchGestures
}
@objc func activeApplicationChanged(_: Notification) {
@ -144,9 +207,18 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
if frontmostApplicationIdentifier != nil && blacklistAppIdentifiers.firstIndex(of: frontmostApplicationIdentifier!) != nil {
dismissTouchBar()
} else {
prepareTouchBar()
if touchBarContainsAnyItems() {
presentTouchBar()
} else {
dismissTouchBar()
}
}
}
func touchBarContainsAnyItems() -> Bool {
return items.count != 0 || swipeItems.count != 0
}
func reloadStandardConfig() {
let presetPath = standardConfigPath
@ -161,7 +233,7 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
func reloadPreset(path: String) {
lastPresetPath = path
let items = path.fileData?.barItemDefinitions() ?? [BarItemDefinition(type: .staticButton(title: "bad preset"), action: .none, longAction: .none, additionalParameters: [:])]
let items = path.fileData?.barItemDefinitions() ?? [BarItemDefinition(type: .staticButton(title: "bad preset"), actions: [], action: .none, legacyLongAction: .none, additionalParameters: [:])]
createAndUpdatePreset(newJsonItems: items)
}
@ -186,8 +258,30 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
}
func createItems() {
items = [:]
swipeItems = []
for (identifier, definition) in itemDefinitions {
items[identifier] = createItem(forIdentifier: identifier, definition: definition)
var show = true
if let frontApp = frontmostApplicationIdentifier {
if case let .matchAppId(regexString)? = definition.additionalParameters[.matchAppId] {
let regex = try! NSRegularExpression(pattern: regexString)
let range = NSRange(location: 0, length: frontApp.count)
if regex.firstMatch(in: frontApp, range: range) == nil {
show = false
}
}
}
if show {
let item = createItem(forIdentifier: identifier, definition: definition)
if item is SwipeItem {
swipeItems.append(item as! SwipeItem)
} else {
items[identifier] = item
}
}
}
}
@ -200,54 +294,54 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
}
func updateControlStripPresence() {
DFRElementSetControlStripPresenceForIdentifier(.controlStripItem, true)
let showMtmrButtonOnControlStrip = touchBarContainsAnyItems()
DFRElementSetControlStripPresenceForIdentifier(.controlStripItem, showMtmrButtonOnControlStrip)
}
@objc private func presentTouchBar() {
if AppSettings.showControlStripState {
updateControlStripPresence()
presentSystemModal(touchBar, systemTrayItemIdentifier: .controlStripItem)
} else {
presentSystemModal(touchBar, placement: 1, systemTrayItemIdentifier: .controlStripItem)
}
updateControlStripPresence()
}
@objc private func dismissTouchBar() {
if touchBarContainsAnyItems() {
minimizeSystemModal(touchBar)
}
updateControlStripPresence()
}
@objc func resetControlStrip() {
dismissTouchBar()
presentTouchBar()
updateActiveApp()
}
func touchBar(_: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? {
if identifier == centerScrollArea {
return scrollArea
if identifier == basicViewIdentifier {
return basicView
}
guard let item = self.items[identifier],
let definition = self.itemDefinitions[identifier],
definition.align != .center else {
return nil
}
return item
}
func createItem(forIdentifier identifier: NSTouchBarItem.Identifier, definition item: BarItemDefinition) -> NSTouchBarItem? {
var barItem: NSTouchBarItem!
switch item.type {
case let .staticButton(title: title):
barItem = CustomButtonTouchBarItem(identifier: identifier, title: title)
case let .appleScriptTitledButton(source: source, refreshInterval: interval):
barItem = AppleScriptTouchBarItem(identifier: identifier, source: source, interval: interval)
case let .appleScriptTitledButton(source: source, refreshInterval: interval, alternativeImages: alternativeImages):
barItem = AppleScriptTouchBarItem(identifier: identifier, source: source, interval: interval, alternativeImages: alternativeImages)
case let .shellScriptTitledButton(source: source, refreshInterval: interval):
barItem = ShellScriptTouchBarItem(identifier: identifier, source: source, interval: interval)
case let .timeButton(formatTemplate: template, timeZone: timeZone, locale: locale):
barItem = TimeTouchBarItem(identifier: identifier, formatTemplate: template, timeZone: timeZone, locale: locale)
case .battery:
barItem = BatteryBarItem(identifier: identifier)
case let .cpu(refreshInterval: refreshInterval):
barItem = CPUBarItem(identifier: identifier, refreshInterval: refreshInterval)
case let .dock(autoResize: autoResize, filter: regexString):
if let regexString = regexString {
guard let regex = try? NSRegularExpression(pattern: regexString, options: []) else {
@ -288,17 +382,27 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
barItem = DnDBarItem(identifier: identifier)
case let .pomodoro(workTime: workTime, restTime: restTime):
barItem = PomodoroBarItem(identifier: identifier, workTime: workTime, restTime: restTime)
case let .network(flip: flip):
barItem = NetworkBarItem(identifier: identifier, flip: flip)
case let .network(flip: flip, units: units):
barItem = NetworkBarItem(identifier: identifier, flip: flip, units: units)
case .darkMode:
barItem = DarkModeBarItem(identifier: identifier)
case let .swipe(direction: direction, fingers: fingers, minOffset: minOffset, sourceApple: sourceApple, sourceBash: sourceBash):
barItem = SwipeItem(identifier: identifier, direction: direction, fingers: fingers, minOffset: minOffset, sourceApple: sourceApple, sourceBash: sourceBash)
case let .upnext(from: from, to: to, maxToShow: maxToShow, autoResize: autoResize):
barItem = UpNextScrubberTouchBarItem(identifier: identifier, interval: 60, from: from, to: to, maxToShow: maxToShow, autoResize: autoResize)
}
if let action = self.action(forItem: item), let item = barItem as? CustomButtonTouchBarItem {
item.tapClosure = action
item.actions.append(ItemAction(trigger: .singleTap, action))
}
if let longAction = self.longAction(forItem: item), let item = barItem as? CustomButtonTouchBarItem {
item.longTapClosure = longAction
item.actions.append(ItemAction(trigger: .longTap, longAction))
}
if let touchBarItem = barItem as? CustomButtonTouchBarItem {
for action in item.actions {
touchBarItem.actions.append(ItemAction(trigger: action.trigger, self.closure(for: action)))
}
}
if case let .bordered(bordered)? = item.additionalParameters[.bordered], let item = barItem as? CustomButtonTouchBarItem {
item.isBordered = bordered
@ -322,8 +426,52 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
return barItem
}
func closure(for action: Action) -> (() -> Void)? {
switch action.value {
case let .hidKey(keycode: keycode):
return { HIDPostAuxKey(keycode) }
case let .keyPress(keycode: keycode):
return { GenericKeyPress(keyCode: CGKeyCode(keycode)).send() }
case let .appleScript(source: source):
guard let appleScript = source.appleScript else {
print("cannot create apple script for item \(action)")
return {}
}
return {
DispatchQueue.appleScriptQueue.async {
var error: NSDictionary?
appleScript.executeAndReturnError(&error)
if let error = error {
print("error \(error) when handling \(action) ")
}
}
}
case let .shellScript(executable: executable, parameters: parameters):
return {
let task = Process()
task.launchPath = executable
task.arguments = parameters
task.launch()
}
case let .openUrl(url: url):
return {
if let url = URL(string: url), NSWorkspace.shared.open(url) {
#if DEBUG
print("URL was successfully opened")
#endif
} else {
print("error", url)
}
}
case let .custom(closure: closure):
return closure
case .none:
return nil
}
}
func action(forItem item: BarItemDefinition) -> (() -> Void)? {
switch item.action {
switch item.legacyAction {
case let .hidKey(keycode: keycode):
return { HIDPostAuxKey(keycode) }
case let .keyPress(keycode: keycode):
@ -367,7 +515,7 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
}
func longAction(forItem item: BarItemDefinition) -> (() -> Void)? {
switch item.longAction {
switch item.legacyLongAction {
case let .hidKey(keycode: keycode):
return { HIDPostAuxKey(keycode) }
case let .keyPress(keycode: keycode):
@ -419,6 +567,12 @@ extension NSCustomTouchBarItem: CanSetWidth {
}
}
extension NSPopoverTouchBarItem: CanSetWidth {
func setWidth(value: CGFloat) {
view?.widthAnchor.constraint(equalToConstant: value).isActive = true
}
}
extension BarItemDefinition {
var align: Align {
if case let .align(result)? = additionalParameters[.align] {

View File

@ -82,12 +82,14 @@ class AppScrubberTouchBarItem: NSCustomTouchBarItem {
public func createAppButton(for app: DockItem) -> DockBarItem {
let item = DockBarItem(app)
item.isBordered = false
item.tapClosure = { [weak self] in
item.actions.append(contentsOf: [
ItemAction(trigger: .singleTap) { [weak self] in
self?.switchToApp(app: app)
}
item.longTapClosure = { [weak self] in
},
ItemAction(trigger: .longTap) { [weak self] in
self?.handleHalfLongPress(item: app)
}
])
item.killAppClosure = {[weak self] in
self?.handleLongPress(item: app)
}

View File

@ -0,0 +1,82 @@
//
// CPUBarItem.swift
// MTMR
//
// Created by bobrosoft on 17/08/2021.
// Copyright © 2018 Anton Palgunov. All rights reserved.
//
import Foundation
class CPUBarItem: CustomButtonTouchBarItem {
private let refreshInterval: TimeInterval
private var refreshQueue: DispatchQueue? = DispatchQueue(label: "mtmr.cpu")
private let defaultSingleTapScript: NSAppleScript! = "activate application \"Activity Monitor\"\rtell application \"System Events\"\r\ttell process \"Activity Monitor\"\r\t\ttell radio button \"CPU\" of radio group 1 of group 2 of toolbar 1 of window 1 to perform action \"AXPress\"\r\tend tell\rend tell".appleScript
init(identifier: NSTouchBarItem.Identifier, refreshInterval: TimeInterval) {
self.refreshInterval = refreshInterval
super.init(identifier: identifier, title: "")
// Set default image
if self.image == nil {
self.image = #imageLiteral(resourceName: "cpu").resize(maxSize: NSSize(width: 24, height: 24));
}
// Set default action
if actions.filter({ $0.trigger == .singleTap }).isEmpty {
actions.append(ItemAction(
trigger: .singleTap,
defaultTapAction
))
}
refreshAndSchedule()
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func refreshAndSchedule() {
DispatchQueue.main.async {
// Get CPU load
let usage = 100 - CPU.systemUsage().idle
guard usage.isFinite else {
return
}
// Choose color based on CPU load
var color: NSColor? = nil
var bgColor: NSColor? = nil
if usage > 70 {
color = .black
bgColor = .yellow
} else if usage > 30 {
color = .yellow
}
// Update layout
let attrTitle = NSMutableAttributedString.init(attributedString: String(format: "%.1f%%", usage).defaultTouchbarAttributedString)
if let color = color {
attrTitle.addAttributes([.foregroundColor: color], range: NSRange(location: 0, length: attrTitle.length))
}
self.attributedTitle = attrTitle
self.backgroundColor = bgColor
}
refreshQueue?.asyncAfter(deadline: .now() + refreshInterval) { [weak self] in
self?.refreshAndSchedule()
}
}
func defaultTapAction() {
refreshQueue?.async { [weak self] in
self?.defaultSingleTapScript.executeAndReturnError(nil)
}
}
deinit {
refreshQueue?.suspend()
refreshQueue = nil
}
}

View File

@ -15,6 +15,9 @@ class CurrencyBarItem: CustomButtonTouchBarItem {
private var postfix: String
private var from: String
private var to: String
private var decimal: Int
private var decimalValue: Float32!
private var decimalString: String!
private var oldValue: Float32!
private var full: Bool = false
@ -32,10 +35,39 @@ class CurrencyBarItem: CustomButtonTouchBarItem {
"IDR": "Rp",
"MXN": "$",
"SGD": "$",
"CHF": "Fr.",
"BTC": "฿",
"LTC": "Ł",
"ETH": "Ξ",
"SOL": "",
"DOT": "",
"DOGE": "Ð",
"XMR": "ɱ",
"ADA": "",
"PLN": "",
"UAH": "",
]
private let decimals = [
"USD": 4,
"EUR": 4,
"RUB": 2,
"JPY": 2,
"GBP": 4,
"CAD": 4,
"KRW": 4,
"CNY": 4,
"AUD": 4,
"BRL": 4,
"IDR": 1,
"MXN": 2,
"SGD": 4,
"CHF": 4,
"BTC": 3,
"LTC": 2,
"ETH": 2,
"DOT": 3,
"DOGE": 4,
"ADA": 3,
"USDT": 3
]
init(identifier: NSTouchBarItem.Identifier, interval: TimeInterval, from: String, to: String, full: Bool) {
@ -57,6 +89,14 @@ class CurrencyBarItem: CustomButtonTouchBarItem {
postfix = to
}
if let decimal = decimals[to] {
self.decimal = decimal
} else {
decimal = 2
}
super.init(identifier: identifier, title: "")
activity.repeats = true
@ -114,10 +154,12 @@ class CurrencyBarItem: CustomButtonTouchBarItem {
}
oldValue = value
decimalValue = (value * pow(10,Float(decimal))).rounded() / pow(10,Float(decimal))
decimalString = String(decimalValue)
var title = ""
if full {
title = String(format: "%@‣%.2f%@", prefix, value, postfix)
title = String(format: "%@%@‣%@", prefix, postfix, decimalString)
} else {
title = String(format: "%@%.2f", prefix, value)
}

View File

@ -11,7 +11,7 @@ class DarkModeBarItem: CustomButtonTouchBarItem, Widget {
isBordered = false
setWidth(value: 24)
tapClosure = { [weak self] in self?.DarkModeToggle() }
actions.append(ItemAction(trigger: .singleTap) { [weak self] in self?.DarkModeToggle() })
timer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(refresh), userInfo: nil, repeats: true)

View File

@ -16,7 +16,7 @@ class DnDBarItem: CustomButtonTouchBarItem {
isBordered = false
setWidth(value: 32)
tapClosure = { [weak self] in self?.DnDToggle() }
actions.append(ItemAction(trigger: .singleTap) { [weak self] in self?.DnDToggle() })
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(refresh), userInfo: nil, repeats: true)

View File

@ -18,9 +18,10 @@ class InputSourceBarItem: CustomButtonTouchBarItem {
observeIputSourceChangedNotification()
textInputSourceDidChange()
tapClosure = { [weak self] in
actions.append(ItemAction(trigger: .singleTap) { [weak self] in
self?.switchInputSource()
}
})
}
required init?(coder _: NSCoder) {

View File

@ -11,6 +11,7 @@ import ScriptingBridge
class MusicBarItem: CustomButtonTouchBarItem {
private enum Player: String {
case Music = "com.apple.Music"
case iTunes = "com.apple.iTunes"
case Spotify = "com.spotify.client"
case VOX = "com.coppertino.Vox"
@ -19,6 +20,7 @@ class MusicBarItem: CustomButtonTouchBarItem {
}
private let playerBundleIdentifiers = [
Player.Music,
Player.iTunes,
Player.Spotify,
Player.VOX,
@ -40,8 +42,11 @@ class MusicBarItem: CustomButtonTouchBarItem {
super.init(identifier: identifier, title: "")
isBordered = false
tapClosure = { [weak self] in self?.playPause() }
longTapClosure = { [weak self] in self?.nextTrack() }
actions = [
ItemAction(trigger: .singleTap) { [weak self] in self?.playPause() },
ItemAction(trigger: .doubleTap) { [weak self] in self?.previousTrack() },
ItemAction(trigger: .longTap) { [weak self] in self?.nextTrack() }
]
refreshAndSchedule()
}
@ -71,6 +76,10 @@ class MusicBarItem: CustomButtonTouchBarItem {
let mp = (musicPlayer as iTunesApplication)
mp.playpause!()
return
} else if ident == .Music {
let mp = (musicPlayer as MusicApplication)
mp.playpause!()
return
} else if ident == .VOX {
let mp = (musicPlayer as VoxApplication)
mp.playpause!()
@ -134,6 +143,11 @@ class MusicBarItem: CustomButtonTouchBarItem {
mp.nextTrack!()
updatePlayer()
return
} else if ident == .Music {
let mp = (musicPlayer as MusicApplication)
mp.nextTrack!()
updatePlayer()
return
} else if ident == .VOX {
let mp = (musicPlayer as VoxApplication)
mp.next!()
@ -167,6 +181,31 @@ class MusicBarItem: CustomButtonTouchBarItem {
}
}
@objc func previousTrack() {
for ident in playerBundleIdentifiers {
if let musicPlayer = SBApplication(bundleIdentifier: ident.rawValue) {
if musicPlayer.isRunning {
if ident == .Spotify {
let mp = (musicPlayer as SpotifyApplication)
mp.previousTrack!()
updatePlayer()
return
} else if ident == .iTunes {
let mp = (musicPlayer as iTunesApplication)
mp.previousTrack!()
updatePlayer()
return
} else if ident == .Music {
let mp = (musicPlayer as MusicApplication)
mp.previousTrack!()
updatePlayer()
return
}
}
}
}
}
func refreshAndSchedule() {
DispatchQueue.main.async {
self.updatePlayer()
@ -188,6 +227,8 @@ class MusicBarItem: CustomButtonTouchBarItem {
tempTitle = (musicPlayer as SpotifyApplication).title
} else if ident == .iTunes {
tempTitle = (musicPlayer as iTunesApplication).title
} else if ident == .Music {
tempTitle = (musicPlayer as MusicApplication).title
} else if ident == .VOX {
tempTitle = (musicPlayer as VoxApplication).title
} else if ident == .Safari {
@ -321,6 +362,29 @@ extension iTunesApplication {
}
}
@objc protocol MusicApplication {
@objc optional var currentTrack: MusicTrack { get }
@objc optional func playpause()
@objc optional func nextTrack()
@objc optional func previousTrack()
}
extension SBApplication: MusicApplication {}
@objc protocol MusicTrack {
@objc optional var artist: String { get }
@objc optional var name: String { get }
}
extension SBObject: MusicTrack {}
extension MusicApplication {
var title: String {
guard let t = currentTrack else { return "" }
return (t.artist ?? "") + "" + (t.name ?? "")
}
}
@objc protocol VoxApplication {
@objc optional func playpause()
@objc optional func next()

View File

@ -13,9 +13,11 @@ class NetworkBarItem: CustomButtonTouchBarItem, Widget {
static var identifier: String = "com.toxblh.mtmr.network"
private let flip: Bool
private let units: String
init(identifier: NSTouchBarItem.Identifier, flip: Bool = false) {
init(identifier: NSTouchBarItem.Identifier, flip: Bool = false, units: String) {
self.flip = flip
self.units = units
super.init(identifier: identifier, title: " ")
startMonitoringProcess()
}
@ -87,14 +89,41 @@ class NetworkBarItem: CustomButtonTouchBarItem, Widget {
func getHumanizeSize(speed: UInt64) -> String {
let humanText: String
func speedB(speed: UInt64)-> String {
return String(format: "%.0f", Double(speed)) + " B/s"
}
func speedKB(speed: UInt64)-> String {
return String(format: "%.1f", Double(speed) / 1024) + " KB/s"
}
func speedMB(speed: UInt64)-> String {
return String(format: "%.1f", Double(speed) / (1024 * 1024)) + " MB/s"
}
func speedGB(speed: UInt64)-> String {
return String(format: "%.2f", Double(speed) / (1024 * 1024 * 1024)) + " GB/s"
}
switch self.units {
case "B/s":
humanText = speedB(speed: speed)
case "KB/s":
humanText = speedKB(speed: speed)
case "MB/s":
humanText = speedMB(speed: speed)
case "GB/s":
humanText = speedGB(speed: speed)
default:
if speed < 1024 {
humanText = String(format: "%.0f", Double(speed)) + " B/s"
humanText = speedB(speed: speed)
} else if speed < (1024 * 1024) {
humanText = String(format: "%.1f", Double(speed) / 1024) + " KB/s"
humanText = speedKB(speed: speed)
} else if speed < (1024 * 1024 * 1024) {
humanText = String(format: "%.1f", Double(speed) / (1024 * 1024)) + " MB/s"
humanText = speedMB(speed: speed)
} else {
humanText = String(format: "%.2f", Double(speed) / (1024 * 1024 * 1024)) + " GB/s"
humanText = speedGB(speed: speed)
}
}
return humanText

View File

@ -31,7 +31,7 @@ class NightShiftBarItem: CustomButtonTouchBarItem {
isBordered = false
setWidth(value: 28)
tapClosure = { [weak self] in self?.nightShiftAction() }
actions.append(ItemAction(trigger: .singleTap) { [weak self] in self?.nightShiftAction() })
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(refresh), userInfo: nil, repeats: true)

View File

@ -23,8 +23,9 @@ class PomodoroBarItem: CustomButtonTouchBarItem, Widget {
return (
item: .pomodoro(workTime: workTime ?? 1500.00, restTime: restTime ?? 300),
action: .none,
longAction: .none,
actions: [],
legacyAction: .none,
legacyLongAction: .none,
parameters: [:]
)
}
@ -35,7 +36,7 @@ class PomodoroBarItem: CustomButtonTouchBarItem, Widget {
case none
}
private let defaultTitle = "🍅"
private let defaultTitle = "🍅 "
private let workTime: TimeInterval
private let restTime: TimeInterval
private var typeTime: TimeTypes = .none
@ -50,8 +51,10 @@ class PomodoroBarItem: CustomButtonTouchBarItem, Widget {
self.workTime = workTime
self.restTime = restTime
super.init(identifier: identifier, title: defaultTitle)
tapClosure = { [weak self] in self?.startStopWork() }
longTapClosure = { [weak self] in self?.startStopRest() }
actions.append(contentsOf: [
ItemAction(trigger: .singleTap) { [weak self] in self?.startStopWork() },
ItemAction(trigger: .longTap) { [weak self] in self?.startStopRest() }
])
}
required init?(coder _: NSCoder) {

View File

@ -0,0 +1,256 @@
//
// UpNextScrubberTouchBarItems.swift
// MTMR
//
// Created by Connor Meehan on 13/7/20.
// Copyright © 2020 Anton Palgunov. All rights reserved.
//
//
import Foundation
import EventKit
class UpNextScrubberTouchBarItem: NSCustomTouchBarItem {
// Dependencies
private let scrollView = NSScrollView()
private let activity: NSBackgroundActivityScheduler // Update scheduler
private var eventSources : [IUpNextSource] = []
private var items: [UpNextItem] = []
// Settings
private var futureSearchCutoff: Double
private var pastSearchCutoff: Double
private var maxToShow: Int
private var widthConstraint: NSLayoutConstraint?
private var autoResize: Bool = false
/// <#Description#>
/// - Parameters:
/// - identifier: Unique identifier of widget
/// - interval: Update view interval in seconds
/// - from: Relative to current time, how far back we search for events in hours
/// - to: Relative to current time, how far forward we search for events in hours
/// - maxToShow: Which event to show (1 is first, 2 is second, and so on)
init(identifier: NSTouchBarItem.Identifier, interval: TimeInterval, from: Double, to: Double, maxToShow: Int, autoResize: Bool) {
// Initialise member properties
activity = NSBackgroundActivityScheduler(identifier: "\(identifier.rawValue).updateCheck")
pastSearchCutoff = from * 3600
futureSearchCutoff = to * 3600
self.maxToShow = maxToShow
self.autoResize = autoResize
UpNextItem.df.dateFormat = "HH:mm"
// Error handling
if (maxToShow <= 0) {
fatalError("Error on UpNext bar item. maxToShow property must be greater than 0.")
}
// Init super
super.init(identifier: identifier)
view = scrollView
// Add event sources
// Can optionally pass an update view callback to an event source to redraw element
self.eventSources.append(UpNextCalenderSource(updateCallback: self.updateView))
// Fallback interactivity via interval
activity.interval = interval
activity.repeats = true
activity.qualityOfService = .utility
activity.schedule { (completion: NSBackgroundActivityScheduler.CompletionHandler) in
self.updateView()
completion(NSBackgroundActivityScheduler.Result.finished)
}
updateView()
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateView() -> Void {
items = []
var upcomingEvents = self.getUpcomingEvents()
upcomingEvents.sort(by: {$0.startDate.compare($1.startDate) == .orderedAscending})
var index = 1
DispatchQueue.main.async {
for event in upcomingEvents {
// Create UpNextItem
let item = UpNextItem(event: event)
item.backgroundColor = self.getBackgroundColor(startDate: event.startDate)
// Bind tap event
item.actions.append(ItemAction(trigger: .singleTap) { [weak self] in
self?.switchToApp(event: event)
})
// Add to view
self.items.append(item)
// Check if should display any more
if (index == self.maxToShow) {
break;
}
index += 1
}
self.reloadData()
self.updateSize()
}
}
private func reloadData() {
let stackView = NSStackView(views: items.compactMap { $0.view })
stackView.spacing = 5
stackView.orientation = .horizontal
let visibleRect = self.scrollView.documentVisibleRect
self.scrollView.documentView = stackView
stackView.scroll(visibleRect.origin)
}
func updateSize() {
if self.autoResize {
self.widthConstraint?.isActive = false
let width = self.scrollView.documentView?.fittingSize.width ?? 0
self.widthConstraint = self.scrollView.widthAnchor.constraint(equalToConstant: width)
self.widthConstraint!.isActive = true
}
}
private func getUpcomingEvents() -> [UpNextEventModel] {
var upcomingEvents: [UpNextEventModel] = []
// Calculate the range we're going to search for events in
let dateLowerBounds = Date(timeIntervalSinceNow: self.pastSearchCutoff)
let dateUpperBounds = Date(timeIntervalSinceNow: self.futureSearchCutoff)
// Get all events from all sources
for eventSource in self.eventSources {
if (eventSource.hasPermission) {
let events = eventSource.getUpcomingEvents(dateLowerBounds: dateLowerBounds, dateUpperBounds: dateUpperBounds)
upcomingEvents.append(contentsOf: events)
}
}
return upcomingEvents
}
public func switchToApp(event: UpNextEventModel) {
var bundleIdentifier: String
switch(event.sourceType) {
case .iCalendar:
bundleIdentifier = UpNextCalenderSource.bundleIdentifier
}
NSWorkspace.shared.launchApplication(withBundleIdentifier: bundleIdentifier, options: [.default], additionalEventParamDescriptor: nil, launchIdentifier: nil)
// NB: if you can't open app which on another space, try to check mark
// "When switching to an application, switch to a Space with open windows for the application"
// in Mission control settings
}
func getBackgroundColor(startDate: Date) -> NSColor {
let distance = startDate.timeIntervalSinceReferenceDate/60 - Date().timeIntervalSinceReferenceDate/60 // Get time difference in minutes
if (distance < 0 as TimeInterval) { // If it's in the past, draw as blue
return NSColor.systemBlue
} else if (distance < 30 as TimeInterval) { // Less than 30 minutes, backround is red
return NSColor.systemRed
}
return NSColor.clear
}
}
private class UpNextItem : CustomButtonTouchBarItem {
static public let df = DateFormatter()
init(event: UpNextEventModel) {
let identifier = UpNextItem.getIdentifier(event: event)
let title = UpNextItem.getTitle(event: event)
super.init(identifier: NSTouchBarItem.Identifier(rawValue: identifier), title: title)
}
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private static func getTitle(event: UpNextEventModel) -> String {
var title = ""
let startDateString = UpNextItem.df.string(for: event.startDate)
switch event.sourceType {
case .iCalendar:
title = String.init(format: "🗓 %@ - %@ ", event.title, startDateString!)
}
return title
}
private static func getIdentifier(event: UpNextEventModel) -> String {
var identifier : String
switch event.sourceType {
case .iCalendar:
identifier = "com.mtmr.iCalendarEvent"
}
return identifier + "." + event.title
}
}
enum UpNextSourceType {
case iCalendar
}
// Model for events to be displayed in dock
struct UpNextEventModel {
let title: String
let startDate: Date
let sourceType: UpNextSourceType
}
// Interface for any event source
protocol IUpNextSource {
static var bundleIdentifier: String { get }
var hasPermission : Bool { get }
var updateCallback : () -> Void { get set }
init(updateCallback: @escaping () -> Void)
func getUpcomingEvents(dateLowerBounds: Date, dateUpperBounds: Date) -> [UpNextEventModel]
}
class UpNextCalenderSource : IUpNextSource {
static public let bundleIdentifier: String = "com.apple.iCal"
public var hasPermission: Bool = false
private var eventStore : EKEventStore
internal var updateCallback: () -> Void
required init(updateCallback: @escaping () -> Void = {}) {
self.updateCallback = updateCallback
eventStore = EKEventStore()
NotificationCenter.default.addObserver(forName: .EKEventStoreChanged, object: eventStore, queue: nil, using: handleUpdate)
let authStatus = EKEventStore.authorizationStatus(for: .event)
if (authStatus != .authorized) {
eventStore.requestAccess(to: .event){ granted, error in
self.hasPermission = granted;
self.handleUpdate()
if(!granted) {
NSLog("Error: MTMR UpNextBarWidget not given calendar access.")
return
}
}
} else {
self.handleUpdate()
}
}
public func handleUpdate() {
self.handleUpdate(note: Notification(name: Notification.Name("refresh view")))
}
public func handleUpdate(note: Notification) {
self.updateCallback()
}
public func getUpcomingEvents(dateLowerBounds: Date, dateUpperBounds: Date) -> [UpNextEventModel] {
var upcomingEvents: [UpNextEventModel] = []
let calendars = self.eventStore.calendars(for: .event)
let predicate = self.eventStore.predicateForEvents(withStart: dateLowerBounds, end: dateUpperBounds, calendars: calendars)
let events = self.eventStore.events(matching: predicate)
for event in events {
upcomingEvents.append(UpNextEventModel(title: event.title, startDate: event.startDate, sourceType: UpNextSourceType.iCalendar))
}
return upcomingEvents
}
}

View File

@ -5,18 +5,11 @@ import CoreAudio
class VolumeViewController: NSCustomTouchBarItem {
private(set) var sliderItem: CustomSlider!
private var currentDeviceId: AudioObjectID = AudioObjectID(0)
init(identifier: NSTouchBarItem.Identifier, image: NSImage? = nil) {
super.init(identifier: identifier)
var forPropertyAddress = AudioObjectPropertyAddress(
mSelector: kAudioHardwareServiceDeviceProperty_VirtualMasterVolume,
mScope: kAudioDevicePropertyScopeOutput,
mElement: kAudioObjectPropertyElementMaster
)
AudioObjectAddPropertyListenerBlock(defaultDeviceID, &forPropertyAddress, nil, audioObjectPropertyListenerBlock)
if image == nil {
sliderItem = CustomSlider()
} else {
@ -29,6 +22,49 @@ class VolumeViewController: NSCustomTouchBarItem {
sliderItem.floatValue = getInputGain() * 100
view = sliderItem
currentDeviceId = defaultDeviceID
self.addAudioRouteChangedListener()
self.addCurrentAudioVolumeChangedListener()
}
private func addAudioRouteChangedListener() {
let audioId = AudioObjectID(bitPattern: kAudioObjectSystemObject)
var forPropertyAddress = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDefaultOutputDevice,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMaster)
AudioObjectAddPropertyListenerBlock(audioId, &forPropertyAddress, nil, audioRouteChanged)
}
func audioRouteChanged(numberAddresses _: UInt32, addresses _: UnsafePointer<AudioObjectPropertyAddress>) {
self.removeLastAudioVolumeChangeListener()
currentDeviceId = defaultDeviceID
self.addCurrentAudioVolumeChangedListener()
DispatchQueue.main.async {
self.sliderItem.floatValue = self.getInputGain() * 100
}
}
private func addCurrentAudioVolumeChangedListener() {
var forPropertyAddress = AudioObjectPropertyAddress(
mSelector: kAudioHardwareServiceDeviceProperty_VirtualMainVolume,
mScope: kAudioDevicePropertyScopeOutput,
mElement: kAudioObjectPropertyElementMaster
)
AudioObjectAddPropertyListenerBlock(defaultDeviceID, &forPropertyAddress, nil, audioObjectPropertyListenerBlock)
}
private func removeLastAudioVolumeChangeListener() {
var forPropertyAddress = AudioObjectPropertyAddress(
mSelector: kAudioHardwareServiceDeviceProperty_VirtualMainVolume,
mScope: kAudioDevicePropertyScopeOutput,
mElement: kAudioObjectPropertyElementMaster
)
AudioObjectRemovePropertyListenerBlock(currentDeviceId, &forPropertyAddress, nil, audioObjectPropertyListenerBlock)
}
func audioObjectPropertyListenerBlock(numberAddresses _: UInt32, addresses _: UnsafePointer<AudioObjectPropertyAddress>) {
@ -66,7 +102,7 @@ class VolumeViewController: NSCustomTouchBarItem {
var volume: Float32 = 0.5
var size: UInt32 = UInt32(MemoryLayout.size(ofValue: volume))
var address: AudioObjectPropertyAddress = AudioObjectPropertyAddress()
address.mSelector = AudioObjectPropertySelector(kAudioHardwareServiceDeviceProperty_VirtualMasterVolume)
address.mSelector = AudioObjectPropertySelector(kAudioHardwareServiceDeviceProperty_VirtualMainVolume)
address.mScope = AudioObjectPropertyScope(kAudioDevicePropertyScopeOutput)
address.mElement = AudioObjectPropertyElement(kAudioObjectPropertyElementMaster)
AudioObjectGetPropertyData(defaultDeviceID, &address, 0, nil, &size, &volume)
@ -86,7 +122,7 @@ class VolumeViewController: NSCustomTouchBarItem {
var address: AudioObjectPropertyAddress = AudioObjectPropertyAddress()
address.mScope = AudioObjectPropertyScope(kAudioDevicePropertyScopeOutput)
address.mElement = AudioObjectPropertyElement(kAudioObjectPropertyElementMaster)
address.mSelector = AudioObjectPropertySelector(kAudioHardwareServiceDeviceProperty_VirtualMasterVolume)
address.mSelector = AudioObjectPropertySelector(kAudioHardwareServiceDeviceProperty_VirtualMainVolume)
return AudioObjectSetPropertyData(defaultDeviceID, &address, 0, nil, size, &inputVolume)
}

View File

@ -12,10 +12,27 @@ import CoreLocation
class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate {
private let activity: NSBackgroundActivityScheduler
private let unitsStr = "°C"
private let iconsSource = ["Ясно": "☀️", "Малооблачно": "🌤", "Облачно с прояснениями": "⛅️", "Пасмурно": "☁️", "Небольшой дождь": "🌦", "Дождь": "🌧", "Ливень": "", "Гроза": "🌩", "Дождь со снегом": "☔️", "Небольшой снег": "❄️", "Снег": "🌨", "Туман": "🌫"]
private let iconsSource = [
"clear": "☀️",
"mostly-clear": "🌤",
"partly-cloudy": "⛅️",
"overcast": "☁️",
"cloudy": "☁️",
"light-rain": "🌦",
"drizzle": "💦",
"rain": "🌧",
"heavy-rain": "",
"storm": "🌩",
"thunderstorm-with-rain": "",
"sleet": "☔️",
"light-snow": "❄️",
"snow": "🌨",
"fog": "🌫"
]
private var location: CLLocation!
private var prevLocation: CLLocation!
private var manager: CLLocationManager!
private var updateWeatherTask: URLSessionDataTask?
init(identifier: NSTouchBarItem.Identifier, interval: TimeInterval) {
activity = NSBackgroundActivityScheduler(identifier: "\(identifier.rawValue).updatecheck")
@ -47,7 +64,12 @@ class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
manager.startUpdatingLocation()
tapClosure = tapClosure ?? defaultTapAction
if actions.filter({ $0.trigger == .singleTap }).isEmpty {
actions.append(ItemAction(
trigger: .singleTap,
defaultTapAction
))
}
}
required init?(coder _: NSCoder) {
@ -58,7 +80,8 @@ class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate
var urlRequest = URLRequest(url: URL(string: getWeatherUrl())!)
urlRequest.addValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36", forHTTPHeaderField: "user-agent") // important for the right format
let task = URLSession.shared.dataTask(with: urlRequest) { data, _, error in
updateWeatherTask?.cancel()
updateWeatherTask = URLSession.shared.dataTask(with: urlRequest) { data, _, error in
guard error == nil, let response = data?.utf8string else {
return
}
@ -70,7 +93,7 @@ class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate
temperature = matches.first?.item(at: 1)
var icon: String?
matches = response.matchingStrings(regex: "link__condition.*?>(.*?)<")
matches = response.matchingStrings(regex: "\"condition\":\"(.*?)\"")
icon = matches.first?.item(at: 1)
if let _ = icon, let test = self.iconsSource[icon!] {
icon = test
@ -83,14 +106,14 @@ class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate
}
}
task.resume()
updateWeatherTask?.resume()
}
func getWeatherUrl() -> String {
if location != nil {
return "https://yandex.ru/pogoda/?lat=\(location.coordinate.latitude)&lon=\(location.coordinate.longitude)"
return "https://yandex.ru/pogoda/?lat=\(location.coordinate.latitude)&lon=\(location.coordinate.longitude)&lang=ru"
} else {
return "https://yandex.ru/pogoda/" // Yandex will try to determine your location by default
return "https://yandex.ru/pogoda/?lang=ru" // Yandex will try to determine your location by default
}
}
@ -133,7 +156,7 @@ extension String {
let nsString = self as NSString
let results = regex.matches(in: self, options: [], range: NSMakeRange(0, nsString.length))
return results.map { result in
(0..<result.numberOfRanges).map {
(0 ..< result.numberOfRanges).map {
result.range(at: $0).location != NSNotFound
? nsString.substring(with: result.range(at: $0))
: ""

View File

@ -28,6 +28,26 @@
}
},
// Music
{
"type": "appleScriptTitledButton",
"source": {
"inline": "if application \"Music\" is running then\rtell application \"Music\"\rif player state is playing then\rreturn (get artist of current track) & \" \" & (get name of current track)\relse\rreturn \"\"\rend if\rend tell\rend if\rreturn \"\"\r"
},
"action": "appleScript",
"actionAppleScript": {
"inline": "if application \"Music\" is running then\rtell application \"Music\"\rif player state is playing then\rnext track\rend if\rend tell\rend if\r"
},
"longAction": "appleScript",
"longActionAppleScript": {
"inline": "if application \"Music\" is running then\rtell application \"Music\"\rif player state is playing then\rprev track\rend if\rend tell\rend if\r"
},
"refreshInterval": 2,
"image": {
"base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAADAUExURUdwTOVVZCzB+3qc0gkDBgEBAgcKEwAAAA4EBP5aVU2V95iJv7V3rtBOvH5W6jaOyclScKZGX3wuQCMuUqZN7+NQYXtDvFd9/sxYni2z6UhBhyhvnIp7sf38/PXz9ePm69/k6fHv8/j29+3q7/v6+ufq7uvu8fTw8+1egOFki/dbboVj/HNy/T+j/dNtnEul81vC8Vmf3OeRqOBVofK4xZfF7sDb7PLe6LxU1KKK79PL6vrW3fh4g5Zi4bi16daa0A3Qc90AAAAddFJOUwD3/v0uOlYNG/z+/v7998OYYztt/Le2/eDqi5jEo2rNTwAABMtJREFUWMPdl1tjqjoQhauC0Hqvta26JVxUQBQFRPBS/f//as8kISBqd/twXs5KjBDyrZkEFfP09D+WoigqFxz+mlbVXncweEZVBoNuD1x+E1vtMVZoURn01J/mob52KRNuj4k5mZjJcRueFotFpfv6EwdVRfy0tSdXSranheN0/zkRSB7x42Q6udExdBy3972Fog4o/kBg4X6bhPJaeX7eTqeT6b0MJtPpdOu6n49XQgX+dOQ8vpWEvYn/2AHiL052PpoROcsOtND17ztQ3rwTuCQz9O/moKiMf6BkG/puKBzurKQ64PmbU2bDzUxk3Uql4lZcl3Vpvt9VbxLoLZwjY7E1WcNZoB0XpbELie/3Sg6KVHG2jGPs1LTCE2UXFfgIgtBgyq8d/E/pehJq1zmZGc7kAPsMX4Ec932T25uX5vUklFcHJlDU1OT4wllkvOtn9lrSbF7dCUggNEtaMOXhQZq4WkpBcksJQBCOnyjvM4P8KqQgFW9BFJka2NMKB1gw+VMxvN9smnwI1EuzpxS+g9FWYySjsTpOtnq+H162iW01m/wyXLUPzT/5HKQoSjQmU8vE6TAElvWggbhuNRpScQa002bVtJmBCz9qNusWBkJmoyHmoHajC4yybVujhR26mJVha7lDo2FrhnA4N0aq+BpE24zjgsMoEsfU0AADaKCwemiIRZA+o6N9oygyMi/EAWk0DMNgFvCmN/5IwqCV3PCGzzIwbINrzgwykVq2iorUalm2UTZotXKWqVYz5uBjzDUoxrxWyzKQWy061LZsNIJ3PAMDIcbVauwdGmxrNblgYNnCgStN54ylBSsYoAxerwwgJsCWTS0sepym0Mdp1gYBw5lmwgDXIDEoaeHLYE36BafzuQWFQ9RASM/XQPpMD5YQ2gA/AwPArQJyZWDsgo64C+/pBRkDG4s31hdmwFNAGz1mBjPukBs8qdSgLDBA1LJm1lw/14IgWAdAQ5nhax4HY/FR7qfpHQMPUChzS0c6eFmv17MZo7HZBP3MQJHTNCnzxPMgOFUteAGtMwMmfRPIijBgc+AmNK6+9zw+9Iw05YsG8aaT/7Kro7eUcoSWw3n/1W57SxgGOZCYxl+VDDabcf6LpNTf3g6IQ4XY7TbiyyUBmhqsViswWK02cE7ITIdmtxnWlcJz6f1tPyMsha+2R4UGyJPdywotwGSHOKHdm+IMYA40BRhtkXOb42DAh8crppjw8CyB4nMBlvFtD/0WSfZebkDHguLNar2JdyTXptqRrx6OmMKZkISQc4Yv9yyDXHiiEx1qXL1OAFdhBJPAAQeRQDEiRZEm+kwnu2p1XHo64yQ8j47bL1kCZ87pDKWxuW4mQJ9O9ba31xE5Y/rnA4VoTCJQwvnyBNgk+pkDi8sSJjlKRPxhX7r3Lytz0LPMi1H1Qv7VuzwuAzh4h1ukKFi/YV9+9E8THZbne2Ezxd/xsNGQ6u+wgoeH4SH9Tl367t+2Ko/acA8Oj/DhWP7X/30Zkvj4WMYlj10MOISXf7DlkPvvH6g43u0oCzDS1U5f/sHWC3d7cn3UAQf4HeHfwxXQY4yu/HTDKNXro3Gngw4vw2FnPKrXJfUXu0fqIdeFZOnXm08FTRSxcf391pW7oNGT8vRf6i9jqljwYzAm6AAAAABJRU5ErkJggg=="
}
},
// iTunes
{
"type": "appleScriptTitledButton",

View File

@ -6,7 +6,7 @@ class AppleScriptDefinitionTests: XCTestCase {
[ { "type": "appleScriptTitledButton", "source": { "inline": "tell everything fine" } } ]
""".data(using: .utf8)!
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonNoActionFixture)
guard case .appleScriptTitledButton(let source, _)? = result?.first?.type else {
guard case .appleScriptTitledButton(let source, _, _)? = result?.first?.type else {
XCTFail()
return
}
@ -18,7 +18,7 @@ class AppleScriptDefinitionTests: XCTestCase {
[ { "type": "appleScriptTitledButton", "source": { "filePath": "/ololo/pew" } } ]
""".data(using: .utf8)!
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonNoActionFixture)
guard case .appleScriptTitledButton(let source, _)? = result?.first?.type else {
guard case .appleScriptTitledButton(let source, _, _)? = result?.first?.type else {
XCTFail()
return
}
@ -32,7 +32,7 @@ class AppleScriptDefinitionTests: XCTestCase {
[ { "type": "appleScriptTitledButton", "source": { "filePath": "~/pew" } } ]
""".data(using: .utf8)!
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonNoActionFixture)
guard case .appleScriptTitledButton(let source, _)? = result?.first?.type else {
guard case .appleScriptTitledButton(let source, _, _)? = result?.first?.type else {
XCTFail()
return
}
@ -58,7 +58,7 @@ class AppleScriptDefinitionTests: XCTestCase {
[ { "type": "appleScriptTitledButton", "source": { "inline": "tell everything fine" }, "refreshInterval": 305} ]
""".data(using: .utf8)!
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonNoActionFixture)
guard case .appleScriptTitledButton(_, 305)? = result?.first?.type else {
guard case .appleScriptTitledButton(_, 305, _)? = result?.first?.type else {
XCTFail()
return
}

View File

@ -10,7 +10,7 @@ class ParseConfig: XCTestCase {
XCTFail()
return
}
guard case .none? = result?.first?.action else {
guard result?.first?.actions.count == 0 else {
XCTFail()
return
}
@ -18,14 +18,29 @@ class ParseConfig: XCTestCase {
func testButtonKeyCodeAction() {
let buttonKeycodeFixture = """
[ { "type": "staticButton", "title": "Pew", "action": "hidKey", "keycode": 123} ]
[ { "type": "staticButton", "title": "Pew", "actions": [ { "trigger": "singleTap", "action": "hidKey", "keycode": 123 } ] } ]
""".data(using: .utf8)!
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonKeycodeFixture)
guard case .staticButton("Pew")? = result?.first?.type else {
XCTFail()
return
}
guard case .hidKey(keycode: 123)? = result?.first?.action else {
guard case .hidKey(keycode: 123)? = result?.first?.actions.filter({ $0.trigger == .singleTap }).first?.value else {
XCTFail()
return
}
}
func testButtonKeyCodeLegacyAction() {
let buttonKeycodeFixture = """
[ { "type": "staticButton", "title": "Pew", "action": "hidKey", "keycode": 123 } ]
""".data(using: .utf8)!
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonKeycodeFixture)
guard case .staticButton("Pew")? = result?.first?.type else {
XCTFail()
return
}
guard case .hidKey(keycode: 123)? = result?.first?.legacyAction else {
XCTFail()
return
}
@ -40,7 +55,7 @@ class ParseConfig: XCTestCase {
XCTFail()
return
}
guard case .keyPress(keycode: 53)? = result?.first?.action else {
guard case .keyPress(keycode: 53)? = result?.first?.actions.filter({ $0.trigger == .singleTap }).first?.value else {
XCTFail()
return
}
@ -55,7 +70,7 @@ class ParseConfig: XCTestCase {
XCTFail()
return
}
guard case .keyPress(keycode: 53)? = result?.first?.action else {
guard case .keyPress(keycode: 53)? = result?.first?.actions.filter({ $0.trigger == .singleTap }).first?.value else {
XCTFail()
return
}

236
README.md
View File

@ -14,11 +14,11 @@ My idea is to create a platform for creating plugins to customize the TouchBar.
</p>
<p align="center">
<a href="https://discord.gg/CmNcDuQ"><img height="20px" src="https://camo.githubusercontent.com/88f53948f291c54736bf08f5fd7b037a848dfc62/68747470733a2f2f646973636f72646170702e636f6d2f6173736574732f30376463613830613130326434313439653937333664346231363263666636662e69636f"> Discord</a>
<a href="https://discord.gg/CmNcDuQ"><img height="20px" src="https://assets-global.website-files.com/6257adef93867e50d84d30e2/62fddf0fde45a8baedcc7ee5_847541504914fd33810e70a0ea73177e%20(2)-1.png"> Discord</a>
<a href="https://t.me/joinchat/AmVYGg8vW38c13_3MxdE_g"><img height="20px" src="https://telegram.org/img/t_logo.png" /> Telegram</a>
</p>
<p align="center"><a href="https://www.paypal.me/toxblh/10" title="Donate via Paypal"><img height="36px" src="Resources/support_paypal.svg" alt="PayPal donate button" /></a>
<p align="center"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=WUAAG2HH58WE4" title="Donate via Paypal"><img height="36px" src="Resources/support_paypal.svg" alt="PayPal donate button" /></a>
<a href="https://www.buymeacoffee.com/toxblh" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" height="36px" ></a>
<a href="https://www.patreon.com/bePatron?u=9900748"><img height="36px" src="https://c5.patreon.com/external/logo/become_a_patron_button.png" srcset="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png 2x"></a>
<a href="https://www.producthunt.com/posts/my-touchbar-my-rules-mtmr">
@ -27,11 +27,11 @@ My idea is to create a platform for creating plugins to customize the TouchBar.
## Installation
- Download lastest [release](https://github.com/Toxblh/MTMR/releases) (.dmg) from github
- Or via Homebrew `brew cask install mtmr`
- Download latest [release](https://github.com/Toxblh/MTMR/releases) (.dmg) from github
- Or via Homebrew `brew install --cask mtmr`
- [Dario Prski](https://medium.com/@urdigitalpulse) has written a [fantastic article on medium](https://medium.com/@urdigitalpulse/customise-your-macbook-pro-touch-bar-966998e606b5) that goes into more detail on installing MTMR
**On first install** you need to allow access for MTMR in Accessibility otherwise buttons like <kbd>Esc</kbd>, <kbd>Volume</kbd>, <kbd>Brightness</kbd> and other system keys won't work
**On first install** you need to allow access for MTMR in Accessibility otherwise buttons like <kbd>Esc</kbd>, <kbd>Volume</kbd>, <kbd>Brightness</kbd> and other system keys won't work.
<p align="center">
<img width="450" alt="screenshot 2019-02-24 at 23 19 20" src="https://user-images.githubusercontent.com/2198153/53307057-2b078200-388c-11e9-8212-8c2b1aff0aa6.png">
@ -51,7 +51,7 @@ My idea is to create a platform for creating plugins to customize the TouchBar.
## Customization
MTMR preferences are stored under `~/Library/Application\ Support/MTMR/items.json`.
MTMR preferences are stored in `~/Library/Application\ Support/MTMR/items.json`.
The pre-installed configuration contains less or more than you'll probably want, try to configure:
@ -73,6 +73,7 @@ The pre-installed configuration contains less or more than you'll probably want,
- timeButton
- battery
- cpu
- currency
- weather
- yandexWeather
@ -84,6 +85,7 @@ The pre-installed configuration contains less or more than you'll probably want,
- darkMode
- pomodoro
- network
- upnext (Calendar events)
> Media Keys
@ -102,11 +104,31 @@ The pre-installed configuration contains less or more than you'll probably want,
- appleScriptTitledButton
- shellScriptTitledButton
## Gestures on central part:
## Gestures
By default you can enable basic gestures from application menu (status bar -> MTMR icon -> Volume/Brightness gestures):
- two finger slide: change you Volume
- three finger slide: change you Brightness
### Custom gestures
You can add custom actions for two/three/four finger swipes. To do it, you need to use `swipe` type:
```json
"type": "swipe",
"fingers": 2, // number of fingers required (2,3 or 4)
"direction": "right", // direction of swipe (right/left)
"minOffset": 10, // optional: minimal required offset for gesture to emit event
"sourceApple": { // optional: apple script to run
"inline": "beep"
},
"sourceBash": { // optional: bash script to run
"inline": "touch /Users/lobster/test"
}
```
You may create as many `swipe` objects in the preset as you want.
## Built-in slider types:
- brightness
@ -133,15 +155,42 @@ The pre-installed configuration contains less or more than you'll probably want,
"inline": "tell application \"Finder\"\rif not (exists window 1) then\rmake new Finder window\rset target of front window to path to home folder as string\rend if\ractivate\rend tell",
// or
"base64": "StringInbase64"
}
},
}
```
#### `shellScriptTitledButton`
> Note: script may return also colors using escape sequences (read more here https://misc.flogisoft.com/bash/tip_colors_and_formatting)
> Only "16 Colors" mode supported atm. If background color returned, button will pick it up as own background color.
> Note: You can change appleScriptTitledButton's icon by following these steps:
1. Declare dictionary of icons in `alternativeImages` field
2. Make you script return array of two values - `{"TITLE", "IMAGE_LABEL"}`
3. Make sure that your `IMAGE_LABEL` is declared in `alternativeImages` field
Example of "CPU load" button which also changes color based on load value.
Example:
```js
{
"type": "appleScriptTitledButton",
"source": {
"inline": "if (random number from 1 to 2) = 1 then\n\tset val to {\"title\", \"play\"}\nelse\n\tset val to {\"title\", \"pause\"}\nend if\nreturn val"
},
"refreshInterval": 1,
"image": {
"base64": "iVBORw0KGgoAAAANSUhEUgA..."
},
"alternativeImages": {
"play": {
"base64": "iVBORw0KGgoAAAANSUhEUgAAAAAA..."
},
"pause": {
"base64": "iVBORw0KGgoAAAANSUhEUgAAAIAA..."
}
}
},
```
#### `shellScriptTitledButton`
> Note: script may also use escape sequences to return colors (read https://misc.flogisoft.com/bash/tip_colors_and_formatting for more information)
> "16 Colors" is the only mode supported presently. Buttons will set their own background color to the color returned.
Example of "CPU load" button which also changes color based on load value (Note: The native `cpu` plugin runs runs better):
```js
{
"type": "shellScriptTitledButton",
@ -150,10 +199,15 @@ Example of "CPU load" button which also changes color based on load value.
"source": {
"inline": "top -l 2 -n 0 -F | egrep -o ' \\d*\\.\\d+% idle' | tail -1 | awk -F% '{p = 100 - $1; if (p > 30) c = \"\\033[33m\"; if (p > 70) c = \"\\033[30;43m\"; printf \"%s%4.1f%%\\n\", c, p}'"
},
"actions": [
{
"trigger": "singleTap",
"action": "appleScript",
"actionAppleScript": {
"inline": "activate application \"Activity Monitor\"\rtell application \"System Events\"\r\ttell process \"Activity Monitor\"\r\t\ttell radio button \"CPU\" of radio group 1 of group 2 of toolbar 1 of window 1 to perform action \"AXPress\"\r\tend tell\rend tell"
},
}
}
],
"align": "right",
"image": {
// Or you can specify a filePath here.
@ -193,9 +247,22 @@ To close a group, use the button:
## Native plugins
#### `cpu`
> Shows current CPU load in percent, changes color based on load value.
> Has lower power consumption and higher stability than the shell-based solution.
```js
{
"type": "cpu",
"refreshInterval": 3,
"width": 80
}
```
#### `timeButton`
> Attention! Works not all: https://en.wikipedia.org/wiki/List_of_time_zone_abbreviations
> NOTE: Some values don't work properly: https://en.wikipedia.org/wiki/List_of_time_zone_abbreviations
> formatTemplate examples: https://www.datetimeformatter.com/how-to-format-date-time-in-swift/
@ -213,22 +280,22 @@ To close a group, use the button:
#### `weather`
> Provider: https://openweathermap.org \
> Note: you need to register on https://openweathermap.org to get your API key \
> Note: you may need to wait for near 20 mins until your API key will be activated by Openweathermap \
> Note: you need to allow using "Location Services" in your Mac OS "Security & Privacy" settings for MTMR
> Note: Register at https://openweathermap.org to get your API key \
> Note: Wait for 20 minutes or so for Openweathermap to activate your API key.\
> Note: Enable MTMR in "Location Services" in the "Security & Privacy" System Preferences pane
```js
"type": "weather",
"refreshInterval": 600, // in seconds
"units": "metric", // or imperial
"icon_type": "text" // or images
"icon_type": "text", // or images
"api_key": "" // you can get the key on openweather
```
#### `yandexWeather` (experimental)
> Provider: https://yandex.ru/pogoda. One click to open up weather forecast in your browser. \
> Note: you need to allow using "Location Services" in your Mac OS "Security & Privacy" settings for MTMR
> Note: Enable MTMR in "Location Services" in the "Security & Privacy" System Preferences pane
```js
"type": "yandexWeather",
@ -263,7 +330,7 @@ To close a group, use the button:
#### `pomodoro`
> Pomodoro plugin. One click to start the work timer, longclick to start the rest timer. Click in progress for reset.
> Pomodoro plugin. One tap starts the work timer, long-press to start the rest timer. Tap an in-progress timer to reset.
```js
{
@ -275,12 +342,13 @@ To close a group, use the button:
#### `network`
> Network plugin. The plugin to show usage a network
> Network plugin. The plugin to show network usage
```js
{
"type": "network",
"flip": true
"flip": true,
"units": "dynamic" // or B/s, KB/s, MB/s, GB/s
},
```
@ -296,8 +364,46 @@ To close a group, use the button:
},
```
#### `upnext`
> Calendar next event plugin
Displays upcoming events from macOS Calendar. Does not display current event.
```js
{
"type": "upnext",
"from": 0, // Lower bound of search range for next event in hours. Default 0 (current time)(can be negative to view events in the past)
"to": 12, // Upper bounds of search range for next event in hours. Default 12 (12 hours in the future)
"maxToShow": 3, // Limits the maximum number of events displayed. Default 3 (the first 3 upcoming events)
"autoResize": false // If true, widget will expand to display all events. Default false (scrollable view within "width")
},
```
## Actions:
### Example:
```js
"actions": [
{
"trigger": "singleTap",
"action": "hidKey",
"keycode": 53
}
]
```
### Triggers:
- `singleTap`
- `doubleTap`
- `tripleTap`
- `longTap`
### Types
- `hidKey`
> https://github.com/aosm/IOHIDFamily/blob/master/IOHIDSystem/IOKit/hidsystem/ev_keymap.h use only numbers
@ -307,6 +413,7 @@ To close a group, use the button:
```
- `keyPress`
> https://eastmanreference.com/complete-list-of-applescript-key-codes
```json
"action": "keyPress",
@ -339,22 +446,6 @@ To close a group, use the button:
"url": "https://google.com",
```
## LongActions
If you want to longPress for some operations, it is similar to the configuration for Actions but with additional parameters, for example:
```js
"longAction": "hidKey",
"longKeycode": 53,
```
- longAction
- longKeycode
- longActionAppleScript
- longExecutablePath
- longShellArguments
- longUrl
## Additional parameters:
- `width` restrict how much room a particular button will take
@ -375,39 +466,52 @@ If you want to longPress for some operations, it is similar to the configuration
"bordered": "false" // "true" or "false"
```
### Roadmap
- `background` allow to specify you button background color
- [x] Create the first prototype with TouchBar in Storyboard
- [x] Put in stripe menu on startup the application
- [x] Find how to simulate real buttons like brightness, volume, night shift and etc.
- [x] Time in touchbar!
- [x] First the weather plugin
- [x] Find how to open full-screen TouchBar without the cross and stripe menu
- [x] Find how to add haptic feedback
- [x] Add icon and menu in StatusBar
- [x] Hide from Dock
- [x] Status menu: "preferences", "quit"
- [x] JSON or another approch for save preset, maybe in `~/Library/Application Support/MTMR/`
- [x] Custom buttons size, actions by click
- [x] Layout: [always left, NSSliderView for center, always right]
- [x] System for autoupdate (https://sparkle-project.org/)
- [ ] Overwrite default values from item types (e.g. title for brightness)
- [ ] Custom settings for paddings and margins for buttons
- [ ] XPC Service for scripts
- [ ] UI for settings
- [ ] Import config from BTT
```js
"background": "#FF0000",
```
by using background with color "#000000" and bordered == false you can create button without gray background but with background when the button is pressed
Settings:
- `title` specify button title
- [ ] Interface for plugins and export like presets
- [x] Startup at login
- [ ] Show on/off in Dock
- [ ] Show on/off in StatusBar
- [ ] On/off Haptic Feedback
```js
"title": "hello"
```
Maybe:
- `image` specify button icon
- [ ] Refactoring the application into packages (AppleScript, JavaScript? and Swift?)
```js
"image": {
//Can be either of those
"base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAABGdB...."
//or
"filePath": "~/img.png"
}
```
- `matchAppId` displays the button only when active app's id matches given regexp
```json
"matchAppId": "Safari"
```
## Troubleshooting
#### If you can't open preferences:
- Opening another program which can't edit text
1. Open Terminal.app
2. Put `open -a TextEdit ~/Library/Application\ Support/MTMR/items.json` command and press <kbd>Enter</kbd>
#### Buttons or gestures doesn't work:
- "After the last update my mtmr is not working anymore!"
- "Buttons sometimes do not trigger action"
- "ESC don't work"
- "Gestures don't work"
Re-tick or check a tick for access 🍏→ System Preferences → Security and Privacy → tab Privacy → Accessibility → MTMR
## Credits

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -0,0 +1 @@
Versions/Current/Autoupdate

View File

@ -0,0 +1 @@
Versions/Current/Updater.app

View File

@ -1,25 +0,0 @@
//
// SPUDownloader.h
// Downloader
//
// Created by Mayur Pawashe on 4/1/16.
// Copyright © 2016 Sparkle Project. All rights reserved.
//
#if __has_feature(modules)
@import Foundation;
#else
#import <Foundation/Foundation.h>
#endif
#import "SPUDownloaderProtocol.h"
@protocol SPUDownloaderDelegate;
// This object implements the protocol which we have defined. It provides the actual behavior for the service. It is 'exported' by the service to make it available to the process hosting the service over an NSXPCConnection.
@interface SPUDownloader : NSObject <SPUDownloaderProtocol>
// Due to XPC remote object reasons, this delegate is strongly referenced
// Invoke cleanup when done with this instance
- (instancetype)initWithDelegate:(id <SPUDownloaderDelegate>)delegate;
@end

View File

@ -1,38 +0,0 @@
//
// SPUDownloaderDelegate.h
// Sparkle
//
// Created by Mayur Pawashe on 4/1/16.
// Copyright © 2016 Sparkle Project. All rights reserved.
//
#if __has_feature(modules)
@import Foundation;
#else
#import <Foundation/Foundation.h>
#endif
NS_ASSUME_NONNULL_BEGIN
@class SPUDownloadData;
@protocol SPUDownloaderDelegate <NSObject>
// This is only invoked for persistent downloads
- (void)downloaderDidSetDestinationName:(NSString *)destinationName temporaryDirectory:(NSString *)temporaryDirectory;
// Under rare cases, this may be called more than once, in which case the current progress should be reset back to 0
// This is only invoked for persistent downloads
- (void)downloaderDidReceiveExpectedContentLength:(int64_t)expectedContentLength;
// This is only invoked for persistent downloads
- (void)downloaderDidReceiveDataOfLength:(uint64_t)length;
// downloadData is nil if this is a persisent download, otherwise it's non-nil if it's a temporary download
- (void)downloaderDidFinishWithTemporaryDownloadData:(SPUDownloadData * _Nullable)downloadData;
- (void)downloaderDidFailWithError:(NSError *)error;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,13 +0,0 @@
//
// SPUDownloaderDeprecated.h
// Sparkle
//
// Created by Deadpikle on 12/20/17.
// Copyright © 2017 Sparkle Project. All rights reserved.
//
#import "SPUDownloader.h"
@interface SPUDownloaderDeprecated : SPUDownloader <SPUDownloaderProtocol>
@end

View File

@ -1,34 +0,0 @@
//
// SPUDownloaderProtocol.h
// PersistentDownloader
//
// Created by Mayur Pawashe on 4/1/16.
// Copyright © 2016 Sparkle Project. All rights reserved.
//
#if __has_feature(modules)
@import Foundation;
#else
#import <Foundation/Foundation.h>
#endif
NS_ASSUME_NONNULL_BEGIN
@class SPUURLRequest;
// The protocol that this service will vend as its API. This header file will also need to be visible to the process hosting the service.
@protocol SPUDownloaderProtocol
- (void)startPersistentDownloadWithRequest:(SPUURLRequest *)request bundleIdentifier:(NSString *)bundleIdentifier desiredFilename:(NSString *)desiredFilename;
- (void)startTemporaryDownloadWithRequest:(SPUURLRequest *)request;
- (void)downloadDidFinish;
- (void)cleanup;
- (void)cancel;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,20 +0,0 @@
//
// SPUDownloaderSession.h
// Sparkle
//
// Created by Deadpikle on 12/20/17.
// Copyright © 2017 Sparkle Project. All rights reserved.
//
#if __has_feature(modules)
@import Foundation;
#else
#import <Foundation/Foundation.h>
#endif
#import "SPUDownloader.h"
#import "SPUDownloaderProtocol.h"
NS_CLASS_AVAILABLE(NSURLSESSION_AVAILABLE, 7_0)
@interface SPUDownloaderSession : SPUDownloader <SPUDownloaderProtocol>
@end

View File

@ -1,35 +0,0 @@
//
// SPUURLRequest.h
// Sparkle
//
// Created by Mayur Pawashe on 5/19/16.
// Copyright © 2016 Sparkle Project. All rights reserved.
//
#if __has_feature(modules)
@import Foundation;
#else
#import <Foundation/Foundation.h>
#endif
NS_ASSUME_NONNULL_BEGIN
// A class that wraps NSURLRequest and implements NSSecureCoding
// This class exists because NSURLRequest did not support NSSecureCoding in macOS 10.8
// I have not verified if NSURLRequest in 10.9 implements NSSecureCoding or not
@interface SPUURLRequest : NSObject <NSSecureCoding>
// Creates a new URL request
// Only these properties are currently tracked:
// * URL
// * Cache policy
// * Timeout interval
// * HTTP header fields
// * networkServiceType
+ (instancetype)URLRequestWithRequest:(NSURLRequest *)request;
@property (nonatomic, readonly) NSURLRequest *request;
@end
NS_ASSUME_NONNULL_END

View File

@ -1,35 +0,0 @@
//
// SUAppcast.h
// Sparkle
//
// Created by Andy Matuschak on 3/12/06.
// Copyright 2006 Andy Matuschak. All rights reserved.
//
#ifndef SUAPPCAST_H
#define SUAPPCAST_H
#if __has_feature(modules)
@import Foundation;
#else
#import <Foundation/Foundation.h>
#endif
#import "SUExport.h"
NS_ASSUME_NONNULL_BEGIN
@class SUAppcastItem;
SU_EXPORT @interface SUAppcast : NSObject
@property (copy, nullable) NSString *userAgentString;
@property (copy, nullable) NSDictionary<NSString *, NSString *> *httpHeaders;
- (void)fetchAppcastFromURL:(NSURL *)url inBackground:(BOOL)bg completionBlock:(void (^)(NSError *_Nullable))err;
- (SUAppcast *)copyWithoutDeltaUpdates;
@property (readonly, copy, nullable) NSArray *items;
@end
NS_ASSUME_NONNULL_END
#endif

View File

@ -1,51 +0,0 @@
//
// SUAppcastItem.h
// Sparkle
//
// Created by Andy Matuschak on 3/12/06.
// Copyright 2006 Andy Matuschak. All rights reserved.
//
#ifndef SUAPPCASTITEM_H
#define SUAPPCASTITEM_H
#if __has_feature(modules)
@import Foundation;
#else
#import <Foundation/Foundation.h>
#endif
#import "SUExport.h"
SU_EXPORT @interface SUAppcastItem : NSObject
@property (copy, readonly) NSString *title;
@property (copy, readonly) NSString *dateString;
@property (copy, readonly) NSString *itemDescription;
@property (strong, readonly) NSURL *releaseNotesURL;
@property (copy, readonly) NSString *DSASignature;
@property (copy, readonly) NSString *minimumSystemVersion;
@property (copy, readonly) NSString *maximumSystemVersion;
@property (strong, readonly) NSURL *fileURL;
@property (nonatomic, readonly) uint64_t contentLength;
@property (copy, readonly) NSString *versionString;
@property (copy, readonly) NSString *osString;
@property (copy, readonly) NSString *displayVersionString;
@property (copy, readonly) NSDictionary *deltaUpdates;
@property (strong, readonly) NSURL *infoURL;
// Initializes with data from a dictionary provided by the RSS class.
- (instancetype)initWithDictionary:(NSDictionary *)dict;
- (instancetype)initWithDictionary:(NSDictionary *)dict failureReason:(NSString **)error;
@property (getter=isDeltaUpdate, readonly) BOOL deltaUpdate;
@property (getter=isCriticalUpdate, readonly) BOOL criticalUpdate;
@property (getter=isMacOsUpdate, readonly) BOOL macOsUpdate;
@property (getter=isInformationOnlyUpdate, readonly) BOOL informationOnlyUpdate;
// Returns the dictionary provided in initWithDictionary; this might be useful later for extensions.
@property (readonly, copy) NSDictionary *propertiesDictionary;
- (NSURL *)infoURL;
@end
#endif

View File

@ -1,22 +0,0 @@
//
// SUCodeSigningVerifier.h
// Sparkle
//
// Created by Andy Matuschak on 7/5/12.
//
//
#ifndef SUCODESIGNINGVERIFIER_H
#define SUCODESIGNINGVERIFIER_H
#import <Foundation/Foundation.h>
#import "SUExport.h"
SU_EXPORT @interface SUCodeSigningVerifier : NSObject
+ (BOOL)codeSignatureAtBundleURL:(NSURL *)oldBundlePath matchesSignatureAtBundleURL:(NSURL *)newBundlePath error:(NSError **)error;
+ (BOOL)codeSignatureIsValidAtBundleURL:(NSURL *)bundlePath error:(NSError **)error;
+ (BOOL)bundleAtURLIsCodeSigned:(NSURL *)bundlePath;
+ (NSDictionary *)codeSignatureInfoAtBundleURL:(NSURL *)bundlePath;
@end
#endif

View File

@ -1,56 +0,0 @@
//
// SUErrors.h
// Sparkle
//
// Created by C.W. Betts on 10/13/14.
// Copyright (c) 2014 Sparkle Project. All rights reserved.
//
#ifndef SUERRORS_H
#define SUERRORS_H
#if __has_feature(modules)
@import Foundation;
#else
#import <Foundation/Foundation.h>
#endif
#import "SUExport.h"
/**
* Error domain used by Sparkle
*/
SU_EXPORT extern NSString *const SUSparkleErrorDomain;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wc++98-compat"
typedef NS_ENUM(OSStatus, SUError) {
// Appcast phase errors.
SUAppcastParseError = 1000,
SUNoUpdateError = 1001,
SUAppcastError = 1002,
SURunningFromDiskImageError = 1003,
// Download phase errors.
SUTemporaryDirectoryError = 2000,
SUDownloadError = 2001,
// Extraction phase errors.
SUUnarchivingError = 3000,
SUSignatureError = 3001,
// Installation phase errors.
SUFileCopyFailure = 4000,
SUAuthenticationFailure = 4001,
SUMissingUpdateError = 4002,
SUMissingInstallerToolError = 4003,
SURelaunchError = 4004,
SUInstallationError = 4005,
SUDowngradeError = 4006,
SUInstallationCancelledError = 4007,
// System phase errors
SUSystemPowerOffError = 5000
};
#pragma clang diagnostic pop
#endif

View File

@ -1,52 +0,0 @@
//
// SUStandardVersionComparator.h
// Sparkle
//
// Created by Andy Matuschak on 12/21/07.
// Copyright 2007 Andy Matuschak. All rights reserved.
//
#ifndef SUSTANDARDVERSIONCOMPARATOR_H
#define SUSTANDARDVERSIONCOMPARATOR_H
#if __has_feature(modules)
@import Foundation;
#else
#import <Foundation/Foundation.h>
#endif
#import "SUExport.h"
#import "SUVersionComparisonProtocol.h"
NS_ASSUME_NONNULL_BEGIN
/*!
Sparkle's default version comparator.
This comparator is adapted from MacPAD, by Kevin Ballard.
It's "dumb" in that it does essentially string comparison,
in components split by character type.
*/
SU_EXPORT @interface SUStandardVersionComparator : NSObject <SUVersionComparison>
/*!
Initializes a new instance of the standard version comparator.
*/
- (instancetype)init;
/*!
Returns a singleton instance of the comparator.
It is usually preferred to alloc/init new a comparator instead.
*/
+ (SUStandardVersionComparator *)defaultComparator;
/*!
Compares version strings through textual analysis.
See the implementation for more details.
*/
- (NSComparisonResult)compareVersion:(NSString *)versionA toVersion:(NSString *)versionB;
@end
NS_ASSUME_NONNULL_END
#endif

View File

@ -1,33 +0,0 @@
//
// Sparkle.h
// Sparkle
//
// Created by Andy Matuschak on 3/16/06. (Modified by CDHW on 23/12/07)
// Copyright 2006 Andy Matuschak. All rights reserved.
//
#ifndef SPARKLE_H
#define SPARKLE_H
// This list should include the shared headers. It doesn't matter if some of them aren't shared (unless
// there are name-space collisions) so we can list all of them to start with:
#import "SUAppcast.h"
#import "SUAppcastItem.h"
#import "SUStandardVersionComparator.h"
#import "SUUpdater.h"
#import "SUUpdaterDelegate.h"
#import "SUVersionComparisonProtocol.h"
#import "SUVersionDisplayProtocol.h"
#import "SUErrors.h"
#import "SPUDownloader.h"
#import "SPUDownloaderDelegate.h"
#import "SPUDownloaderDeprecated.h"
#import "SPUDownloadData.h"
#import "SPUDownloaderProtocol.h"
#import "SPUDownloaderSession.h"
#import "SPUURLRequest.h"
#import "SUCodeSigningVerifier.h"
#endif

View File

@ -1,21 +0,0 @@
//
// SUUnarchiver.h
// Sparkle
//
// Created by Andy Matuschak on 3/16/06.
// Copyright 2006 Andy Matuschak. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@protocol SUUnarchiverProtocol;
@interface SUUnarchiver : NSObject
+ (nullable id <SUUnarchiverProtocol>)unarchiverForPath:(NSString *)path updatingHostBundlePath:(nullable NSString *)hostPath decryptionPassword:(nullable NSString *)decryptionPassword;
@end
NS_ASSUME_NONNULL_END

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More