Compare commits
111 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd99e9d73d | ||
|
|
58beb5a213 | ||
|
|
7da9ca2c68 | ||
|
|
88a4ce82db | ||
|
|
d39b4c0c31 | ||
|
|
5e609c2446 | ||
|
|
14301c4dbd | ||
|
|
a879498e4c | ||
|
|
36bf749a46 | ||
|
|
ac0e44db4d | ||
|
|
26ad83be70 | ||
|
|
d199bbd852 | ||
|
|
352bf4887c | ||
|
|
211ca4be32 | ||
|
|
d270a7bbcd | ||
|
|
44732e8ad6 | ||
|
|
8c57342070 | ||
|
|
3add660d72 | ||
|
|
eb617ff31b | ||
| 3e82676008 | |||
| a2ad47c7ba | |||
| bbe901a572 | |||
| 54eaa3fd9f | |||
|
|
6660bb2d8f | ||
|
|
7a1800252c | ||
|
|
588e6ae09b | ||
|
|
87141e381b | ||
|
|
14282b86a9 | ||
|
|
810cdeed36 | ||
|
|
a65613acaf | ||
|
|
aa69d5f592 | ||
|
|
2f00c9ffb3 | ||
|
|
1def53878d | ||
| 3e5fa14494 | |||
|
|
2e2f556daf | ||
| 1e1ae2af61 | |||
| bc11728c2e | |||
| 445584bb1b | |||
| b6721f0274 | |||
| 75df82a567 | |||
| a1f64028cc | |||
| 3fc75ca8f0 | |||
| 6d266394a4 | |||
|
|
52758f947d | ||
|
|
502f989417 | ||
|
|
f61550e510 | ||
|
|
a0fc0b33c5 | ||
|
|
42ce95b72e | ||
|
|
642f0807bb | ||
|
|
dbb2f16222 | ||
|
|
3864591777 | ||
|
|
7e0db70fab | ||
|
|
40c684f528 | ||
|
|
093faa2c02 | ||
|
|
aeee9983d1 | ||
|
|
bdfb7118c9 | ||
|
|
878cd346c1 | ||
|
|
712448f207 | ||
| e9e5a6f739 | |||
| 466c0e5f68 | |||
|
|
85e3deac79 | ||
|
|
9e49cf8beb | ||
|
|
d81998862a | ||
|
|
1ae2041d0d | ||
|
|
5eb2d73c94 | ||
|
|
855ddea44e | ||
|
|
eaa26645c5 | ||
|
|
2cdb705b37 | ||
|
|
fa413f2fa2 | ||
|
|
6920664fad | ||
|
|
e9a7b6d32a | ||
|
|
dd23a3bda8 | ||
|
|
f82d7694eb | ||
|
|
94717a5ea3 | ||
|
|
b155cac2b0 | ||
|
|
cd2ec3d032 | ||
|
|
c4928ee382 | ||
|
|
0eef872f2b | ||
| 3f78bbe42c | |||
| f2e6959b71 | |||
|
|
820853d300 | ||
|
|
2635e2611f | ||
|
|
e68fa10c42 | ||
|
|
92975cb8e4 | ||
| 0b39795bd7 | |||
| d4b950ab64 | |||
| 41a9544c3a | |||
|
|
e44ff00f3b | ||
|
|
f378de675e | ||
|
|
66b175d5ba | ||
|
|
59cde098f2 | ||
|
|
f3015df82a | ||
|
|
6629cfd11c | ||
|
|
d6e48c8197 | ||
|
|
0e8dab4677 | ||
| aa73fd1dc3 | |||
|
|
29da70c477 | ||
|
|
aa67dc10b7 | ||
|
|
aaa54e2709 | ||
| a110e00b9c | |||
|
|
12137c6732 | ||
|
|
36d1028af1 | ||
|
|
77f56df144 | ||
|
|
80b56779fb | ||
|
|
580f0275fc | ||
|
|
168b629810 | ||
|
|
2d64c091e3 | ||
|
|
63e3de7313 | ||
|
|
229c55c367 | ||
|
|
11530ef180 | ||
| 96f26ab7f7 |
2
.github/FUNDING.yml
vendored
@ -1,4 +1,6 @@
|
|||||||
# These are supported funding model platforms
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
issuehunt: Toxblh
|
||||||
patreon: toxblh
|
patreon: toxblh
|
||||||
ko_fi: toxblh
|
ko_fi: 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
@ -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
@ -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
|
||||||
@ -1,6 +0,0 @@
|
|||||||
language: swift
|
|
||||||
xcode_project: MTMR.xcodeproj
|
|
||||||
xcode_scheme: UnitTests
|
|
||||||
osx_image: xcode10
|
|
||||||
install: gem install xcpretty
|
|
||||||
script: "xcodebuild test -project MTMR.xcodeproj -scheme 'UnitTests' | xcpretty -c && exit ${PIPESTATUS[0]}"
|
|
||||||
@ -18,7 +18,14 @@
|
|||||||
36C2ECDD207C723B003CDA33 /* ParseConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36C2ECDC207C723B003CDA33 /* ParseConfigTests.swift */; };
|
36C2ECDD207C723B003CDA33 /* ParseConfigTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36C2ECDC207C723B003CDA33 /* ParseConfigTests.swift */; };
|
||||||
36C2ECDE207C82DE003CDA33 /* ItemsParsing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36C2ECDA207C3FE7003CDA33 /* ItemsParsing.swift */; };
|
36C2ECDE207C82DE003CDA33 /* ItemsParsing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36C2ECDA207C3FE7003CDA33 /* ItemsParsing.swift */; };
|
||||||
36C2ECE0207CB1B0003CDA33 /* defaultPreset.json in Resources */ = {isa = PBXBuildFile; fileRef = 36C2ECDF207CB1B0003CDA33 /* defaultPreset.json */; };
|
36C2ECE0207CB1B0003CDA33 /* defaultPreset.json in Resources */ = {isa = PBXBuildFile; fileRef = 36C2ECDF207CB1B0003CDA33 /* defaultPreset.json */; };
|
||||||
|
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 */; };
|
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 */; };
|
60173D3E20C0031B002C305F /* LaunchAtLoginController.m in Sources */ = {isa = PBXBuildFile; fileRef = 60173D3D20C0031B002C305F /* LaunchAtLoginController.m */; };
|
||||||
6027D1B92080E52A004FFDC7 /* BrightnessViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6027D1B72080E52A004FFDC7 /* BrightnessViewController.swift */; };
|
6027D1B92080E52A004FFDC7 /* BrightnessViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6027D1B72080E52A004FFDC7 /* BrightnessViewController.swift */; };
|
||||||
6027D1BA2080E52A004FFDC7 /* VolumeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6027D1B82080E52A004FFDC7 /* VolumeViewController.swift */; };
|
6027D1BA2080E52A004FFDC7 /* VolumeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6027D1B82080E52A004FFDC7 /* VolumeViewController.swift */; };
|
||||||
@ -64,6 +71,9 @@
|
|||||||
B0F3112520C9E35F0076BB88 /* SupportNSTouchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0F3112420C9E35F0076BB88 /* SupportNSTouchBar.swift */; };
|
B0F3112520C9E35F0076BB88 /* SupportNSTouchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0F3112420C9E35F0076BB88 /* SupportNSTouchBar.swift */; };
|
||||||
B0F54A7A2295AC7D00B4C509 /* DarkModeBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0F54A792295AC7D00B4C509 /* DarkModeBarItem.swift */; };
|
B0F54A7A2295AC7D00B4C509 /* DarkModeBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0F54A792295AC7D00B4C509 /* DarkModeBarItem.swift */; };
|
||||||
B0F8771A207AC1EA00D6E430 /* TouchBarSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = B0F87719207AC1EA00D6E430 /* TouchBarSupport.m */; };
|
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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
@ -92,7 +102,15 @@
|
|||||||
36C2ECDA207C3FE7003CDA33 /* ItemsParsing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemsParsing.swift; sourceTree = "<group>"; };
|
36C2ECDA207C3FE7003CDA33 /* ItemsParsing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemsParsing.swift; sourceTree = "<group>"; };
|
||||||
36C2ECDC207C723B003CDA33 /* ParseConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseConfigTests.swift; sourceTree = "<group>"; };
|
36C2ECDC207C723B003CDA33 /* ParseConfigTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseConfigTests.swift; sourceTree = "<group>"; };
|
||||||
36C2ECDF207CB1B0003CDA33 /* defaultPreset.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = defaultPreset.json; sourceTree = "<group>"; };
|
36C2ECDF207CB1B0003CDA33 /* defaultPreset.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = defaultPreset.json; sourceTree = "<group>"; };
|
||||||
|
36FEF871235A1CFC00A0ABCE /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
|
||||||
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
6027D1B72080E52A004FFDC7 /* BrightnessViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BrightnessViewController.swift; sourceTree = "<group>"; };
|
||||||
@ -148,6 +166,9 @@
|
|||||||
B0F54A792295AC7D00B4C509 /* DarkModeBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DarkModeBarItem.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -220,9 +241,14 @@
|
|||||||
36C2ECDF207CB1B0003CDA33 /* defaultPreset.json */,
|
36C2ECDF207CB1B0003CDA33 /* defaultPreset.json */,
|
||||||
B082B252205C7D8000BC04DC /* AppDelegate.swift */,
|
B082B252205C7D8000BC04DC /* AppDelegate.swift */,
|
||||||
B0A7E9A9205D6AA400EEF070 /* KeyPress.swift */,
|
B0A7E9A9205D6AA400EEF070 /* KeyPress.swift */,
|
||||||
|
36FEF871235A1CFC00A0ABCE /* AppSettings.swift */,
|
||||||
B059D623205E04F3006E6B86 /* CustomButtonTouchBarItem.swift */,
|
B059D623205E04F3006E6B86 /* CustomButtonTouchBarItem.swift */,
|
||||||
|
4CF222EC26CC43D40025BDA7 /* CPU.swift */,
|
||||||
36C2ECD8207B74B4003CDA33 /* AppleScriptTouchBarItem.swift */,
|
36C2ECD8207B74B4003CDA33 /* AppleScriptTouchBarItem.swift */,
|
||||||
|
BAF5AB5824317CAF00B04904 /* SwipeItem.swift */,
|
||||||
|
4CDC6E4F22FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift */,
|
||||||
B059D621205E03F5006E6B86 /* TouchBarController.swift */,
|
B059D621205E03F5006E6B86 /* TouchBarController.swift */,
|
||||||
|
BAF5AB5624317B4300B04904 /* BasicView.swift */,
|
||||||
36C2ECDA207C3FE7003CDA33 /* ItemsParsing.swift */,
|
36C2ECDA207C3FE7003CDA33 /* ItemsParsing.swift */,
|
||||||
B0008E542080286C003AD4DD /* SupportHelpers.swift */,
|
B0008E542080286C003AD4DD /* SupportHelpers.swift */,
|
||||||
B09EB1E3207C082000D5C1E0 /* HapticFeedback.swift */,
|
B09EB1E3207C082000D5C1E0 /* HapticFeedback.swift */,
|
||||||
@ -249,6 +275,8 @@
|
|||||||
B0B17426207D6AFE0004B740 /* AppleScripts */ = {
|
B0B17426207D6AFE0004B740 /* AppleScripts */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
5DC6CA01241F92CB005CD5E8 /* Music.next.scpt */,
|
||||||
|
5DC6CA00241F92CB005CD5E8 /* Music.nowPlaying.scpt */,
|
||||||
B0B1742A207D6B580004B740 /* Battery.scpt */,
|
B0B1742A207D6B580004B740 /* Battery.scpt */,
|
||||||
B0B17429207D6B580004B740 /* Finder.scpt */,
|
B0B17429207D6B580004B740 /* Finder.scpt */,
|
||||||
B0B1742C207D6B590004B740 /* iTunes.next.scpt */,
|
B0B1742C207D6B590004B740 /* iTunes.next.scpt */,
|
||||||
@ -266,6 +294,8 @@
|
|||||||
B0B1743B207D6ED40004B740 /* CBridge */ = {
|
B0B1743B207D6ED40004B740 /* CBridge */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
4CC9FEDA22FDEA65001512EB /* AMR_ANSIEscapeHelper.h */,
|
||||||
|
4CC9FEDB22FDEA65001512EB /* AMR_ANSIEscapeHelper.m */,
|
||||||
B059D629205E13E5006E6B86 /* TouchBarPrivateApi.h */,
|
B059D629205E13E5006E6B86 /* TouchBarPrivateApi.h */,
|
||||||
B059D62A205F0E7D006E6B86 /* TouchBarPrivateApi-Bridging.h */,
|
B059D62A205F0E7D006E6B86 /* TouchBarPrivateApi-Bridging.h */,
|
||||||
B0F87719207AC1EA00D6E430 /* TouchBarSupport.m */,
|
B0F87719207AC1EA00D6E430 /* TouchBarSupport.m */,
|
||||||
@ -285,6 +315,7 @@
|
|||||||
6042B6A62083E03A00C525C8 /* AppScrubberTouchBarItem.swift */,
|
6042B6A62083E03A00C525C8 /* AppScrubberTouchBarItem.swift */,
|
||||||
B04B7BB62087398C00C835D0 /* BatteryBarItem.swift */,
|
B04B7BB62087398C00C835D0 /* BatteryBarItem.swift */,
|
||||||
6027D1B72080E52A004FFDC7 /* BrightnessViewController.swift */,
|
6027D1B72080E52A004FFDC7 /* BrightnessViewController.swift */,
|
||||||
|
4CF222EA26CC00470025BDA7 /* CPUBarItem.swift */,
|
||||||
607EEA4C2087A8DA009DA5F0 /* CurrencyBarItem.swift */,
|
607EEA4C2087A8DA009DA5F0 /* CurrencyBarItem.swift */,
|
||||||
B081732B213739FE005D4908 /* DnDBarItem.swift */,
|
B081732B213739FE005D4908 /* DnDBarItem.swift */,
|
||||||
60669B4220AD8FA80074E817 /* GroupBarItem.swift */,
|
60669B4220AD8FA80074E817 /* GroupBarItem.swift */,
|
||||||
@ -299,6 +330,7 @@
|
|||||||
4CFF5E5B22E623DD00BFB1EE /* YandexWeatherBarItem.swift */,
|
4CFF5E5B22E623DD00BFB1EE /* YandexWeatherBarItem.swift */,
|
||||||
B08126F0217BE19000A98970 /* WidgetProtocol.swift */,
|
B08126F0217BE19000A98970 /* WidgetProtocol.swift */,
|
||||||
B0F54A792295AC7D00B4C509 /* DarkModeBarItem.swift */,
|
B0F54A792295AC7D00B4C509 /* DarkModeBarItem.swift */,
|
||||||
|
F29F6A2424BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift */,
|
||||||
);
|
);
|
||||||
path = Widgets;
|
path = Widgets;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -354,7 +386,7 @@
|
|||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
B082B24E205C7D8000BC04DC = {
|
B082B24E205C7D8000BC04DC = {
|
||||||
CreatedOnToolsVersion = 9.2;
|
CreatedOnToolsVersion = 9.2;
|
||||||
LastSwiftMigration = 1000;
|
LastSwiftMigration = 1020;
|
||||||
ProvisioningStyle = Manual;
|
ProvisioningStyle = Manual;
|
||||||
SystemCapabilities = {
|
SystemCapabilities = {
|
||||||
com.apple.Sandbox = {
|
com.apple.Sandbox = {
|
||||||
@ -364,7 +396,7 @@
|
|||||||
};
|
};
|
||||||
B082B260205C7D8000BC04DC = {
|
B082B260205C7D8000BC04DC = {
|
||||||
CreatedOnToolsVersion = 9.2;
|
CreatedOnToolsVersion = 9.2;
|
||||||
LastSwiftMigration = 1000;
|
LastSwiftMigration = 1020;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -396,6 +428,8 @@
|
|||||||
B0B1743A207D6B590004B740 /* iTunes.nowPlaying.scpt in Resources */,
|
B0B1743A207D6B590004B740 /* iTunes.nowPlaying.scpt in Resources */,
|
||||||
B082B257205C7D8000BC04DC /* Assets.xcassets in Resources */,
|
B082B257205C7D8000BC04DC /* Assets.xcassets in Resources */,
|
||||||
B0B17437207D6B590004B740 /* Spotify.nowPlaying.scpt 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 */,
|
B0B17436207D6B590004B740 /* iTunes.next.scpt in Resources */,
|
||||||
B082B25A205C7D8000BC04DC /* Main.storyboard in Resources */,
|
B082B25A205C7D8000BC04DC /* Main.storyboard in Resources */,
|
||||||
B0B17435207D6B590004B740 /* Spotify.next.scpt in Resources */,
|
B0B17435207D6B590004B740 /* Spotify.next.scpt in Resources */,
|
||||||
@ -446,25 +480,32 @@
|
|||||||
B059D622205E03F5006E6B86 /* TouchBarController.swift in Sources */,
|
B059D622205E03F5006E6B86 /* TouchBarController.swift in Sources */,
|
||||||
B05600D32083E9BB00EB218D /* CustomSlider.swift in Sources */,
|
B05600D32083E9BB00EB218D /* CustomSlider.swift in Sources */,
|
||||||
36C2ECD9207B74B4003CDA33 /* AppleScriptTouchBarItem.swift in Sources */,
|
36C2ECD9207B74B4003CDA33 /* AppleScriptTouchBarItem.swift in Sources */,
|
||||||
|
BAF5AB5724317B4300B04904 /* BasicView.swift in Sources */,
|
||||||
B0846A752220C968000288A7 /* NetworkBarItem.swift in Sources */,
|
B0846A752220C968000288A7 /* NetworkBarItem.swift in Sources */,
|
||||||
B0F8771A207AC1EA00D6E430 /* TouchBarSupport.m in Sources */,
|
B0F8771A207AC1EA00D6E430 /* TouchBarSupport.m in Sources */,
|
||||||
B08126F1217BE19000A98970 /* WidgetProtocol.swift in Sources */,
|
B08126F1217BE19000A98970 /* WidgetProtocol.swift in Sources */,
|
||||||
|
4CF222EB26CC00470025BDA7 /* CPUBarItem.swift in Sources */,
|
||||||
B0008E552080286C003AD4DD /* SupportHelpers.swift in Sources */,
|
B0008E552080286C003AD4DD /* SupportHelpers.swift in Sources */,
|
||||||
6042B6A72083E03A00C525C8 /* AppScrubberTouchBarItem.swift in Sources */,
|
6042B6A72083E03A00C525C8 /* AppScrubberTouchBarItem.swift in Sources */,
|
||||||
|
BAF5AB5924317CAF00B04904 /* SwipeItem.swift in Sources */,
|
||||||
B082B253205C7D8000BC04DC /* AppDelegate.swift in Sources */,
|
B082B253205C7D8000BC04DC /* AppDelegate.swift in Sources */,
|
||||||
B0F54A7A2295AC7D00B4C509 /* DarkModeBarItem.swift in Sources */,
|
B0F54A7A2295AC7D00B4C509 /* DarkModeBarItem.swift in Sources */,
|
||||||
B08126EF217BD0B900A98970 /* PomodoroBarItem.swift in Sources */,
|
B08126EF217BD0B900A98970 /* PomodoroBarItem.swift in Sources */,
|
||||||
|
4CF222ED26CC43D50025BDA7 /* CPU.swift in Sources */,
|
||||||
B081732C213739FE005D4908 /* DnDBarItem.swift in Sources */,
|
B081732C213739FE005D4908 /* DnDBarItem.swift in Sources */,
|
||||||
60C44AFD20A373A100C0EC91 /* MusicBarItem.swift in Sources */,
|
60C44AFD20A373A100C0EC91 /* MusicBarItem.swift in Sources */,
|
||||||
B059D624205E04F3006E6B86 /* CustomButtonTouchBarItem.swift in Sources */,
|
B059D624205E04F3006E6B86 /* CustomButtonTouchBarItem.swift in Sources */,
|
||||||
60173D3E20C0031B002C305F /* LaunchAtLoginController.m in Sources */,
|
60173D3E20C0031B002C305F /* LaunchAtLoginController.m in Sources */,
|
||||||
6027D1BA2080E52A004FFDC7 /* VolumeViewController.swift in Sources */,
|
6027D1BA2080E52A004FFDC7 /* VolumeViewController.swift in Sources */,
|
||||||
|
4CDC6E5022FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift in Sources */,
|
||||||
607EEA4B2087835F009DA5F0 /* WeatherBarItem.swift in Sources */,
|
607EEA4B2087835F009DA5F0 /* WeatherBarItem.swift in Sources */,
|
||||||
|
F29F6A2524BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift in Sources */,
|
||||||
B0F3112520C9E35F0076BB88 /* SupportNSTouchBar.swift in Sources */,
|
B0F3112520C9E35F0076BB88 /* SupportNSTouchBar.swift in Sources */,
|
||||||
4CFF5E5C22E623DD00BFB1EE /* YandexWeatherBarItem.swift in Sources */,
|
4CFF5E5C22E623DD00BFB1EE /* YandexWeatherBarItem.swift in Sources */,
|
||||||
6042B6AA2083E27000C525C8 /* DeprecatedCarbonAPI.c in Sources */,
|
6042B6AA2083E27000C525C8 /* DeprecatedCarbonAPI.c in Sources */,
|
||||||
B09EB1E4207C082000D5C1E0 /* HapticFeedback.swift in Sources */,
|
B09EB1E4207C082000D5C1E0 /* HapticFeedback.swift in Sources */,
|
||||||
B08173272135F02B005D4908 /* NightShiftBarItem.swift in Sources */,
|
B08173272135F02B005D4908 /* NightShiftBarItem.swift in Sources */,
|
||||||
|
36FEF872235A1CFC00A0ABCE /* AppSettings.swift in Sources */,
|
||||||
60F7D454208CC31400ABF5D2 /* InputSourceBarItem.swift in Sources */,
|
60F7D454208CC31400ABF5D2 /* InputSourceBarItem.swift in Sources */,
|
||||||
36A778BE20A6C27100B38714 /* GeneralExtensions.swift in Sources */,
|
36A778BE20A6C27100B38714 /* GeneralExtensions.swift in Sources */,
|
||||||
60669B4320AD8FA80074E817 /* GroupBarItem.swift in Sources */,
|
60669B4320AD8FA80074E817 /* GroupBarItem.swift in Sources */,
|
||||||
@ -472,6 +513,7 @@
|
|||||||
B0A7E9AA205D6AA400EEF070 /* KeyPress.swift in Sources */,
|
B0A7E9AA205D6AA400EEF070 /* KeyPress.swift in Sources */,
|
||||||
36C2ECD7207B6DAE003CDA33 /* TimeTouchBarItem.swift in Sources */,
|
36C2ECD7207B6DAE003CDA33 /* TimeTouchBarItem.swift in Sources */,
|
||||||
607EEA4D2087A8DA009DA5F0 /* CurrencyBarItem.swift in Sources */,
|
607EEA4D2087A8DA009DA5F0 /* CurrencyBarItem.swift in Sources */,
|
||||||
|
4CC9FEDC22FDEA65001512EB /* AMR_ANSIEscapeHelper.m in Sources */,
|
||||||
6027D1B92080E52A004FFDC7 /* BrightnessViewController.swift in Sources */,
|
6027D1B92080E52A004FFDC7 /* BrightnessViewController.swift in Sources */,
|
||||||
368EDDE720812A1D00E10953 /* ScrollViewItem.swift in Sources */,
|
368EDDE720812A1D00E10953 /* ScrollViewItem.swift in Sources */,
|
||||||
B04B7BB72087398C00C835D0 /* BatteryBarItem.swift in Sources */,
|
B04B7BB72087398C00C835D0 /* BatteryBarItem.swift in Sources */,
|
||||||
@ -630,7 +672,7 @@
|
|||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "MTMR/CBridge/TouchBarPrivateApi-Bridging.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "MTMR/CBridge/TouchBarPrivateApi-Bridging.h";
|
||||||
SWIFT_VERSION = 4.2;
|
SWIFT_VERSION = 5.0;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@ -653,7 +695,7 @@
|
|||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "MTMR/CBridge/TouchBarPrivateApi-Bridging.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "MTMR/CBridge/TouchBarPrivateApi-Bridging.h";
|
||||||
SWIFT_VERSION = 4.2;
|
SWIFT_VERSION = 5.0;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
@ -666,7 +708,7 @@
|
|||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = Toxblh.MTMRTests;
|
PRODUCT_BUNDLE_IDENTIFIER = Toxblh.MTMRTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 4.2;
|
SWIFT_VERSION = 5.0;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@ -681,7 +723,7 @@
|
|||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = Toxblh.MTMRTests;
|
PRODUCT_BUNDLE_IDENTIFIER = Toxblh.MTMRTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_VERSION = 4.2;
|
SWIFT_VERSION = 5.0;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict/>
|
||||||
<key>BuildSystemType</key>
|
|
||||||
<string>Original</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@ -41,17 +41,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
func applicationWillTerminate(_: Notification) {}
|
func applicationWillTerminate(_: Notification) {}
|
||||||
|
|
||||||
@objc func updateIsBlockedApp() {
|
@objc func updateIsBlockedApp() {
|
||||||
var blacklistAppIdentifiers: [String] = []
|
if let frontmostAppId = TouchBarController.shared.frontmostApplicationIdentifier {
|
||||||
if let blackListed = UserDefaults.standard.stringArray(forKey: "com.toxblh.mtmr.blackListedApps") {
|
isBlockedApp = AppSettings.blacklistedAppIds.firstIndex(of: frontmostAppId) != nil
|
||||||
blacklistAppIdentifiers = blackListed
|
|
||||||
}
|
|
||||||
var frontmostApplicationIdentifier: String? {
|
|
||||||
guard let frontmostId = NSWorkspace.shared.frontmostApplication?.bundleIdentifier else { return nil }
|
|
||||||
return frontmostId
|
|
||||||
}
|
|
||||||
|
|
||||||
if frontmostApplicationIdentifier != nil {
|
|
||||||
isBlockedApp = blacklistAppIdentifiers.index(of: frontmostApplicationIdentifier!) != nil
|
|
||||||
} else {
|
} else {
|
||||||
isBlockedApp = false
|
isBlockedApp = false
|
||||||
}
|
}
|
||||||
@ -67,29 +58,37 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
task.launch()
|
task.launch()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func toggleControlStrip(_: Any?) {
|
@objc func toggleControlStrip(_ item: NSMenuItem) {
|
||||||
TouchBarController.shared.showControlStripState = !TouchBarController.shared.showControlStripState
|
item.state = item.state == .on ? .off : .on
|
||||||
|
AppSettings.showControlStripState = item.state == .off
|
||||||
TouchBarController.shared.resetControlStrip()
|
TouchBarController.shared.resetControlStrip()
|
||||||
createMenu()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func toggleBlackListedApp(_: Any?) {
|
@objc func toggleBlackListedApp(_: Any?) {
|
||||||
let appIdentifier = TouchBarController.shared.frontmostApplicationIdentifier
|
if let appIdentifier = TouchBarController.shared.frontmostApplicationIdentifier {
|
||||||
if appIdentifier != nil {
|
if let index = TouchBarController.shared.blacklistAppIdentifiers.firstIndex(of: appIdentifier) {
|
||||||
if let index = TouchBarController.shared.blacklistAppIdentifiers.index(of: appIdentifier!) {
|
|
||||||
TouchBarController.shared.blacklistAppIdentifiers.remove(at: index)
|
TouchBarController.shared.blacklistAppIdentifiers.remove(at: index)
|
||||||
} else {
|
} else {
|
||||||
TouchBarController.shared.blacklistAppIdentifiers.append(appIdentifier!)
|
TouchBarController.shared.blacklistAppIdentifiers.append(appIdentifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
UserDefaults.standard.set(TouchBarController.shared.blacklistAppIdentifiers, forKey: "com.toxblh.mtmr.blackListedApps")
|
AppSettings.blacklistedAppIds = TouchBarController.shared.blacklistAppIdentifiers
|
||||||
UserDefaults.standard.synchronize()
|
|
||||||
|
|
||||||
TouchBarController.shared.updateActiveApp()
|
TouchBarController.shared.updateActiveApp()
|
||||||
updateIsBlockedApp()
|
updateIsBlockedApp()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func toggleHapticFeedback(_ item: NSMenuItem) {
|
||||||
|
item.state = item.state == .on ? .off : .on
|
||||||
|
AppSettings.hapticFeedbackState = item.state == .on
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func toggleMultitouch(_ item: NSMenuItem) {
|
||||||
|
item.state = item.state == .on ? .off : .on
|
||||||
|
AppSettings.multitouchGestures = item.state == .on
|
||||||
|
TouchBarController.shared.basicView?.legacyGesturesEnabled = item.state == .on
|
||||||
|
}
|
||||||
|
|
||||||
@objc func openPreset(_: Any?) {
|
@objc func openPreset(_: Any?) {
|
||||||
let dialog = NSOpenPanel()
|
let dialog = NSOpenPanel()
|
||||||
|
|
||||||
@ -122,7 +121,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
toggleBlackList.state = isBlockedApp ? .on : .off
|
toggleBlackList.state = isBlockedApp ? .on : .off
|
||||||
|
|
||||||
let hideControlStrip = NSMenuItem(title: "Hide Control Strip", action: #selector(toggleControlStrip(_:)), keyEquivalent: "T")
|
let hideControlStrip = NSMenuItem(title: "Hide Control Strip", action: #selector(toggleControlStrip(_:)), keyEquivalent: "T")
|
||||||
hideControlStrip.state = TouchBarController.shared.showControlStripState ? .off : .on
|
hideControlStrip.state = AppSettings.showControlStripState ? .off : .on
|
||||||
|
|
||||||
|
let hapticFeedback = NSMenuItem(title: "Haptic Feedback", action: #selector(toggleHapticFeedback(_:)), keyEquivalent: "H")
|
||||||
|
hapticFeedback.state = AppSettings.hapticFeedbackState ? .on : .off
|
||||||
|
|
||||||
|
let multitouchGestures = NSMenuItem(title: "Volume/Brightness gestures", action: #selector(toggleMultitouch(_:)), keyEquivalent: "")
|
||||||
|
multitouchGestures.state = AppSettings.multitouchGestures ? .on : .off
|
||||||
|
|
||||||
let settingSeparator = NSMenuItem(title: "Settings", action: nil, keyEquivalent: "")
|
let settingSeparator = NSMenuItem(title: "Settings", action: nil, keyEquivalent: "")
|
||||||
settingSeparator.isEnabled = false
|
settingSeparator.isEnabled = false
|
||||||
@ -133,9 +138,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
|
|
||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
menu.addItem(settingSeparator)
|
menu.addItem(settingSeparator)
|
||||||
|
menu.addItem(hapticFeedback)
|
||||||
menu.addItem(hideControlStrip)
|
menu.addItem(hideControlStrip)
|
||||||
menu.addItem(toggleBlackList)
|
menu.addItem(toggleBlackList)
|
||||||
menu.addItem(startAtLogin)
|
menu.addItem(startAtLogin)
|
||||||
|
menu.addItem(multitouchGestures)
|
||||||
menu.addItem(NSMenuItem.separator())
|
menu.addItem(NSMenuItem.separator())
|
||||||
menu.addItem(withTitle: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
|
menu.addItem(withTitle: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
|
||||||
statusItem.menu = menu
|
statusItem.menu = menu
|
||||||
|
|||||||
33
MTMR/AppSettings.swift
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct AppSettings {
|
||||||
|
@UserDefault(key: "com.toxblh.mtmr.settings.showControlStrip", defaultValue: false)
|
||||||
|
static var showControlStripState: Bool
|
||||||
|
|
||||||
|
@UserDefault(key: "com.toxblh.mtmr.settings.hapticFeedback", defaultValue: true)
|
||||||
|
static var hapticFeedbackState: Bool
|
||||||
|
|
||||||
|
@UserDefault(key: "com.toxblh.mtmr.settings.multitouchGestures", defaultValue: true)
|
||||||
|
static var multitouchGestures: Bool
|
||||||
|
|
||||||
|
@UserDefault(key: "com.toxblh.mtmr.blackListedApps", defaultValue: [])
|
||||||
|
static var blacklistedAppIds: [String]
|
||||||
|
|
||||||
|
@UserDefault(key: "com.toxblh.mtmr.dock.persistent", defaultValue: [])
|
||||||
|
static var dockPersistentAppIds: [String]
|
||||||
|
}
|
||||||
|
|
||||||
|
@propertyWrapper
|
||||||
|
struct UserDefault<T> {
|
||||||
|
let key: String
|
||||||
|
let defaultValue: T
|
||||||
|
var wrappedValue: T {
|
||||||
|
get {
|
||||||
|
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
UserDefaults.standard.set(newValue, forKey: key)
|
||||||
|
UserDefaults.standard.synchronize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,9 +4,11 @@ class AppleScriptTouchBarItem: CustomButtonTouchBarItem {
|
|||||||
private var script: NSAppleScript!
|
private var script: NSAppleScript!
|
||||||
private let interval: TimeInterval
|
private let interval: TimeInterval
|
||||||
private var forceHideConstraint: NSLayoutConstraint!
|
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.interval = interval
|
||||||
|
self.alternativeImages = alternativeImages
|
||||||
super.init(identifier: identifier, title: "⏳")
|
super.init(identifier: identifier, title: "⏳")
|
||||||
forceHideConstraint = view.widthAnchor.constraint(equalToConstant: 0)
|
forceHideConstraint = view.widthAnchor.constraint(equalToConstant: 0)
|
||||||
title = "scheduled"
|
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 {
|
func execute() -> String {
|
||||||
var error: NSDictionary?
|
var error: NSDictionary?
|
||||||
let output = script.executeAndReturnError(&error)
|
let output = script.executeAndReturnError(&error)
|
||||||
@ -64,6 +76,18 @@ class AppleScriptTouchBarItem: CustomButtonTouchBarItem {
|
|||||||
print(error)
|
print(error)
|
||||||
return "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 ?? ""
|
return output.stringValue ?? ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
tell application "Finder"
|
tell application "Finder"
|
||||||
make new Finder window
|
if not (exists window 1) then
|
||||||
set target of front window to path to home folder as string
|
make new Finder window
|
||||||
|
set target of front window to path to home folder as string
|
||||||
|
end if
|
||||||
activate
|
activate
|
||||||
end tell
|
end tell
|
||||||
|
|||||||
7
MTMR/AppleScripts/Music.next.scpt
Normal 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
|
||||||
10
MTMR/AppleScripts/Music.nowPlaying.scpt
Normal 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 ""
|
||||||
@ -2,6 +2,10 @@ if application "iTunes" is running then
|
|||||||
tell application "iTunes" to playpause
|
tell application "iTunes" to playpause
|
||||||
end if
|
end if
|
||||||
|
|
||||||
|
if application "Music" is running then
|
||||||
|
tell application "Music" to playpause
|
||||||
|
end if
|
||||||
|
|
||||||
if application "Spotify" is running then
|
if application "Spotify" is running then
|
||||||
tell application "Spotify" to playpause
|
tell application "Spotify" to playpause
|
||||||
end if
|
end if
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 667 B |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 2.7 KiB |
21
MTMR/Assets.xcassets/cpu.imageset/Contents.json
vendored
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
MTMR/Assets.xcassets/cpu.imageset/cpu.png
vendored
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
107
MTMR/BasicView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
326
MTMR/CBridge/AMR_ANSIEscapeHelper.h
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
//
|
||||||
|
// ANSIEscapeHelper.h
|
||||||
|
// AnsiColorsTest
|
||||||
|
//
|
||||||
|
// Created by Ali Rantakari on 18.3.09.
|
||||||
|
//
|
||||||
|
// Version 0.9.6
|
||||||
|
//
|
||||||
|
/*
|
||||||
|
The MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2008-2009,2013 Ali Rantakari
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
|
|
||||||
|
#if !__has_feature(objc_arc)
|
||||||
|
#warning "This code requires ARC to be enabled."
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
// dictionary keys for the SGR code dictionaries that the array
|
||||||
|
// escapeCodesForString:cleanString: returns contains
|
||||||
|
#define kAMRCodeDictKey_code @"code"
|
||||||
|
#define kAMRCodeDictKey_location @"location"
|
||||||
|
|
||||||
|
// dictionary keys for the string formatting attribute
|
||||||
|
// dictionaries that the array attributesForString:cleanString:
|
||||||
|
// returns contains
|
||||||
|
#define kAMRAttrDictKey_range @"range"
|
||||||
|
#define kAMRAttrDictKey_attrName @"attributeName"
|
||||||
|
#define kAMRAttrDictKey_attrValue @"attributeValue"
|
||||||
|
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@enum AMR_SGRCode
|
||||||
|
|
||||||
|
@abstract SGR (Select Graphic Rendition) ANSI control codes.
|
||||||
|
*/
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
AMR_SGRCodeNoneOrInvalid = -1,
|
||||||
|
|
||||||
|
AMR_SGRCodeAllReset = 0,
|
||||||
|
|
||||||
|
AMR_SGRCodeIntensityBold = 1,
|
||||||
|
AMR_SGRCodeIntensityFaint = 2,
|
||||||
|
AMR_SGRCodeIntensityNormal = 22,
|
||||||
|
|
||||||
|
AMR_SGRCodeItalicOn = 3,
|
||||||
|
|
||||||
|
AMR_SGRCodeUnderlineSingle = 4,
|
||||||
|
AMR_SGRCodeUnderlineDouble = 21,
|
||||||
|
AMR_SGRCodeUnderlineNone = 24,
|
||||||
|
|
||||||
|
AMR_SGRCodeFgBlack = 30,
|
||||||
|
AMR_SGRCodeFgRed = 31,
|
||||||
|
AMR_SGRCodeFgGreen = 32,
|
||||||
|
AMR_SGRCodeFgYellow = 33,
|
||||||
|
AMR_SGRCodeFgBlue = 34,
|
||||||
|
AMR_SGRCodeFgMagenta = 35,
|
||||||
|
AMR_SGRCodeFgCyan = 36,
|
||||||
|
AMR_SGRCodeFgWhite = 37,
|
||||||
|
AMR_SGRCodeFgReset = 39,
|
||||||
|
|
||||||
|
AMR_SGRCodeBgBlack = 40,
|
||||||
|
AMR_SGRCodeBgRed = 41,
|
||||||
|
AMR_SGRCodeBgGreen = 42,
|
||||||
|
AMR_SGRCodeBgYellow = 43,
|
||||||
|
AMR_SGRCodeBgBlue = 44,
|
||||||
|
AMR_SGRCodeBgMagenta = 45,
|
||||||
|
AMR_SGRCodeBgCyan = 46,
|
||||||
|
AMR_SGRCodeBgWhite = 47,
|
||||||
|
AMR_SGRCodeBgReset = 49,
|
||||||
|
|
||||||
|
AMR_SGRCodeFgBrightBlack = 90,
|
||||||
|
AMR_SGRCodeFgBrightRed = 91,
|
||||||
|
AMR_SGRCodeFgBrightGreen = 92,
|
||||||
|
AMR_SGRCodeFgBrightYellow = 93,
|
||||||
|
AMR_SGRCodeFgBrightBlue = 94,
|
||||||
|
AMR_SGRCodeFgBrightMagenta = 95,
|
||||||
|
AMR_SGRCodeFgBrightCyan = 96,
|
||||||
|
AMR_SGRCodeFgBrightWhite = 97,
|
||||||
|
|
||||||
|
AMR_SGRCodeBgBrightBlack = 100,
|
||||||
|
AMR_SGRCodeBgBrightRed = 101,
|
||||||
|
AMR_SGRCodeBgBrightGreen = 102,
|
||||||
|
AMR_SGRCodeBgBrightYellow = 103,
|
||||||
|
AMR_SGRCodeBgBrightBlue = 104,
|
||||||
|
AMR_SGRCodeBgBrightMagenta = 105,
|
||||||
|
AMR_SGRCodeBgBrightCyan = 106,
|
||||||
|
AMR_SGRCodeBgBrightWhite = 107
|
||||||
|
} AMR_SGRCode;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@class AMR_ANSIEscapeHelper
|
||||||
|
|
||||||
|
@abstract Contains helper methods for dealing with strings
|
||||||
|
that contain ANSI escape sequences for formatting (colors,
|
||||||
|
underlining, bold etc.)
|
||||||
|
*/
|
||||||
|
@interface AMR_ANSIEscapeHelper : NSObject
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@property defaultStringColor
|
||||||
|
|
||||||
|
@abstract The default color used when creating an attributed string (default is black).
|
||||||
|
*/
|
||||||
|
@property(copy) NSColor *defaultStringColor;
|
||||||
|
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@property font
|
||||||
|
|
||||||
|
@abstract The font to use when creating string formatting attribute values.
|
||||||
|
*/
|
||||||
|
@property(copy) NSFont *font;
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@property ansiColors
|
||||||
|
|
||||||
|
@abstract The colors to use for displaying ANSI colors.
|
||||||
|
|
||||||
|
@discussion Keys in this dictionary should be NSNumber objects containing SGR code
|
||||||
|
values from the AMR_SGRCode enum. The corresponding values for these keys
|
||||||
|
should be NSColor objects. If this property is nil or if it doesn't
|
||||||
|
contain a key for a specific SGR code, the default color will be used
|
||||||
|
instead.
|
||||||
|
*/
|
||||||
|
@property(retain) NSMutableDictionary *ansiColors;
|
||||||
|
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@method attributedStringWithANSIEscapedString:
|
||||||
|
|
||||||
|
@abstract Returns an attributed string that corresponds both in contents
|
||||||
|
and formatting to a given string that contains ANSI escape
|
||||||
|
sequences.
|
||||||
|
|
||||||
|
@param aString A String containing ANSI escape sequences
|
||||||
|
|
||||||
|
@result An attributed string that mimics as closely as possible
|
||||||
|
the formatting of the given ANSI-escaped string.
|
||||||
|
*/
|
||||||
|
- (NSAttributedString*) attributedStringWithANSIEscapedString:(NSString*)aString;
|
||||||
|
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@method ansiEscapedStringWithAttributedString:
|
||||||
|
|
||||||
|
@abstract Returns a string containing ANSI escape sequences that corresponds
|
||||||
|
both in contents and formatting to a given attributed string.
|
||||||
|
|
||||||
|
@param aAttributedString An attributed string
|
||||||
|
|
||||||
|
@result A string that mimics as closely as possible
|
||||||
|
the formatting of the given attributed string with
|
||||||
|
ANSI escape sequences.
|
||||||
|
*/
|
||||||
|
- (NSString*) ansiEscapedStringWithAttributedString:(NSAttributedString*)aAttributedString;
|
||||||
|
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@method escapeCodesForString:cleanString:
|
||||||
|
|
||||||
|
@abstract Returns an array of SGR codes and their locations from a
|
||||||
|
string containing ANSI escape sequences as well as a "clean"
|
||||||
|
version of the string (i.e. one without the ANSI escape
|
||||||
|
sequences.)
|
||||||
|
|
||||||
|
@param aString A String containing ANSI escape sequences
|
||||||
|
@param aCleanString Upon return, contains a "clean" version of aString (i.e. aString
|
||||||
|
without the ANSI escape sequences)
|
||||||
|
|
||||||
|
@result An array of NSDictionary objects, each of which has
|
||||||
|
an NSNumber value for the key "code" (specifying an SGR code) and
|
||||||
|
another NSNumber value for the key "location" (specifying the
|
||||||
|
location of the code within aCleanString.)
|
||||||
|
*/
|
||||||
|
- (NSArray*) escapeCodesForString:(NSString*)aString cleanString:(NSString**)aCleanString;
|
||||||
|
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@method ansiEscapedStringWithCodesAndLocations:cleanString:
|
||||||
|
|
||||||
|
@abstract Returns a string containing ANSI escape codes for formatting based
|
||||||
|
on a string and an array of SGR codes and their locations within
|
||||||
|
the given string.
|
||||||
|
|
||||||
|
@param aCodesArray An array of NSDictionary objects, each of which should have
|
||||||
|
an NSNumber value for the key "code" (specifying an SGR
|
||||||
|
code) and another NSNumber value for the key "location"
|
||||||
|
(specifying the location of this SGR code in aCleanString.)
|
||||||
|
@param aCleanString The string to which to insert the ANSI escape codes
|
||||||
|
described in aCodesArray.
|
||||||
|
|
||||||
|
@result A string containing ANSI escape sequences.
|
||||||
|
*/
|
||||||
|
- (NSString*) ansiEscapedStringWithCodesAndLocations:(NSArray*)aCodesArray cleanString:(NSString*)aCleanString;
|
||||||
|
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@method attributesForString:cleanString:
|
||||||
|
|
||||||
|
@abstract Convert ANSI escape sequences in a string to string formatting attributes.
|
||||||
|
|
||||||
|
@discussion Given a string with some ANSI escape sequences in it, this method returns
|
||||||
|
attributes for formatting the specified string according to those ANSI
|
||||||
|
escape sequences as well as a "clean" (i.e. free of the escape sequences)
|
||||||
|
version of this string.
|
||||||
|
|
||||||
|
@param aString A String containing ANSI escape sequences
|
||||||
|
@param aCleanString Upon return, contains a "clean" version of aString (i.e. aString
|
||||||
|
without the ANSI escape sequences.) Pass in NULL if you're not
|
||||||
|
interested in this.
|
||||||
|
|
||||||
|
@result An array containing NSDictionary objects, each of which has keys "range"
|
||||||
|
(an NSValue containing an NSRange, specifying the range for the
|
||||||
|
attribute within the "clean" version of aString), "attributeName" (an
|
||||||
|
NSString) and "attributeValue" (an NSObject). You may use these as
|
||||||
|
arguments for NSMutableAttributedString's methods for setting the
|
||||||
|
visual formatting.
|
||||||
|
*/
|
||||||
|
- (NSArray*) attributesForString:(NSString*)aString cleanString:(NSString**)aCleanString;
|
||||||
|
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@method AMR_SGRCode:endsFormattingIntroducedByCode:
|
||||||
|
|
||||||
|
@abstract Whether the occurrence of a given SGR code would end the formatting run
|
||||||
|
introduced by another SGR code.
|
||||||
|
|
||||||
|
@discussion For example, AMR_SGRCodeFgReset, AMR_SGRCodeAllReset or any SGR code
|
||||||
|
specifying a foreground color would end the formatting run
|
||||||
|
introduced by a foreground color -specifying SGR code.
|
||||||
|
|
||||||
|
@param endCode The SGR code to test as a candidate for ending the formatting run
|
||||||
|
introduced by startCode
|
||||||
|
@param startCode The SGR code that has introduced a formatting run
|
||||||
|
|
||||||
|
@result YES if the occurrence of endCode would end the formatting run
|
||||||
|
introduced by startCode, NO otherwise.
|
||||||
|
*/
|
||||||
|
- (BOOL) AMR_SGRCode:(AMR_SGRCode)endCode endsFormattingIntroducedByCode:(AMR_SGRCode)startCode;
|
||||||
|
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@method colorForSGRCode:
|
||||||
|
|
||||||
|
@abstract Returns the color to use for displaying a specific ANSI color.
|
||||||
|
|
||||||
|
@discussion This method first considers the values set in the ansiColors
|
||||||
|
property and only then the standard basic colors (NSColor's
|
||||||
|
redColor, blueColor etc.)
|
||||||
|
|
||||||
|
@param code An SGR code that specifies an ANSI color.
|
||||||
|
|
||||||
|
@result The color to use for displaying the ANSI color specified by code.
|
||||||
|
*/
|
||||||
|
- (NSColor*) colorForSGRCode:(AMR_SGRCode)code;
|
||||||
|
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@method AMR_SGRCodeForColor:isForegroundColor:
|
||||||
|
|
||||||
|
@abstract Returns a color SGR code that corresponds to a given color.
|
||||||
|
|
||||||
|
@discussion This method matches colors to their equivalent SGR codes
|
||||||
|
by going through the colors specified in the ansiColors
|
||||||
|
dictionary, and if ansiColors is null or if a match is
|
||||||
|
not found there, by comparing the given color to the
|
||||||
|
standard basic colors (NSColor's redColor, blueColor
|
||||||
|
etc.) The comparison is done simply by checking for
|
||||||
|
equality.
|
||||||
|
|
||||||
|
@param aColor The color to get a corresponding SGR code for
|
||||||
|
@param aForeground Whether you want a foreground or background color code
|
||||||
|
|
||||||
|
@result SGR code that corresponds with aColor.
|
||||||
|
*/
|
||||||
|
- (AMR_SGRCode) AMR_SGRCodeForColor:(NSColor*)aColor isForegroundColor:(BOOL)aForeground;
|
||||||
|
|
||||||
|
|
||||||
|
/*!
|
||||||
|
@method closestSGRCodeForColor:isForegroundColor:
|
||||||
|
|
||||||
|
@abstract Returns a color SGR code that represents the closest ANSI
|
||||||
|
color to a given color.
|
||||||
|
|
||||||
|
@discussion This method attempts to find the closest ANSI color to
|
||||||
|
aColor and return its SGR code.
|
||||||
|
|
||||||
|
@param color The color to get a closest color SGR code match for
|
||||||
|
@param foreground Whether you want a foreground or background color code
|
||||||
|
|
||||||
|
@result SGR code for the ANSI color that is closest to aColor.
|
||||||
|
*/
|
||||||
|
- (AMR_SGRCode) closestSGRCodeForColor:(NSColor *)color isForegroundColor:(BOOL)foreground;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@end
|
||||||
996
MTMR/CBridge/AMR_ANSIEscapeHelper.m
Normal file
@ -0,0 +1,996 @@
|
|||||||
|
//
|
||||||
|
// ANSIEscapeHelper.m
|
||||||
|
//
|
||||||
|
// Created by Ali Rantakari on 18.3.09.
|
||||||
|
|
||||||
|
/*
|
||||||
|
The MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2008-2009,2013 Ali Rantakari
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
todo:
|
||||||
|
|
||||||
|
- don't add useless "reset" escape codes to the string in
|
||||||
|
-ansiEscapedStringWithAttributedString:
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#import "AMR_ANSIEscapeHelper.h"
|
||||||
|
|
||||||
|
|
||||||
|
// the CSI (Control Sequence Initiator) -- i.e. "escape sequence prefix".
|
||||||
|
// (add your own CSI:Miami joke here)
|
||||||
|
#define kANSIEscapeCSI @"\033["
|
||||||
|
|
||||||
|
// the end byte of an SGR (Select Graphic Rendition)
|
||||||
|
// ANSI Escape Sequence
|
||||||
|
#define kANSIEscapeSGREnd @"m"
|
||||||
|
|
||||||
|
|
||||||
|
// color definition helper macros
|
||||||
|
#define kBrightColorBrightness 1.0
|
||||||
|
#define kBrightColorSaturation 0.4
|
||||||
|
#define kBrightColorAlpha 1.0
|
||||||
|
#define kBrightColorWithHue(h) [NSColor colorWithCalibratedHue:(h) saturation:kBrightColorSaturation brightness:kBrightColorBrightness alpha:kBrightColorAlpha]
|
||||||
|
|
||||||
|
// default colors
|
||||||
|
#define kDefaultANSIColorFgBlack NSColor.blackColor
|
||||||
|
#define kDefaultANSIColorFgRed NSColor.redColor
|
||||||
|
#define kDefaultANSIColorFgGreen NSColor.greenColor
|
||||||
|
#define kDefaultANSIColorFgYellow NSColor.yellowColor
|
||||||
|
#define kDefaultANSIColorFgBlue NSColor.blueColor
|
||||||
|
#define kDefaultANSIColorFgMagenta NSColor.magentaColor
|
||||||
|
#define kDefaultANSIColorFgCyan NSColor.cyanColor
|
||||||
|
#define kDefaultANSIColorFgWhite NSColor.whiteColor
|
||||||
|
|
||||||
|
#define kDefaultANSIColorFgBrightBlack [NSColor colorWithCalibratedWhite:0.337 alpha:1.0]
|
||||||
|
#define kDefaultANSIColorFgBrightRed kBrightColorWithHue(1.0)
|
||||||
|
#define kDefaultANSIColorFgBrightGreen kBrightColorWithHue(1.0/3.0)
|
||||||
|
#define kDefaultANSIColorFgBrightYellow kBrightColorWithHue(1.0/6.0)
|
||||||
|
#define kDefaultANSIColorFgBrightBlue kBrightColorWithHue(2.0/3.0)
|
||||||
|
#define kDefaultANSIColorFgBrightMagenta kBrightColorWithHue(5.0/6.0)
|
||||||
|
#define kDefaultANSIColorFgBrightCyan kBrightColorWithHue(0.5)
|
||||||
|
#define kDefaultANSIColorFgBrightWhite NSColor.whiteColor
|
||||||
|
|
||||||
|
#define kDefaultANSIColorBgBlack NSColor.blackColor
|
||||||
|
#define kDefaultANSIColorBgRed NSColor.redColor
|
||||||
|
#define kDefaultANSIColorBgGreen NSColor.greenColor
|
||||||
|
#define kDefaultANSIColorBgYellow NSColor.yellowColor
|
||||||
|
#define kDefaultANSIColorBgBlue NSColor.blueColor
|
||||||
|
#define kDefaultANSIColorBgMagenta NSColor.magentaColor
|
||||||
|
#define kDefaultANSIColorBgCyan NSColor.cyanColor
|
||||||
|
#define kDefaultANSIColorBgWhite NSColor.whiteColor
|
||||||
|
|
||||||
|
#define kDefaultANSIColorBgBrightBlack kDefaultANSIColorFgBrightBlack
|
||||||
|
#define kDefaultANSIColorBgBrightRed kDefaultANSIColorFgBrightRed
|
||||||
|
#define kDefaultANSIColorBgBrightGreen kDefaultANSIColorFgBrightGreen
|
||||||
|
#define kDefaultANSIColorBgBrightYellow kDefaultANSIColorFgBrightYellow
|
||||||
|
#define kDefaultANSIColorBgBrightBlue kDefaultANSIColorFgBrightBlue
|
||||||
|
#define kDefaultANSIColorBgBrightMagenta kDefaultANSIColorFgBrightMagenta
|
||||||
|
#define kDefaultANSIColorBgBrightCyan kDefaultANSIColorFgBrightCyan
|
||||||
|
#define kDefaultANSIColorBgBrightWhite kDefaultANSIColorFgBrightWhite
|
||||||
|
|
||||||
|
#define kDefaultFontSize [NSFont systemFontOfSize:NSFont.systemFontSize]
|
||||||
|
#define kDefaultForegroundColor NSColor.blackColor
|
||||||
|
|
||||||
|
// minimum weight for an NSFont for it to be considered bold
|
||||||
|
#define kBoldFontMinWeight 9
|
||||||
|
|
||||||
|
|
||||||
|
@implementation AMR_ANSIEscapeHelper
|
||||||
|
|
||||||
|
- (id) init
|
||||||
|
{
|
||||||
|
if (!(self = [super init]))
|
||||||
|
return nil;
|
||||||
|
|
||||||
|
self.ansiColors = [NSMutableDictionary dictionary];
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- (NSAttributedString*) attributedStringWithANSIEscapedString:(NSString*)aString
|
||||||
|
{
|
||||||
|
if (aString == nil)
|
||||||
|
return nil;
|
||||||
|
|
||||||
|
NSString *cleanString;
|
||||||
|
NSArray *attributesAndRanges = [self attributesForString:aString cleanString:&cleanString];
|
||||||
|
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc]
|
||||||
|
initWithString:cleanString
|
||||||
|
attributes:@{
|
||||||
|
NSFontAttributeName: self.font ?: kDefaultFontSize,
|
||||||
|
NSForegroundColorAttributeName: self.defaultStringColor ?: kDefaultForegroundColor
|
||||||
|
}];
|
||||||
|
|
||||||
|
for (NSDictionary *thisAttributeDict in attributesAndRanges)
|
||||||
|
{
|
||||||
|
[attributedString
|
||||||
|
addAttribute:thisAttributeDict[kAMRAttrDictKey_attrName]
|
||||||
|
value:thisAttributeDict[kAMRAttrDictKey_attrValue]
|
||||||
|
range:[thisAttributeDict[kAMRAttrDictKey_range] rangeValue]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributedString;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- (NSString*) ansiEscapedStringWithAttributedString:(NSAttributedString*)aAttributedString
|
||||||
|
{
|
||||||
|
NSMutableArray *codesAndLocations = [NSMutableArray array];
|
||||||
|
|
||||||
|
NSArray *attrNames = @[
|
||||||
|
NSFontAttributeName, NSForegroundColorAttributeName,
|
||||||
|
NSBackgroundColorAttributeName, NSUnderlineStyleAttributeName,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (NSString *thisAttrName in attrNames)
|
||||||
|
{
|
||||||
|
NSRange limitRange = NSMakeRange(0, aAttributedString.length);
|
||||||
|
id attributeValue;
|
||||||
|
NSRange effectiveRange;
|
||||||
|
|
||||||
|
while (limitRange.length > 0)
|
||||||
|
{
|
||||||
|
attributeValue = [aAttributedString
|
||||||
|
attribute:thisAttrName
|
||||||
|
atIndex:limitRange.location
|
||||||
|
longestEffectiveRange:&effectiveRange
|
||||||
|
inRange:limitRange
|
||||||
|
];
|
||||||
|
|
||||||
|
AMR_SGRCode thisSGRCode = AMR_SGRCodeNoneOrInvalid;
|
||||||
|
|
||||||
|
if ([thisAttrName isEqualToString:NSForegroundColorAttributeName])
|
||||||
|
{
|
||||||
|
if (attributeValue != nil)
|
||||||
|
thisSGRCode = [self closestSGRCodeForColor:attributeValue isForegroundColor:YES];
|
||||||
|
else
|
||||||
|
thisSGRCode = AMR_SGRCodeFgReset;
|
||||||
|
}
|
||||||
|
else if ([thisAttrName isEqualToString:NSBackgroundColorAttributeName])
|
||||||
|
{
|
||||||
|
if (attributeValue != nil)
|
||||||
|
thisSGRCode = [self closestSGRCodeForColor:attributeValue isForegroundColor:NO];
|
||||||
|
else
|
||||||
|
thisSGRCode = AMR_SGRCodeBgReset;
|
||||||
|
}
|
||||||
|
else if ([thisAttrName isEqualToString:NSFontAttributeName])
|
||||||
|
{
|
||||||
|
// we currently only use NSFontAttributeName for bolding so
|
||||||
|
// here we assume that the formatting "type" in ANSI SGR
|
||||||
|
// terms is indeed intensity
|
||||||
|
if (attributeValue != nil)
|
||||||
|
thisSGRCode = ([NSFontManager.sharedFontManager weightOfFont:attributeValue] >= kBoldFontMinWeight)
|
||||||
|
? AMR_SGRCodeIntensityBold : AMR_SGRCodeIntensityNormal;
|
||||||
|
else
|
||||||
|
thisSGRCode = AMR_SGRCodeIntensityNormal;
|
||||||
|
}
|
||||||
|
else if ([thisAttrName isEqualToString:NSUnderlineStyleAttributeName])
|
||||||
|
{
|
||||||
|
if (attributeValue != nil)
|
||||||
|
{
|
||||||
|
if ([attributeValue intValue] == NSUnderlineStyleSingle)
|
||||||
|
thisSGRCode = AMR_SGRCodeUnderlineSingle;
|
||||||
|
else if ([attributeValue intValue] == NSUnderlineStyleDouble)
|
||||||
|
thisSGRCode = AMR_SGRCodeUnderlineDouble;
|
||||||
|
else
|
||||||
|
thisSGRCode = AMR_SGRCodeUnderlineNone;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
thisSGRCode = AMR_SGRCodeUnderlineNone;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thisSGRCode != AMR_SGRCodeNoneOrInvalid)
|
||||||
|
{
|
||||||
|
[codesAndLocations addObject: @{
|
||||||
|
kAMRCodeDictKey_code: @(thisSGRCode),
|
||||||
|
kAMRCodeDictKey_location: @(effectiveRange.location),
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
limitRange = NSMakeRange(NSMaxRange(effectiveRange),
|
||||||
|
NSMaxRange(limitRange) - NSMaxRange(effectiveRange));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [self ansiEscapedStringWithCodesAndLocations:codesAndLocations cleanString:aAttributedString.string];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
- (NSArray*) escapeCodesForString:(NSString*)aString cleanString:(NSString**)aCleanString
|
||||||
|
{
|
||||||
|
if (aString == nil)
|
||||||
|
return nil;
|
||||||
|
if (aString.length <= kANSIEscapeCSI.length)
|
||||||
|
{
|
||||||
|
if (aCleanString)
|
||||||
|
*aCleanString = aString.copy;
|
||||||
|
return @[];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *cleanString = @"";
|
||||||
|
|
||||||
|
// find all escape sequence codes from aString and put them in this array
|
||||||
|
// along with their start locations within the "clean" version of aString
|
||||||
|
NSMutableArray *formatCodes = [NSMutableArray array];
|
||||||
|
|
||||||
|
NSUInteger aStringLength = aString.length;
|
||||||
|
NSUInteger coveredLength = 0;
|
||||||
|
NSRange searchRange = NSMakeRange(0,aStringLength);
|
||||||
|
NSRange thisEscapeSequenceRange;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
thisEscapeSequenceRange = [aString rangeOfString:kANSIEscapeCSI options:NSLiteralSearch range:searchRange];
|
||||||
|
if (thisEscapeSequenceRange.location != NSNotFound)
|
||||||
|
{
|
||||||
|
// adjust range's length so that it encompasses the whole ANSI escape sequence
|
||||||
|
// and not just the Control Sequence Initiator (the "prefix") by finding the
|
||||||
|
// final byte of the control sequence (one that has an ASCII decimal value
|
||||||
|
// between 64 and 126.) at the same time, read all formatting codes from inside
|
||||||
|
// this escape sequence (there may be several, separated by semicolons.)
|
||||||
|
NSMutableArray *codes = [NSMutableArray array];
|
||||||
|
unsigned int code = 0;
|
||||||
|
unsigned int lengthAddition = 1;
|
||||||
|
NSUInteger thisIndex;
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
thisIndex = (NSMaxRange(thisEscapeSequenceRange)+lengthAddition-1);
|
||||||
|
if (thisIndex >= aStringLength)
|
||||||
|
break;
|
||||||
|
|
||||||
|
unichar c = [aString characterAtIndex:thisIndex];
|
||||||
|
|
||||||
|
if (('0' <= c) && (c <= '9'))
|
||||||
|
{
|
||||||
|
int digit = c - '0';
|
||||||
|
code = (code == 0) ? digit : code*10+digit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ASCII decimal 109 is the SGR (Select Graphic Rendition) final byte
|
||||||
|
// ("m"). this means that the code value we've just read specifies formatting
|
||||||
|
// for the output; exactly what we're interested in.
|
||||||
|
if (c == 'm')
|
||||||
|
{
|
||||||
|
[codes addObject:@(code)];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if ((64 <= c) && (c <= 126)) // any other valid final byte
|
||||||
|
{
|
||||||
|
[codes removeAllObjects];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (c == ';') // separates codes within the same sequence
|
||||||
|
{
|
||||||
|
[codes addObject:@(code)];
|
||||||
|
code = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
lengthAddition++;
|
||||||
|
}
|
||||||
|
thisEscapeSequenceRange.length += lengthAddition;
|
||||||
|
|
||||||
|
NSUInteger locationInCleanString = coveredLength+thisEscapeSequenceRange.location-searchRange.location;
|
||||||
|
|
||||||
|
for (NSNumber *codeToAdd in codes)
|
||||||
|
{
|
||||||
|
[formatCodes addObject: @{
|
||||||
|
kAMRCodeDictKey_code: codeToAdd,
|
||||||
|
kAMRCodeDictKey_location: @(locationInCleanString)
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSUInteger thisCoveredLength = thisEscapeSequenceRange.location-searchRange.location;
|
||||||
|
if (thisCoveredLength > 0)
|
||||||
|
cleanString = [cleanString stringByAppendingString:[aString substringWithRange:NSMakeRange(searchRange.location, thisCoveredLength)]];
|
||||||
|
|
||||||
|
coveredLength += thisCoveredLength;
|
||||||
|
searchRange.location = NSMaxRange(thisEscapeSequenceRange);
|
||||||
|
searchRange.length = aStringLength-searchRange.location;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while(thisEscapeSequenceRange.location != NSNotFound);
|
||||||
|
|
||||||
|
if (searchRange.length > 0)
|
||||||
|
cleanString = [cleanString stringByAppendingString:[aString substringWithRange:searchRange]];
|
||||||
|
|
||||||
|
if (aCleanString)
|
||||||
|
*aCleanString = cleanString;
|
||||||
|
return formatCodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- (NSString*) ansiEscapedStringWithCodesAndLocations:(NSArray*)aCodesArray cleanString:(NSString*)aCleanString
|
||||||
|
{
|
||||||
|
NSMutableString* retStr = [NSMutableString stringWithCapacity:aCleanString.length];
|
||||||
|
|
||||||
|
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:kAMRCodeDictKey_location ascending:YES];
|
||||||
|
NSArray *codesArray = [aCodesArray sortedArrayUsingDescriptors:@[sortDescriptor]];
|
||||||
|
|
||||||
|
NSUInteger aCleanStringIndex = 0;
|
||||||
|
NSUInteger aCleanStringLength = aCleanString.length;
|
||||||
|
for (NSDictionary *thisCodeDict in codesArray)
|
||||||
|
{
|
||||||
|
if (!( thisCodeDict[kAMRCodeDictKey_code] &&
|
||||||
|
thisCodeDict[kAMRCodeDictKey_location]
|
||||||
|
))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
AMR_SGRCode thisCode = [thisCodeDict[kAMRCodeDictKey_code] unsignedIntValue];
|
||||||
|
NSUInteger formattingRunStartLocation = [thisCodeDict[kAMRCodeDictKey_location] unsignedIntegerValue];
|
||||||
|
|
||||||
|
if (formattingRunStartLocation > aCleanStringLength)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (aCleanStringIndex < formattingRunStartLocation)
|
||||||
|
[retStr appendString:[aCleanString substringWithRange:NSMakeRange(aCleanStringIndex, formattingRunStartLocation-aCleanStringIndex)]];
|
||||||
|
[retStr appendFormat:@"%@%d%@", kANSIEscapeCSI, thisCode, kANSIEscapeSGREnd];
|
||||||
|
|
||||||
|
aCleanStringIndex = formattingRunStartLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aCleanStringIndex < aCleanStringLength)
|
||||||
|
[retStr appendString:[aCleanString substringFromIndex:aCleanStringIndex]];
|
||||||
|
|
||||||
|
[retStr appendFormat:@"%@%d%@", kANSIEscapeCSI, AMR_SGRCodeAllReset, kANSIEscapeSGREnd];
|
||||||
|
|
||||||
|
return retStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- (NSArray*) attributesForString:(NSString*)aString cleanString:(NSString**)aCleanString
|
||||||
|
{
|
||||||
|
if (aString == nil)
|
||||||
|
return nil;
|
||||||
|
if (aString.length <= kANSIEscapeCSI.length)
|
||||||
|
{
|
||||||
|
if (aCleanString)
|
||||||
|
*aCleanString = aString.copy;
|
||||||
|
return @[];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSMutableArray *attrsAndRanges = [NSMutableArray array];
|
||||||
|
|
||||||
|
NSString *cleanString;
|
||||||
|
NSArray *formatCodes = [self escapeCodesForString:aString cleanString:&cleanString];
|
||||||
|
|
||||||
|
// go through all the found escape sequence codes and for each one, create
|
||||||
|
// the string formatting attribute name and value, find the next escape
|
||||||
|
// sequence that specifies the end of the formatting run started by
|
||||||
|
// the currently handled code, and generate a range from the difference
|
||||||
|
// in those codes' locations within the clean aString.
|
||||||
|
for (NSUInteger iCode = 0; iCode < formatCodes.count; iCode++)
|
||||||
|
{
|
||||||
|
NSDictionary *thisCodeDict = formatCodes[iCode];
|
||||||
|
AMR_SGRCode thisCode = [thisCodeDict[kAMRCodeDictKey_code] unsignedIntValue];
|
||||||
|
NSUInteger formattingRunStartLocation = [thisCodeDict[kAMRCodeDictKey_location] unsignedIntegerValue];
|
||||||
|
|
||||||
|
// the attributed string attribute name for the formatting run introduced
|
||||||
|
// by this code
|
||||||
|
NSString *thisAttributeName = nil;
|
||||||
|
|
||||||
|
// the attributed string attribute value for this formatting run introduced
|
||||||
|
// by this code
|
||||||
|
NSObject *thisAttributeValue = nil;
|
||||||
|
|
||||||
|
// set attribute name
|
||||||
|
switch(thisCode)
|
||||||
|
{
|
||||||
|
case AMR_SGRCodeFgBlack:
|
||||||
|
case AMR_SGRCodeFgRed:
|
||||||
|
case AMR_SGRCodeFgGreen:
|
||||||
|
case AMR_SGRCodeFgYellow:
|
||||||
|
case AMR_SGRCodeFgBlue:
|
||||||
|
case AMR_SGRCodeFgMagenta:
|
||||||
|
case AMR_SGRCodeFgCyan:
|
||||||
|
case AMR_SGRCodeFgWhite:
|
||||||
|
case AMR_SGRCodeFgBrightBlack:
|
||||||
|
case AMR_SGRCodeFgBrightRed:
|
||||||
|
case AMR_SGRCodeFgBrightGreen:
|
||||||
|
case AMR_SGRCodeFgBrightYellow:
|
||||||
|
case AMR_SGRCodeFgBrightBlue:
|
||||||
|
case AMR_SGRCodeFgBrightMagenta:
|
||||||
|
case AMR_SGRCodeFgBrightCyan:
|
||||||
|
case AMR_SGRCodeFgBrightWhite:
|
||||||
|
thisAttributeName = NSForegroundColorAttributeName;
|
||||||
|
break;
|
||||||
|
case AMR_SGRCodeBgBlack:
|
||||||
|
case AMR_SGRCodeBgRed:
|
||||||
|
case AMR_SGRCodeBgGreen:
|
||||||
|
case AMR_SGRCodeBgYellow:
|
||||||
|
case AMR_SGRCodeBgBlue:
|
||||||
|
case AMR_SGRCodeBgMagenta:
|
||||||
|
case AMR_SGRCodeBgCyan:
|
||||||
|
case AMR_SGRCodeBgWhite:
|
||||||
|
case AMR_SGRCodeBgBrightBlack:
|
||||||
|
case AMR_SGRCodeBgBrightRed:
|
||||||
|
case AMR_SGRCodeBgBrightGreen:
|
||||||
|
case AMR_SGRCodeBgBrightYellow:
|
||||||
|
case AMR_SGRCodeBgBrightBlue:
|
||||||
|
case AMR_SGRCodeBgBrightMagenta:
|
||||||
|
case AMR_SGRCodeBgBrightCyan:
|
||||||
|
case AMR_SGRCodeBgBrightWhite:
|
||||||
|
thisAttributeName = NSBackgroundColorAttributeName;
|
||||||
|
break;
|
||||||
|
case AMR_SGRCodeIntensityBold:
|
||||||
|
case AMR_SGRCodeIntensityNormal:
|
||||||
|
case AMR_SGRCodeIntensityFaint:
|
||||||
|
thisAttributeName = NSFontAttributeName;
|
||||||
|
break;
|
||||||
|
case AMR_SGRCodeUnderlineSingle:
|
||||||
|
case AMR_SGRCodeUnderlineDouble:
|
||||||
|
case AMR_SGRCodeUnderlineNone:
|
||||||
|
thisAttributeName = NSUnderlineStyleAttributeName;
|
||||||
|
break;
|
||||||
|
case AMR_SGRCodeAllReset:
|
||||||
|
case AMR_SGRCodeFgReset:
|
||||||
|
case AMR_SGRCodeBgReset:
|
||||||
|
case AMR_SGRCodeNoneOrInvalid:
|
||||||
|
case AMR_SGRCodeItalicOn:
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set attribute value
|
||||||
|
switch(thisCode)
|
||||||
|
{
|
||||||
|
case AMR_SGRCodeBgBlack:
|
||||||
|
case AMR_SGRCodeFgBlack:
|
||||||
|
case AMR_SGRCodeBgRed:
|
||||||
|
case AMR_SGRCodeFgRed:
|
||||||
|
case AMR_SGRCodeBgGreen:
|
||||||
|
case AMR_SGRCodeFgGreen:
|
||||||
|
case AMR_SGRCodeBgYellow:
|
||||||
|
case AMR_SGRCodeFgYellow:
|
||||||
|
case AMR_SGRCodeBgBlue:
|
||||||
|
case AMR_SGRCodeFgBlue:
|
||||||
|
case AMR_SGRCodeBgMagenta:
|
||||||
|
case AMR_SGRCodeFgMagenta:
|
||||||
|
case AMR_SGRCodeBgCyan:
|
||||||
|
case AMR_SGRCodeFgCyan:
|
||||||
|
case AMR_SGRCodeBgWhite:
|
||||||
|
case AMR_SGRCodeFgWhite:
|
||||||
|
case AMR_SGRCodeBgBrightBlack:
|
||||||
|
case AMR_SGRCodeFgBrightBlack:
|
||||||
|
case AMR_SGRCodeBgBrightRed:
|
||||||
|
case AMR_SGRCodeFgBrightRed:
|
||||||
|
case AMR_SGRCodeBgBrightGreen:
|
||||||
|
case AMR_SGRCodeFgBrightGreen:
|
||||||
|
case AMR_SGRCodeBgBrightYellow:
|
||||||
|
case AMR_SGRCodeFgBrightYellow:
|
||||||
|
case AMR_SGRCodeBgBrightBlue:
|
||||||
|
case AMR_SGRCodeFgBrightBlue:
|
||||||
|
case AMR_SGRCodeBgBrightMagenta:
|
||||||
|
case AMR_SGRCodeFgBrightMagenta:
|
||||||
|
case AMR_SGRCodeBgBrightCyan:
|
||||||
|
case AMR_SGRCodeFgBrightCyan:
|
||||||
|
case AMR_SGRCodeBgBrightWhite:
|
||||||
|
case AMR_SGRCodeFgBrightWhite:
|
||||||
|
thisAttributeValue = [self colorForSGRCode:thisCode];
|
||||||
|
break;
|
||||||
|
case AMR_SGRCodeIntensityBold:
|
||||||
|
{
|
||||||
|
NSFont *boldFont = [NSFontManager.sharedFontManager convertFont:self.font toHaveTrait:NSBoldFontMask];
|
||||||
|
thisAttributeValue = boldFont;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case AMR_SGRCodeIntensityNormal:
|
||||||
|
case AMR_SGRCodeIntensityFaint:
|
||||||
|
{
|
||||||
|
NSFont *unboldFont = [NSFontManager.sharedFontManager convertFont:self.font toHaveTrait:NSUnboldFontMask];
|
||||||
|
thisAttributeValue = unboldFont;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case AMR_SGRCodeUnderlineSingle:
|
||||||
|
thisAttributeValue = @(NSUnderlineStyleSingle);
|
||||||
|
break;
|
||||||
|
case AMR_SGRCodeUnderlineDouble:
|
||||||
|
thisAttributeValue = @(NSUnderlineStyleDouble);
|
||||||
|
break;
|
||||||
|
case AMR_SGRCodeUnderlineNone:
|
||||||
|
thisAttributeValue = @(NSUnderlineStyleNone);
|
||||||
|
break;
|
||||||
|
case AMR_SGRCodeAllReset:
|
||||||
|
case AMR_SGRCodeFgReset:
|
||||||
|
case AMR_SGRCodeBgReset:
|
||||||
|
case AMR_SGRCodeNoneOrInvalid:
|
||||||
|
case AMR_SGRCodeItalicOn:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// find the next sequence that specifies the end of this formatting run
|
||||||
|
NSInteger formattingRunEndLocation = -1;
|
||||||
|
if (iCode < (formatCodes.count - 1))
|
||||||
|
{
|
||||||
|
NSDictionary *thisEndCodeCandidateDict;
|
||||||
|
unichar thisEndCodeCandidate;
|
||||||
|
for (NSUInteger iEndCode = iCode+1; iEndCode < formatCodes.count; iEndCode++)
|
||||||
|
{
|
||||||
|
thisEndCodeCandidateDict = formatCodes[iEndCode];
|
||||||
|
thisEndCodeCandidate = [thisEndCodeCandidateDict[kAMRCodeDictKey_code] unsignedIntValue];
|
||||||
|
|
||||||
|
if ([self AMR_SGRCode:thisEndCodeCandidate endsFormattingIntroducedByCode:thisCode])
|
||||||
|
{
|
||||||
|
formattingRunEndLocation = [thisEndCodeCandidateDict[kAMRCodeDictKey_location] unsignedIntegerValue];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (formattingRunEndLocation == -1)
|
||||||
|
formattingRunEndLocation = cleanString.length;
|
||||||
|
|
||||||
|
if (thisAttributeName && thisAttributeValue)
|
||||||
|
{
|
||||||
|
[attrsAndRanges addObject:@{
|
||||||
|
kAMRAttrDictKey_range: [NSValue valueWithRange:NSMakeRange(formattingRunStartLocation, (formattingRunEndLocation-formattingRunStartLocation))],
|
||||||
|
kAMRAttrDictKey_attrName: thisAttributeName,
|
||||||
|
kAMRAttrDictKey_attrValue: thisAttributeValue,
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aCleanString)
|
||||||
|
*aCleanString = cleanString;
|
||||||
|
return attrsAndRanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- (BOOL) AMR_SGRCode:(AMR_SGRCode)endCode endsFormattingIntroducedByCode:(AMR_SGRCode)startCode
|
||||||
|
{
|
||||||
|
switch(startCode)
|
||||||
|
{
|
||||||
|
case AMR_SGRCodeFgBlack:
|
||||||
|
case AMR_SGRCodeFgRed:
|
||||||
|
case AMR_SGRCodeFgGreen:
|
||||||
|
case AMR_SGRCodeFgYellow:
|
||||||
|
case AMR_SGRCodeFgBlue:
|
||||||
|
case AMR_SGRCodeFgMagenta:
|
||||||
|
case AMR_SGRCodeFgCyan:
|
||||||
|
case AMR_SGRCodeFgWhite:
|
||||||
|
case AMR_SGRCodeFgBrightBlack:
|
||||||
|
case AMR_SGRCodeFgBrightRed:
|
||||||
|
case AMR_SGRCodeFgBrightGreen:
|
||||||
|
case AMR_SGRCodeFgBrightYellow:
|
||||||
|
case AMR_SGRCodeFgBrightBlue:
|
||||||
|
case AMR_SGRCodeFgBrightMagenta:
|
||||||
|
case AMR_SGRCodeFgBrightCyan:
|
||||||
|
case AMR_SGRCodeFgBrightWhite:
|
||||||
|
return (endCode == AMR_SGRCodeAllReset || endCode == AMR_SGRCodeFgReset ||
|
||||||
|
endCode == AMR_SGRCodeFgBlack || endCode == AMR_SGRCodeFgRed ||
|
||||||
|
endCode == AMR_SGRCodeFgGreen || endCode == AMR_SGRCodeFgYellow ||
|
||||||
|
endCode == AMR_SGRCodeFgBlue || endCode == AMR_SGRCodeFgMagenta ||
|
||||||
|
endCode == AMR_SGRCodeFgCyan || endCode == AMR_SGRCodeFgWhite ||
|
||||||
|
endCode == AMR_SGRCodeFgBrightBlack || endCode == AMR_SGRCodeFgBrightRed ||
|
||||||
|
endCode == AMR_SGRCodeFgBrightGreen || endCode == AMR_SGRCodeFgBrightYellow ||
|
||||||
|
endCode == AMR_SGRCodeFgBrightBlue || endCode == AMR_SGRCodeFgBrightMagenta ||
|
||||||
|
endCode == AMR_SGRCodeFgBrightCyan || endCode == AMR_SGRCodeFgBrightWhite);
|
||||||
|
case AMR_SGRCodeBgBlack:
|
||||||
|
case AMR_SGRCodeBgRed:
|
||||||
|
case AMR_SGRCodeBgGreen:
|
||||||
|
case AMR_SGRCodeBgYellow:
|
||||||
|
case AMR_SGRCodeBgBlue:
|
||||||
|
case AMR_SGRCodeBgMagenta:
|
||||||
|
case AMR_SGRCodeBgCyan:
|
||||||
|
case AMR_SGRCodeBgWhite:
|
||||||
|
case AMR_SGRCodeBgBrightBlack:
|
||||||
|
case AMR_SGRCodeBgBrightRed:
|
||||||
|
case AMR_SGRCodeBgBrightGreen:
|
||||||
|
case AMR_SGRCodeBgBrightYellow:
|
||||||
|
case AMR_SGRCodeBgBrightBlue:
|
||||||
|
case AMR_SGRCodeBgBrightMagenta:
|
||||||
|
case AMR_SGRCodeBgBrightCyan:
|
||||||
|
case AMR_SGRCodeBgBrightWhite:
|
||||||
|
return (endCode == AMR_SGRCodeAllReset || endCode == AMR_SGRCodeBgReset ||
|
||||||
|
endCode == AMR_SGRCodeBgBlack || endCode == AMR_SGRCodeBgRed ||
|
||||||
|
endCode == AMR_SGRCodeBgGreen || endCode == AMR_SGRCodeBgYellow ||
|
||||||
|
endCode == AMR_SGRCodeBgBlue || endCode == AMR_SGRCodeBgMagenta ||
|
||||||
|
endCode == AMR_SGRCodeBgCyan || endCode == AMR_SGRCodeBgWhite ||
|
||||||
|
endCode == AMR_SGRCodeBgBrightBlack || endCode == AMR_SGRCodeBgBrightRed ||
|
||||||
|
endCode == AMR_SGRCodeBgBrightGreen || endCode == AMR_SGRCodeBgBrightYellow ||
|
||||||
|
endCode == AMR_SGRCodeBgBrightBlue || endCode == AMR_SGRCodeBgBrightMagenta ||
|
||||||
|
endCode == AMR_SGRCodeBgBrightCyan || endCode == AMR_SGRCodeBgBrightWhite);
|
||||||
|
case AMR_SGRCodeIntensityBold:
|
||||||
|
case AMR_SGRCodeIntensityNormal:
|
||||||
|
return (endCode == AMR_SGRCodeAllReset || endCode == AMR_SGRCodeIntensityNormal ||
|
||||||
|
endCode == AMR_SGRCodeIntensityBold || endCode == AMR_SGRCodeIntensityFaint);
|
||||||
|
case AMR_SGRCodeUnderlineSingle:
|
||||||
|
case AMR_SGRCodeUnderlineDouble:
|
||||||
|
return (endCode == AMR_SGRCodeAllReset || endCode == AMR_SGRCodeUnderlineNone ||
|
||||||
|
endCode == AMR_SGRCodeUnderlineSingle || endCode == AMR_SGRCodeUnderlineDouble);
|
||||||
|
case AMR_SGRCodeNoneOrInvalid:
|
||||||
|
case AMR_SGRCodeItalicOn:
|
||||||
|
case AMR_SGRCodeUnderlineNone:
|
||||||
|
case AMR_SGRCodeIntensityFaint:
|
||||||
|
case AMR_SGRCodeAllReset:
|
||||||
|
case AMR_SGRCodeBgReset:
|
||||||
|
case AMR_SGRCodeFgReset:
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- (NSColor*) colorForSGRCode:(AMR_SGRCode)code
|
||||||
|
{
|
||||||
|
if (self.ansiColors)
|
||||||
|
{
|
||||||
|
NSColor *preferredColor = self.ansiColors[@(code)];
|
||||||
|
if (preferredColor)
|
||||||
|
return preferredColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(code)
|
||||||
|
{
|
||||||
|
case AMR_SGRCodeFgBlack:
|
||||||
|
return kDefaultANSIColorFgBlack;
|
||||||
|
case AMR_SGRCodeFgRed:
|
||||||
|
return kDefaultANSIColorFgRed;
|
||||||
|
case AMR_SGRCodeFgGreen:
|
||||||
|
return kDefaultANSIColorFgGreen;
|
||||||
|
case AMR_SGRCodeFgYellow:
|
||||||
|
return kDefaultANSIColorFgYellow;
|
||||||
|
case AMR_SGRCodeFgBlue:
|
||||||
|
return kDefaultANSIColorFgBlue;
|
||||||
|
case AMR_SGRCodeFgMagenta:
|
||||||
|
return kDefaultANSIColorFgMagenta;
|
||||||
|
case AMR_SGRCodeFgCyan:
|
||||||
|
return kDefaultANSIColorFgCyan;
|
||||||
|
case AMR_SGRCodeFgWhite:
|
||||||
|
return kDefaultANSIColorFgWhite;
|
||||||
|
case AMR_SGRCodeFgBrightBlack:
|
||||||
|
return kDefaultANSIColorFgBrightBlack;
|
||||||
|
case AMR_SGRCodeFgBrightRed:
|
||||||
|
return kDefaultANSIColorFgBrightRed;
|
||||||
|
case AMR_SGRCodeFgBrightGreen:
|
||||||
|
return kDefaultANSIColorFgBrightGreen;
|
||||||
|
case AMR_SGRCodeFgBrightYellow:
|
||||||
|
return kDefaultANSIColorFgBrightYellow;
|
||||||
|
case AMR_SGRCodeFgBrightBlue:
|
||||||
|
return kDefaultANSIColorFgBrightBlue;
|
||||||
|
case AMR_SGRCodeFgBrightMagenta:
|
||||||
|
return kDefaultANSIColorFgBrightMagenta;
|
||||||
|
case AMR_SGRCodeFgBrightCyan:
|
||||||
|
return kDefaultANSIColorFgBrightCyan;
|
||||||
|
case AMR_SGRCodeFgBrightWhite:
|
||||||
|
return kDefaultANSIColorFgBrightWhite;
|
||||||
|
case AMR_SGRCodeBgBlack:
|
||||||
|
return kDefaultANSIColorBgBlack;
|
||||||
|
case AMR_SGRCodeBgRed:
|
||||||
|
return kDefaultANSIColorBgRed;
|
||||||
|
case AMR_SGRCodeBgGreen:
|
||||||
|
return kDefaultANSIColorBgGreen;
|
||||||
|
case AMR_SGRCodeBgYellow:
|
||||||
|
return kDefaultANSIColorBgYellow;
|
||||||
|
case AMR_SGRCodeBgBlue:
|
||||||
|
return kDefaultANSIColorBgBlue;
|
||||||
|
case AMR_SGRCodeBgMagenta:
|
||||||
|
return kDefaultANSIColorBgMagenta;
|
||||||
|
case AMR_SGRCodeBgCyan:
|
||||||
|
return kDefaultANSIColorBgCyan;
|
||||||
|
case AMR_SGRCodeBgWhite:
|
||||||
|
return kDefaultANSIColorBgWhite;
|
||||||
|
case AMR_SGRCodeBgBrightBlack:
|
||||||
|
return kDefaultANSIColorBgBrightBlack;
|
||||||
|
case AMR_SGRCodeBgBrightRed:
|
||||||
|
return kDefaultANSIColorBgBrightRed;
|
||||||
|
case AMR_SGRCodeBgBrightGreen:
|
||||||
|
return kDefaultANSIColorBgBrightGreen;
|
||||||
|
case AMR_SGRCodeBgBrightYellow:
|
||||||
|
return kDefaultANSIColorBgBrightYellow;
|
||||||
|
case AMR_SGRCodeBgBrightBlue:
|
||||||
|
return kDefaultANSIColorBgBrightBlue;
|
||||||
|
case AMR_SGRCodeBgBrightMagenta:
|
||||||
|
return kDefaultANSIColorBgBrightMagenta;
|
||||||
|
case AMR_SGRCodeBgBrightCyan:
|
||||||
|
return kDefaultANSIColorBgBrightCyan;
|
||||||
|
case AMR_SGRCodeBgBrightWhite:
|
||||||
|
return kDefaultANSIColorBgBrightWhite;
|
||||||
|
case AMR_SGRCodeNoneOrInvalid:
|
||||||
|
case AMR_SGRCodeItalicOn:
|
||||||
|
case AMR_SGRCodeUnderlineNone:
|
||||||
|
case AMR_SGRCodeIntensityFaint:
|
||||||
|
case AMR_SGRCodeAllReset:
|
||||||
|
case AMR_SGRCodeBgReset:
|
||||||
|
case AMR_SGRCodeFgReset:
|
||||||
|
case AMR_SGRCodeIntensityBold:
|
||||||
|
case AMR_SGRCodeIntensityNormal:
|
||||||
|
case AMR_SGRCodeUnderlineSingle:
|
||||||
|
case AMR_SGRCodeUnderlineDouble:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return kDefaultANSIColorFgBlack;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
- (AMR_SGRCode) AMR_SGRCodeForColor:(NSColor*)aColor isForegroundColor:(BOOL)aForeground
|
||||||
|
{
|
||||||
|
if (self.ansiColors)
|
||||||
|
{
|
||||||
|
NSArray *codesForGivenColor = [self.ansiColors allKeysForObject:aColor];
|
||||||
|
|
||||||
|
if (codesForGivenColor != nil && 0 < codesForGivenColor.count)
|
||||||
|
{
|
||||||
|
for (NSNumber *thisCode in codesForGivenColor)
|
||||||
|
{
|
||||||
|
BOOL thisIsForegroundColor = (thisCode.intValue < 40);
|
||||||
|
if (aForeground == thisIsForegroundColor)
|
||||||
|
return thisCode.intValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aForeground)
|
||||||
|
{
|
||||||
|
if ([aColor isEqual:kDefaultANSIColorFgBlack])
|
||||||
|
return AMR_SGRCodeFgBlack;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorFgRed])
|
||||||
|
return AMR_SGRCodeFgRed;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorFgGreen])
|
||||||
|
return AMR_SGRCodeFgGreen;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorFgYellow])
|
||||||
|
return AMR_SGRCodeFgYellow;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorFgBlue])
|
||||||
|
return AMR_SGRCodeFgBlue;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorFgMagenta])
|
||||||
|
return AMR_SGRCodeFgMagenta;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorFgCyan])
|
||||||
|
return AMR_SGRCodeFgCyan;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorFgWhite])
|
||||||
|
return AMR_SGRCodeFgWhite;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorFgBrightBlack])
|
||||||
|
return AMR_SGRCodeFgBrightBlack;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorFgBrightRed])
|
||||||
|
return AMR_SGRCodeFgBrightRed;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorFgBrightGreen])
|
||||||
|
return AMR_SGRCodeFgBrightGreen;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorFgBrightYellow])
|
||||||
|
return AMR_SGRCodeFgBrightYellow;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorFgBrightBlue])
|
||||||
|
return AMR_SGRCodeFgBrightBlue;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorFgBrightMagenta])
|
||||||
|
return AMR_SGRCodeFgBrightMagenta;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorFgBrightCyan])
|
||||||
|
return AMR_SGRCodeFgBrightCyan;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorFgBrightWhite])
|
||||||
|
return AMR_SGRCodeFgBrightWhite;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if ([aColor isEqual:kDefaultANSIColorBgBlack])
|
||||||
|
return AMR_SGRCodeBgBlack;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorBgRed])
|
||||||
|
return AMR_SGRCodeBgRed;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorBgGreen])
|
||||||
|
return AMR_SGRCodeBgGreen;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorBgYellow])
|
||||||
|
return AMR_SGRCodeBgYellow;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorBgBlue])
|
||||||
|
return AMR_SGRCodeBgBlue;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorBgMagenta])
|
||||||
|
return AMR_SGRCodeBgMagenta;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorBgCyan])
|
||||||
|
return AMR_SGRCodeBgCyan;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorBgWhite])
|
||||||
|
return AMR_SGRCodeBgWhite;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorBgBrightBlack])
|
||||||
|
return AMR_SGRCodeBgBrightBlack;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorBgBrightRed])
|
||||||
|
return AMR_SGRCodeBgBrightRed;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorBgBrightGreen])
|
||||||
|
return AMR_SGRCodeBgBrightGreen;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorBgBrightYellow])
|
||||||
|
return AMR_SGRCodeBgBrightYellow;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorBgBrightBlue])
|
||||||
|
return AMR_SGRCodeBgBrightBlue;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorBgBrightMagenta])
|
||||||
|
return AMR_SGRCodeBgBrightMagenta;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorBgBrightCyan])
|
||||||
|
return AMR_SGRCodeBgBrightCyan;
|
||||||
|
else if ([aColor isEqual:kDefaultANSIColorBgBrightWhite])
|
||||||
|
return AMR_SGRCodeBgBrightWhite;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AMR_SGRCodeNoneOrInvalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// helper struct typedef and a few functions for
|
||||||
|
// -closestSGRCodeForColor:isForegroundColor:
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
CGFloat hue;
|
||||||
|
CGFloat saturation;
|
||||||
|
CGFloat brightness;
|
||||||
|
} AMR_HSB;
|
||||||
|
|
||||||
|
AMR_HSB makeHSB(CGFloat hue, CGFloat saturation, CGFloat brightness)
|
||||||
|
{
|
||||||
|
AMR_HSB outHSB;
|
||||||
|
outHSB.hue = hue;
|
||||||
|
outHSB.saturation = saturation;
|
||||||
|
outHSB.brightness = brightness;
|
||||||
|
return outHSB;
|
||||||
|
}
|
||||||
|
|
||||||
|
AMR_HSB getHSBFromColor(NSColor *color)
|
||||||
|
{
|
||||||
|
CGFloat hue = 0.0;
|
||||||
|
CGFloat saturation = 0.0;
|
||||||
|
CGFloat brightness = 0.0;
|
||||||
|
[[color colorUsingColorSpaceName:NSCalibratedRGBColorSpace]
|
||||||
|
getHue:&hue
|
||||||
|
saturation:&saturation
|
||||||
|
brightness:&brightness
|
||||||
|
alpha:NULL
|
||||||
|
];
|
||||||
|
return makeHSB(hue, saturation, brightness);
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL floatsEqual(CGFloat first, CGFloat second, CGFloat maxAbsError)
|
||||||
|
{
|
||||||
|
return (fabs(first-second)) < maxAbsError;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define MAX_HUE_FLOAT_EQUALITY_ABS_ERROR 0.000001
|
||||||
|
|
||||||
|
- (AMR_SGRCode) closestSGRCodeForColor:(NSColor *)color isForegroundColor:(BOOL)foreground
|
||||||
|
{
|
||||||
|
if (color == nil)
|
||||||
|
return AMR_SGRCodeNoneOrInvalid;
|
||||||
|
|
||||||
|
AMR_SGRCode closestColorSGRCode = [self AMR_SGRCodeForColor:color isForegroundColor:foreground];
|
||||||
|
if (closestColorSGRCode != AMR_SGRCodeNoneOrInvalid)
|
||||||
|
return closestColorSGRCode;
|
||||||
|
|
||||||
|
AMR_HSB givenColorHSB = getHSBFromColor(color);
|
||||||
|
|
||||||
|
CGFloat closestColorHueDiff = FLT_MAX;
|
||||||
|
CGFloat closestColorSaturationDiff = FLT_MAX;
|
||||||
|
CGFloat closestColorBrightnessDiff = FLT_MAX;
|
||||||
|
|
||||||
|
// (background SGR codes are +10 from foreground ones:)
|
||||||
|
NSUInteger AMR_SGRCodeShift = (foreground)?0:10;
|
||||||
|
NSArray *ansiFgColorCodes = @[
|
||||||
|
@(AMR_SGRCodeFgBlack+AMR_SGRCodeShift),
|
||||||
|
@(AMR_SGRCodeFgRed+AMR_SGRCodeShift),
|
||||||
|
@(AMR_SGRCodeFgGreen+AMR_SGRCodeShift),
|
||||||
|
@(AMR_SGRCodeFgYellow+AMR_SGRCodeShift),
|
||||||
|
@(AMR_SGRCodeFgBlue+AMR_SGRCodeShift),
|
||||||
|
@(AMR_SGRCodeFgMagenta+AMR_SGRCodeShift),
|
||||||
|
@(AMR_SGRCodeFgCyan+AMR_SGRCodeShift),
|
||||||
|
@(AMR_SGRCodeFgWhite+AMR_SGRCodeShift),
|
||||||
|
@(AMR_SGRCodeFgBrightBlack+AMR_SGRCodeShift),
|
||||||
|
@(AMR_SGRCodeFgBrightRed+AMR_SGRCodeShift),
|
||||||
|
@(AMR_SGRCodeFgBrightGreen+AMR_SGRCodeShift),
|
||||||
|
@(AMR_SGRCodeFgBrightYellow+AMR_SGRCodeShift),
|
||||||
|
@(AMR_SGRCodeFgBrightBlue+AMR_SGRCodeShift),
|
||||||
|
@(AMR_SGRCodeFgBrightMagenta+AMR_SGRCodeShift),
|
||||||
|
@(AMR_SGRCodeFgBrightCyan+AMR_SGRCodeShift),
|
||||||
|
@(AMR_SGRCodeFgBrightWhite+AMR_SGRCodeShift),
|
||||||
|
];
|
||||||
|
for (NSNumber *thisSGRCodeNumber in ansiFgColorCodes)
|
||||||
|
{
|
||||||
|
AMR_SGRCode thisSGRCode = thisSGRCodeNumber.intValue;
|
||||||
|
NSColor *thisColor = [self colorForSGRCode:thisSGRCode];
|
||||||
|
|
||||||
|
AMR_HSB thisColorHSB = getHSBFromColor(thisColor);
|
||||||
|
|
||||||
|
CGFloat hueDiff = fabs(givenColorHSB.hue - thisColorHSB.hue);
|
||||||
|
CGFloat saturationDiff = fabs(givenColorHSB.saturation - thisColorHSB.saturation);
|
||||||
|
CGFloat brightnessDiff = fabs(givenColorHSB.brightness - thisColorHSB.brightness);
|
||||||
|
|
||||||
|
// comparison depends on hue, saturation and brightness
|
||||||
|
// (strictly in that order):
|
||||||
|
|
||||||
|
if (!floatsEqual(hueDiff, closestColorHueDiff, MAX_HUE_FLOAT_EQUALITY_ABS_ERROR))
|
||||||
|
{
|
||||||
|
if (hueDiff > closestColorHueDiff)
|
||||||
|
continue;
|
||||||
|
closestColorSGRCode = thisSGRCode;
|
||||||
|
closestColorHueDiff = hueDiff;
|
||||||
|
closestColorSaturationDiff = saturationDiff;
|
||||||
|
closestColorBrightnessDiff = brightnessDiff;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!floatsEqual(saturationDiff, closestColorSaturationDiff, MAX_HUE_FLOAT_EQUALITY_ABS_ERROR))
|
||||||
|
{
|
||||||
|
if (saturationDiff > closestColorSaturationDiff)
|
||||||
|
continue;
|
||||||
|
closestColorSGRCode = thisSGRCode;
|
||||||
|
closestColorHueDiff = hueDiff;
|
||||||
|
closestColorSaturationDiff = saturationDiff;
|
||||||
|
closestColorBrightnessDiff = brightnessDiff;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!floatsEqual(brightnessDiff, closestColorBrightnessDiff, MAX_HUE_FLOAT_EQUALITY_ABS_ERROR))
|
||||||
|
{
|
||||||
|
if (brightnessDiff > closestColorBrightnessDiff)
|
||||||
|
continue;
|
||||||
|
closestColorSGRCode = thisSGRCode;
|
||||||
|
closestColorHueDiff = hueDiff;
|
||||||
|
closestColorSaturationDiff = saturationDiff;
|
||||||
|
closestColorBrightnessDiff = brightnessDiff;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If hue (especially hue!), saturation and brightness diffs all
|
||||||
|
// are equal to some other color, we need to prefer one or the
|
||||||
|
// other so we'll select the more 'distinctive' color of the
|
||||||
|
// two (this is *very* subjective, obviously). I basically just
|
||||||
|
// looked at the hue chart, went through all the points between
|
||||||
|
// our main ANSI colors and decided which side the middle point
|
||||||
|
// would lean on. (e.g. the purple color that is exactly between
|
||||||
|
// the blue and magenta ANSI colors looks more magenta than
|
||||||
|
// blue to me so I put magenta higher than blue in the list
|
||||||
|
// below.)
|
||||||
|
//
|
||||||
|
// subjective ordering of colors from most to least 'distinctive':
|
||||||
|
long colorDistinctivenessOrder[6] = {
|
||||||
|
AMR_SGRCodeFgRed+AMR_SGRCodeShift,
|
||||||
|
AMR_SGRCodeFgMagenta+AMR_SGRCodeShift,
|
||||||
|
AMR_SGRCodeFgBlue+AMR_SGRCodeShift,
|
||||||
|
AMR_SGRCodeFgGreen+AMR_SGRCodeShift,
|
||||||
|
AMR_SGRCodeFgCyan+AMR_SGRCodeShift,
|
||||||
|
AMR_SGRCodeFgYellow+AMR_SGRCodeShift
|
||||||
|
};
|
||||||
|
for (int i = 0; i < 6; i++)
|
||||||
|
{
|
||||||
|
if (colorDistinctivenessOrder[i] == closestColorSGRCode)
|
||||||
|
break;
|
||||||
|
else if (colorDistinctivenessOrder[i] == thisSGRCode)
|
||||||
|
{
|
||||||
|
closestColorSGRCode = thisSGRCode;
|
||||||
|
closestColorHueDiff = hueDiff;
|
||||||
|
closestColorSaturationDiff = saturationDiff;
|
||||||
|
closestColorBrightnessDiff = brightnessDiff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return closestColorSGRCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@end
|
||||||
@ -6,6 +6,7 @@
|
|||||||
// Copyright © 2018 Anton Palgunov. All rights reserved.
|
// Copyright © 2018 Anton Palgunov. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
#import "AMR_ANSIEscapeHelper.h"
|
||||||
#import "TouchBarPrivateApi.h"
|
#import "TouchBarPrivateApi.h"
|
||||||
#import "TouchBarSupport.h"
|
#import "TouchBarSupport.h"
|
||||||
#import "DeprecatedCarbonAPI.h"
|
#import "DeprecatedCarbonAPI.h"
|
||||||
|
|||||||
191
MTMR/CPU.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,13 +8,32 @@
|
|||||||
|
|
||||||
import Cocoa
|
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 {
|
class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegate {
|
||||||
var tapClosure: (() -> Void)?
|
|
||||||
var longTapClosure: (() -> Void)?
|
var actions: [ItemAction] = [] {
|
||||||
|
didSet {
|
||||||
|
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 button: NSButton!
|
||||||
private var singleClick: HapticClickGestureRecognizer!
|
private var longClick: LongPressGestureRecognizer!
|
||||||
private var longClick: NSPressGestureRecognizer!
|
private var multiClick: MultiClickGestureRecognizer!
|
||||||
|
|
||||||
init(identifier: NSTouchBarItem.Identifier, title: String) {
|
init(identifier: NSTouchBarItem.Identifier, title: String) {
|
||||||
attributedTitle = title.defaultTouchbarAttributedString
|
attributedTitle = title.defaultTouchbarAttributedString
|
||||||
@ -22,13 +41,21 @@ class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegat
|
|||||||
super.init(identifier: identifier)
|
super.init(identifier: identifier)
|
||||||
button = CustomHeightButton(title: title, target: nil, action: nil)
|
button = CustomHeightButton(title: title, target: nil, action: nil)
|
||||||
|
|
||||||
longClick = NSPressGestureRecognizer(target: self, action: #selector(handleGestureLong))
|
longClick = LongPressGestureRecognizer(target: self, action: #selector(handleGestureLong))
|
||||||
|
longClick.isEnabled = false
|
||||||
longClick.allowedTouchTypes = .direct
|
longClick.allowedTouchTypes = .direct
|
||||||
longClick.delegate = self
|
longClick.delegate = self
|
||||||
|
|
||||||
singleClick = HapticClickGestureRecognizer(target: self, action: #selector(handleGestureSingle))
|
multiClick = MultiClickGestureRecognizer(
|
||||||
singleClick.allowedTouchTypes = .direct
|
target: self,
|
||||||
singleClick.delegate = self
|
action: #selector(handleGestureSingleTap),
|
||||||
|
doubleAction: #selector(handleGestureDoubleTap),
|
||||||
|
tripleAction: #selector(handleGestureTripleTap)
|
||||||
|
)
|
||||||
|
multiClick.allowedTouchTypes = .direct
|
||||||
|
multiClick.delegate = self
|
||||||
|
multiClick.isDoubleClickEnabled = false
|
||||||
|
multiClick.isTripleClickEnabled = false
|
||||||
|
|
||||||
reinstallButton()
|
reinstallButton()
|
||||||
button.attributedTitle = attributedTitle
|
button.attributedTitle = attributedTitle
|
||||||
@ -80,6 +107,7 @@ class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegat
|
|||||||
if let color = backgroundColor {
|
if let color = backgroundColor {
|
||||||
cell.isBordered = true
|
cell.isBordered = true
|
||||||
button.bezelColor = color
|
button.bezelColor = color
|
||||||
|
button.bezelStyle = .rounded
|
||||||
cell.backgroundColor = color
|
cell.backgroundColor = color
|
||||||
} else {
|
} else {
|
||||||
button.isBordered = isBordered
|
button.isBordered = isBordered
|
||||||
@ -93,37 +121,43 @@ class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegat
|
|||||||
view = button
|
view = button
|
||||||
|
|
||||||
view.addGestureRecognizer(longClick)
|
view.addGestureRecognizer(longClick)
|
||||||
view.addGestureRecognizer(singleClick)
|
// view.addGestureRecognizer(singleClick)
|
||||||
|
view.addGestureRecognizer(multiClick)
|
||||||
|
finishViewConfiguration()
|
||||||
}
|
}
|
||||||
|
|
||||||
func gestureRecognizer(_ gestureRecognizer: NSGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: NSGestureRecognizer) -> Bool {
|
func gestureRecognizer(_ gestureRecognizer: NSGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: NSGestureRecognizer) -> Bool {
|
||||||
if gestureRecognizer == singleClick && otherGestureRecognizer == longClick {
|
if gestureRecognizer == multiClick && otherGestureRecognizer == longClick
|
||||||
|
|| gestureRecognizer == longClick && otherGestureRecognizer == multiClick // need it
|
||||||
|
{
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func handleGestureSingle(gr: NSClickGestureRecognizer) {
|
func callActions(for trigger: Action.Trigger) {
|
||||||
switch gr.state {
|
let itemActions = self.actions.filter { $0.trigger == trigger }
|
||||||
case .ended:
|
for itemAction in itemActions {
|
||||||
tapClosure?()
|
itemAction.closure?()
|
||||||
break
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func handleGestureSingleTap() {
|
||||||
|
callActions(for: .singleTap)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func handleGestureDoubleTap() {
|
||||||
|
callActions(for: .doubleTap)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func handleGestureTripleTap() {
|
||||||
|
callActions(for: .tripleTap)
|
||||||
|
}
|
||||||
|
|
||||||
@objc func handleGestureLong(gr: NSPressGestureRecognizer) {
|
@objc func handleGestureLong(gr: NSPressGestureRecognizer) {
|
||||||
switch gr.state {
|
switch gr.state {
|
||||||
case .began:
|
case .possible: // tiny hack because we're calling action manually
|
||||||
if let closure = self.longTapClosure {
|
callActions(for: .longTap)
|
||||||
HapticFeedback.shared.tap(strong: 2)
|
|
||||||
closure()
|
|
||||||
} else if let closure = self.tapClosure {
|
|
||||||
HapticFeedback.shared.tap(strong: 6)
|
|
||||||
closure()
|
|
||||||
print("long click")
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
@ -157,6 +191,10 @@ class CustomButtonCell: NSButtonCell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func drawingRect(forBounds rect: NSRect) -> NSRect {
|
||||||
|
return rect // need that so content may better fit in button with very limited width
|
||||||
|
}
|
||||||
|
|
||||||
required init(coder _: NSCoder) {
|
required init(coder _: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
@ -169,16 +207,128 @@ 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) {
|
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.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LongPressGestureRecognizer: NSPressGestureRecognizer {
|
||||||
|
var recognizeTimeout = 0.4
|
||||||
|
private var timer: Timer?
|
||||||
|
|
||||||
|
override func touchesBegan(with event: NSEvent) {
|
||||||
|
timerInvalidate()
|
||||||
|
|
||||||
|
let touches = event.touches(for: self.view!)
|
||||||
|
if touches.count == 1 { // to prevent it for built-in two/three-finger gestures
|
||||||
|
timer = Timer.scheduledTimer(timeInterval: recognizeTimeout, target: self, selector: #selector(self.onTimer), userInfo: nil, repeats: false)
|
||||||
|
}
|
||||||
|
|
||||||
super.touchesBegan(with: event)
|
super.touchesBegan(with: event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func touchesMoved(with event: NSEvent) {
|
||||||
|
timerInvalidate() // to prevent it for built-in two/three-finger gestures
|
||||||
|
super.touchesMoved(with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesCancelled(with event: NSEvent) {
|
||||||
|
timerInvalidate()
|
||||||
|
super.touchesCancelled(with: event)
|
||||||
|
}
|
||||||
|
|
||||||
override func touchesEnded(with event: NSEvent) {
|
override func touchesEnded(with event: NSEvent) {
|
||||||
HapticFeedback.shared.tap(strong: 1)
|
timerInvalidate()
|
||||||
super.touchesEnded(with: event)
|
super.touchesEnded(with: event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func timerInvalidate() {
|
||||||
|
if let timer = timer {
|
||||||
|
timer.invalidate()
|
||||||
|
self.timer = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func onTimer() {
|
||||||
|
if let target = self.target, let action = self.action {
|
||||||
|
target.performSelector(onMainThread: action, with: self, waitUntilDone: false)
|
||||||
|
HapticFeedback.instance.tap(type: .strong)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
timerInvalidate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
|
|||||||
@ -9,81 +9,92 @@
|
|||||||
import IOKit
|
import IOKit
|
||||||
|
|
||||||
class HapticFeedback {
|
class HapticFeedback {
|
||||||
static let shared = HapticFeedback()
|
|
||||||
|
|
||||||
// Here we have list of possible IDs for Haptic Generator Device. They are not constant
|
// 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
|
// 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
|
// 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 :(
|
// There should be programmatic way to get it but I can't find, no docs for macOS :(
|
||||||
private let possibleDeviceIDs: [UInt64] = [
|
private let possibleDeviceIDs: [UInt64] = [
|
||||||
0x200_0000_0100_0000, // MacBook Pro 2016/2017
|
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?
|
|
||||||
private var actuatorRef: CFTypeRef?
|
// 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 {
|
||||||
init() {
|
case back = 1
|
||||||
recreateDevice()
|
case click = 2
|
||||||
|
case weak = 3
|
||||||
|
case medium = 4
|
||||||
|
case weakMedium = 5
|
||||||
|
case strong = 6
|
||||||
|
case reserved1 = 15
|
||||||
|
case reserved2 = 16
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't know how to do strong is enum one of
|
private var actuatorRef: CFTypeRef?
|
||||||
// 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`
|
|
||||||
|
|
||||||
func tap(strong: Int32) {
|
static var instance = HapticFeedback()
|
||||||
guard correctDeviceID != nil, actuatorRef != nil else {
|
|
||||||
|
// MARK: - Init
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
self.recreateDevice()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func recreateDevice() {
|
||||||
|
if let actuatorRef = self.actuatorRef {
|
||||||
|
MTActuatorClose(actuatorRef)
|
||||||
|
self.actuatorRef = nil // just in case %)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard self.actuatorRef == nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let's find our Haptic device
|
||||||
|
self.possibleDeviceIDs.forEach {(deviceID) in
|
||||||
|
let actuatorRef = MTActuatorCreateFromDeviceID(deviceID).takeRetainedValue()
|
||||||
|
|
||||||
|
if actuatorRef != nil {
|
||||||
|
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?)")
|
print("guard actuatorRef == nil (no haptic device found?)")
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var result: IOReturn
|
guard MTActuatorOpen(actuatorRef) == kIOReturnSuccess else {
|
||||||
|
|
||||||
result = MTActuatorOpen(actuatorRef!)
|
|
||||||
guard result == kIOReturnSuccess else {
|
|
||||||
print("guard MTActuatorOpen")
|
print("guard MTActuatorOpen")
|
||||||
recreateDevice()
|
self.recreateDevice()
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
result = MTActuatorActuate(actuatorRef!, strong, 0, 0, 0)
|
return actuatorRef
|
||||||
guard result == kIOReturnSuccess else {
|
}
|
||||||
|
|
||||||
|
func tap(type: HapticType) {
|
||||||
|
guard let actuator = getActuatorIfPosible() else { return }
|
||||||
|
|
||||||
|
guard MTActuatorActuate(actuator, type.rawValue, 0, 0, 0) == kIOReturnSuccess else {
|
||||||
print("guard MTActuatorActuate")
|
print("guard MTActuatorActuate")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result = MTActuatorClose(actuatorRef!)
|
guard MTActuatorClose(actuator) == kIOReturnSuccess else {
|
||||||
guard result == kIOReturnSuccess else {
|
|
||||||
print("guard MTActuatorClose")
|
print("guard MTActuatorClose")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func recreateDevice() {
|
|
||||||
if let actuatorRef = actuatorRef {
|
|
||||||
MTActuatorClose(actuatorRef)
|
|
||||||
self.actuatorRef = nil // just in case %)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let correctDeviceID = correctDeviceID {
|
|
||||||
actuatorRef = MTActuatorCreateFromDeviceID(correctDeviceID).takeRetainedValue()
|
|
||||||
} else {
|
|
||||||
// Let's find our Haptic device
|
|
||||||
possibleDeviceIDs.forEach {(deviceID) in
|
|
||||||
guard correctDeviceID == nil else {return}
|
|
||||||
actuatorRef = MTActuatorCreateFromDeviceID(deviceID).takeRetainedValue()
|
|
||||||
|
|
||||||
if actuatorRef != nil {
|
|
||||||
correctDeviceID = deviceID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,9 +17,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.22</string>
|
<string>0.27</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>270</string>
|
<string>448</string>
|
||||||
<key>LSApplicationCategoryType</key>
|
<key>LSApplicationCategoryType</key>
|
||||||
<string>public.app-category.utilities</string>
|
<string>public.app-category.utilities</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
@ -39,7 +39,7 @@
|
|||||||
<key>NSHomeKitUsageDescription</key>
|
<key>NSHomeKitUsageDescription</key>
|
||||||
<string>MTMR needs access to HomeKit for work</string>
|
<string>MTMR needs access to HomeKit for work</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<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>
|
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||||
<string>Weather widget need your location for correct work</string>
|
<string>Weather widget need your location for correct work</string>
|
||||||
<key>NSLocationAlwaysUsageDescription</key>
|
<key>NSLocationAlwaysUsageDescription</key>
|
||||||
|
|||||||
@ -3,47 +3,52 @@ import Foundation
|
|||||||
|
|
||||||
extension Data {
|
extension Data {
|
||||||
func barItemDefinitions() -> [BarItemDefinition]? {
|
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 {
|
struct BarItemDefinition: Decodable {
|
||||||
let type: ItemType
|
let type: ItemType
|
||||||
let action: ActionType
|
let actions: [Action]
|
||||||
let longAction: LongActionType
|
let legacyAction: LegacyActionType
|
||||||
|
let legacyLongAction: LegacyLongActionType
|
||||||
let additionalParameters: [GeneralParameters.CodingKeys: GeneralParameter]
|
let additionalParameters: [GeneralParameters.CodingKeys: GeneralParameter]
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case type
|
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.type = type
|
||||||
self.action = action
|
self.actions = actions
|
||||||
self.longAction = longAction
|
self.legacyAction = action
|
||||||
|
self.legacyLongAction = legacyLongAction
|
||||||
self.additionalParameters = additionalParameters
|
self.additionalParameters = additionalParameters
|
||||||
}
|
}
|
||||||
|
|
||||||
init(from decoder: Decoder) throws {
|
init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
let type = try container.decode(String.self, forKey: .type)
|
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
|
var additionalParameters = try GeneralParameters(from: decoder).parameters
|
||||||
|
|
||||||
if let result = try? parametersDecoder(decoder),
|
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 }
|
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 {
|
} 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 -> (
|
typealias ParametersDecoder = (Decoder) throws -> (
|
||||||
item: ItemType,
|
item: ItemType,
|
||||||
action: ActionType,
|
actions: [Action],
|
||||||
longAction: LongActionType,
|
legacyAction: LegacyActionType,
|
||||||
|
legacyLongAction: LegacyLongActionType,
|
||||||
parameters: [GeneralParameters.CodingKeys: GeneralParameter]
|
parameters: [GeneralParameters.CodingKeys: GeneralParameter]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -51,15 +56,21 @@ class SupportedTypesHolder {
|
|||||||
private var supportedTypes: [String: ParametersDecoder] = [
|
private var supportedTypes: [String: ParametersDecoder] = [
|
||||||
"escape": { _ in (
|
"escape": { _ in (
|
||||||
item: .staticButton(title: "esc"),
|
item: .staticButton(title: "esc"),
|
||||||
action: .keyPress(keycode: 53),
|
actions: [
|
||||||
longAction: .none,
|
Action(trigger: .singleTap, value: .keyPress(keycode: 53))
|
||||||
|
],
|
||||||
|
legacyAction: .none,
|
||||||
|
legacyLongAction: .none,
|
||||||
parameters: [.align: .align(.left)]
|
parameters: [.align: .align(.left)]
|
||||||
) },
|
) },
|
||||||
|
|
||||||
"delete": { _ in (
|
"delete": { _ in (
|
||||||
item: .staticButton(title: "del"),
|
item: .staticButton(title: "del"),
|
||||||
action: .keyPress(keycode: 117),
|
actions: [
|
||||||
longAction: .none,
|
Action(trigger: .singleTap, value: .keyPress(keycode: 117))
|
||||||
|
],
|
||||||
|
legacyAction: .none,
|
||||||
|
legacyLongAction: .none,
|
||||||
parameters: [:]
|
parameters: [:]
|
||||||
) },
|
) },
|
||||||
|
|
||||||
@ -67,8 +78,11 @@ class SupportedTypesHolder {
|
|||||||
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "brightnessUp"))
|
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "brightnessUp"))
|
||||||
return (
|
return (
|
||||||
item: .staticButton(title: ""),
|
item: .staticButton(title: ""),
|
||||||
action: .keyPress(keycode: 144),
|
actions: [
|
||||||
longAction: .none,
|
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_BRIGHTNESS_UP))
|
||||||
|
],
|
||||||
|
legacyAction: .none,
|
||||||
|
legacyLongAction: .none,
|
||||||
parameters: [.image: imageParameter]
|
parameters: [.image: imageParameter]
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -77,8 +91,11 @@ class SupportedTypesHolder {
|
|||||||
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "brightnessDown"))
|
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "brightnessDown"))
|
||||||
return (
|
return (
|
||||||
item: .staticButton(title: ""),
|
item: .staticButton(title: ""),
|
||||||
action: .keyPress(keycode: 145),
|
actions: [
|
||||||
longAction: .none,
|
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_BRIGHTNESS_DOWN))
|
||||||
|
],
|
||||||
|
legacyAction: .none,
|
||||||
|
legacyLongAction: .none,
|
||||||
parameters: [.image: imageParameter]
|
parameters: [.image: imageParameter]
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -87,8 +104,11 @@ class SupportedTypesHolder {
|
|||||||
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "ill_up"))
|
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "ill_up"))
|
||||||
return (
|
return (
|
||||||
item: .staticButton(title: ""),
|
item: .staticButton(title: ""),
|
||||||
action: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_UP),
|
actions: [
|
||||||
longAction: .none,
|
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_UP))
|
||||||
|
],
|
||||||
|
legacyAction: .none,
|
||||||
|
legacyLongAction: .none,
|
||||||
parameters: [.image: imageParameter]
|
parameters: [.image: imageParameter]
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -97,8 +117,11 @@ class SupportedTypesHolder {
|
|||||||
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "ill_down"))
|
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "ill_down"))
|
||||||
return (
|
return (
|
||||||
item: .staticButton(title: ""),
|
item: .staticButton(title: ""),
|
||||||
action: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_DOWN),
|
actions: [
|
||||||
longAction: .none,
|
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_DOWN))
|
||||||
|
],
|
||||||
|
legacyAction: .none,
|
||||||
|
legacyLongAction: .none,
|
||||||
parameters: [.image: imageParameter]
|
parameters: [.image: imageParameter]
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -107,8 +130,11 @@ class SupportedTypesHolder {
|
|||||||
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarVolumeDownTemplateName)!)
|
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarVolumeDownTemplateName)!)
|
||||||
return (
|
return (
|
||||||
item: .staticButton(title: ""),
|
item: .staticButton(title: ""),
|
||||||
action: .hidKey(keycode: NX_KEYTYPE_SOUND_DOWN),
|
actions: [
|
||||||
longAction: .none,
|
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_SOUND_DOWN))
|
||||||
|
],
|
||||||
|
legacyAction: .none,
|
||||||
|
legacyLongAction: .none,
|
||||||
parameters: [.image: imageParameter]
|
parameters: [.image: imageParameter]
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -117,8 +143,11 @@ class SupportedTypesHolder {
|
|||||||
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarVolumeUpTemplateName)!)
|
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarVolumeUpTemplateName)!)
|
||||||
return (
|
return (
|
||||||
item: .staticButton(title: ""),
|
item: .staticButton(title: ""),
|
||||||
action: .hidKey(keycode: NX_KEYTYPE_SOUND_UP),
|
actions: [
|
||||||
longAction: .none,
|
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_SOUND_UP))
|
||||||
|
],
|
||||||
|
legacyAction: .none,
|
||||||
|
legacyLongAction: .none,
|
||||||
parameters: [.image: imageParameter]
|
parameters: [.image: imageParameter]
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -127,8 +156,11 @@ class SupportedTypesHolder {
|
|||||||
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarAudioOutputMuteTemplateName)!)
|
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarAudioOutputMuteTemplateName)!)
|
||||||
return (
|
return (
|
||||||
item: .staticButton(title: ""),
|
item: .staticButton(title: ""),
|
||||||
action: .hidKey(keycode: NX_KEYTYPE_MUTE),
|
actions: [
|
||||||
longAction: .none,
|
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_MUTE))
|
||||||
|
],
|
||||||
|
legacyAction: .none,
|
||||||
|
legacyLongAction: .none,
|
||||||
parameters: [.image: imageParameter]
|
parameters: [.image: imageParameter]
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -137,8 +169,11 @@ class SupportedTypesHolder {
|
|||||||
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarRewindTemplateName)!)
|
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarRewindTemplateName)!)
|
||||||
return (
|
return (
|
||||||
item: .staticButton(title: ""),
|
item: .staticButton(title: ""),
|
||||||
action: .hidKey(keycode: NX_KEYTYPE_PREVIOUS),
|
actions: [
|
||||||
longAction: .none,
|
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_PREVIOUS))
|
||||||
|
],
|
||||||
|
legacyAction: .none,
|
||||||
|
legacyLongAction: .none,
|
||||||
parameters: [.image: imageParameter]
|
parameters: [.image: imageParameter]
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -147,8 +182,11 @@ class SupportedTypesHolder {
|
|||||||
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarPlayPauseTemplateName)!)
|
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarPlayPauseTemplateName)!)
|
||||||
return (
|
return (
|
||||||
item: .staticButton(title: ""),
|
item: .staticButton(title: ""),
|
||||||
action: .hidKey(keycode: NX_KEYTYPE_PLAY),
|
actions: [
|
||||||
longAction: .none,
|
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_PLAY))
|
||||||
|
],
|
||||||
|
legacyAction: .none,
|
||||||
|
legacyLongAction: .none,
|
||||||
parameters: [.image: imageParameter]
|
parameters: [.image: imageParameter]
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@ -157,169 +195,45 @@ class SupportedTypesHolder {
|
|||||||
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarFastForwardTemplateName)!)
|
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarFastForwardTemplateName)!)
|
||||||
return (
|
return (
|
||||||
item: .staticButton(title: ""),
|
item: .staticButton(title: ""),
|
||||||
action: .hidKey(keycode: NX_KEYTYPE_NEXT),
|
actions: [
|
||||||
longAction: .none,
|
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_NEXT))
|
||||||
|
],
|
||||||
|
legacyAction: .none,
|
||||||
|
legacyLongAction: .none,
|
||||||
parameters: [.image: imageParameter]
|
parameters: [.image: imageParameter]
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
"weather": { decoder in
|
|
||||||
enum CodingKeys: String, CodingKey { case refreshInterval; case units; case api_key; case icon_type }
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval)
|
|
||||||
let units = try container.decodeIfPresent(String.self, forKey: .units)
|
|
||||||
let api_key = try container.decodeIfPresent(String.self, forKey: .api_key)
|
|
||||||
let icon_type = try container.decodeIfPresent(String.self, forKey: .icon_type)
|
|
||||||
let action = try ActionType(from: decoder)
|
|
||||||
let longAction = try LongActionType(from: decoder)
|
|
||||||
return (
|
|
||||||
item: .weather(interval: interval ?? 1800.00, units: units ?? "metric", api_key: api_key ?? "32c4256d09a4c52b38aecddba7a078f6", icon_type: icon_type ?? "text"),
|
|
||||||
action,
|
|
||||||
longAction,
|
|
||||||
parameters: [:]
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
"yandexWeather": { decoder in
|
|
||||||
enum CodingKeys: String, CodingKey { case refreshInterval }
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval)
|
|
||||||
let action = try ActionType(from: decoder)
|
|
||||||
let longAction = try LongActionType(from: decoder)
|
|
||||||
return (
|
|
||||||
item: .yandexWeather(interval: interval ?? 1800.00),
|
|
||||||
action,
|
|
||||||
longAction,
|
|
||||||
parameters: [:]
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
"currency": { decoder in
|
|
||||||
enum CodingKeys: String, CodingKey { case refreshInterval; case from; case to; case full }
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval)
|
|
||||||
let from = try container.decodeIfPresent(String.self, forKey: .from)
|
|
||||||
let to = try container.decodeIfPresent(String.self, forKey: .to)
|
|
||||||
let full = try container.decodeIfPresent(Bool.self, forKey: .full)
|
|
||||||
let action = try ActionType(from: decoder)
|
|
||||||
let longAction = try LongActionType(from: decoder)
|
|
||||||
return (
|
|
||||||
item: .currency(interval: interval ?? 600.00, from: from ?? "RUB", to: to ?? "USD", full: full ?? false),
|
|
||||||
action,
|
|
||||||
longAction,
|
|
||||||
parameters: [:]
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
"dock": { decoder in
|
|
||||||
enum CodingKeys: String, CodingKey { case autoResize }
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
let autoResize = try container.decodeIfPresent(Bool.self, forKey: .autoResize) ?? false
|
|
||||||
return (
|
|
||||||
item: .dock(autoResize: autoResize),
|
|
||||||
action: .none,
|
|
||||||
longAction: .none,
|
|
||||||
parameters: [:]
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
"inputsource": { _ in
|
|
||||||
(
|
|
||||||
item: .inputsource(),
|
|
||||||
action: .none,
|
|
||||||
longAction: .none,
|
|
||||||
parameters: [:]
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
"volume": { decoder in
|
|
||||||
enum CodingKeys: String, CodingKey { case image }
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
if var img = try container.decodeIfPresent(Source.self, forKey: .image) {
|
|
||||||
return (
|
|
||||||
item: .volume(),
|
|
||||||
action: .none,
|
|
||||||
longAction: .none,
|
|
||||||
parameters: [.image: .image(source: img)]
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
item: .volume(),
|
|
||||||
action: .none,
|
|
||||||
longAction: .none,
|
|
||||||
parameters: [:]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"brightness": { decoder in
|
|
||||||
enum CodingKeys: String, CodingKey { case refreshInterval; case image }
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval)
|
|
||||||
if var img = try container.decodeIfPresent(Source.self, forKey: .image) {
|
|
||||||
return (
|
|
||||||
item: .brightness(refreshInterval: interval ?? 0.5),
|
|
||||||
action: .none,
|
|
||||||
longAction: .none,
|
|
||||||
parameters: [.image: .image(source: img)]
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
item: .brightness(refreshInterval: interval ?? 0.5),
|
|
||||||
action: .none,
|
|
||||||
longAction: .none,
|
|
||||||
parameters: [:]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"sleep": { _ in (
|
"sleep": { _ in (
|
||||||
item: .staticButton(title: "☕️"),
|
item: .staticButton(title: "☕️"),
|
||||||
action: .shellScript(executable: "/usr/bin/pmset", parameters: ["sleepnow"]),
|
actions: [
|
||||||
longAction: .none,
|
Action(trigger: .singleTap, value: .shellScript(executable: "/usr/bin/pmset", parameters: ["sleepnow"]))
|
||||||
|
],
|
||||||
|
legacyAction: .none,
|
||||||
|
legacyLongAction: .none,
|
||||||
parameters: [:]
|
parameters: [:]
|
||||||
) },
|
) },
|
||||||
|
|
||||||
"displaySleep": { _ in (
|
"displaySleep": { _ in (
|
||||||
item: .staticButton(title: "☕️"),
|
item: .staticButton(title: "☕️"),
|
||||||
action: .shellScript(executable: "/usr/bin/pmset", parameters: ["displaysleepnow"]),
|
actions: [
|
||||||
longAction: .none,
|
Action(trigger: .singleTap, value: .shellScript(executable: "/usr/bin/pmset", parameters: ["displaysleepnow"]))
|
||||||
|
],
|
||||||
|
legacyAction: .none,
|
||||||
|
legacyLongAction: .none,
|
||||||
parameters: [:]
|
parameters: [:]
|
||||||
) },
|
) },
|
||||||
|
|
||||||
"music": { decoder in
|
|
||||||
enum CodingKeys: String, CodingKey { case refreshInterval; case disableMarquee }
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval)
|
|
||||||
let disableMarquee = try container.decodeIfPresent(Bool.self, forKey: .disableMarquee)
|
|
||||||
return (
|
|
||||||
item: .music(interval: interval ?? 5.0, disableMarquee: disableMarquee ?? false),
|
|
||||||
action: .none,
|
|
||||||
longAction: .none,
|
|
||||||
parameters: [:]
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
"group": { decoder in
|
|
||||||
enum CodingKeys: CodingKey { case items }
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
let items = try container.decode([BarItemDefinition].self, forKey: .items)
|
|
||||||
return (
|
|
||||||
item: .groupBar(items: items),
|
|
||||||
action: .none,
|
|
||||||
longAction: .none,
|
|
||||||
parameters: [:]
|
|
||||||
)
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
static let sharedInstance = SupportedTypesHolder()
|
static let sharedInstance = SupportedTypesHolder()
|
||||||
|
|
||||||
func lookup(by type: String) -> ParametersDecoder {
|
func lookup(by type: String, actions: [Action]) -> ParametersDecoder {
|
||||||
return supportedTypes[type] ?? { decoder in (
|
return supportedTypes[type] ?? { decoder in (
|
||||||
item: try ItemType(from: decoder),
|
item: try ItemType(from: decoder),
|
||||||
action: try ActionType(from: decoder),
|
actions: actions,
|
||||||
longAction: try LongActionType(from: decoder),
|
legacyAction: try LegacyActionType(from: decoder),
|
||||||
|
legacyLongAction: try LegacyLongActionType(from: decoder),
|
||||||
parameters: [:]
|
parameters: [:]
|
||||||
) }
|
) }
|
||||||
}
|
}
|
||||||
@ -328,12 +242,13 @@ class SupportedTypesHolder {
|
|||||||
supportedTypes[typename] = decoder
|
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
|
register(typename: typename) { _ in
|
||||||
(
|
(
|
||||||
item: item,
|
item: item,
|
||||||
action,
|
actions,
|
||||||
longAction,
|
legacyAction,
|
||||||
|
legacyLongAction,
|
||||||
parameters: [:]
|
parameters: [:]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -342,23 +257,27 @@ class SupportedTypesHolder {
|
|||||||
|
|
||||||
enum ItemType: Decodable {
|
enum ItemType: Decodable {
|
||||||
case staticButton(title: String)
|
case staticButton(title: String)
|
||||||
case appleScriptTitledButton(source: SourceProtocol, refreshInterval: Double)
|
case appleScriptTitledButton(source: SourceProtocol, refreshInterval: Double, alternativeImages: [String: SourceProtocol])
|
||||||
case timeButton(formatTemplate: String, timeZone: String?)
|
case shellScriptTitledButton(source: SourceProtocol, refreshInterval: Double)
|
||||||
case battery()
|
case timeButton(formatTemplate: String, timeZone: String?, locale: String?)
|
||||||
case dock(autoResize: Bool)
|
case battery
|
||||||
case volume()
|
case cpu(refreshInterval: Double)
|
||||||
|
case dock(autoResize: Bool, filter: String?)
|
||||||
|
case volume
|
||||||
case brightness(refreshInterval: Double)
|
case brightness(refreshInterval: Double)
|
||||||
case weather(interval: Double, units: String, api_key: String, icon_type: String)
|
case weather(interval: Double, units: String, api_key: String, icon_type: String)
|
||||||
case yandexWeather(interval: Double)
|
case yandexWeather(interval: Double)
|
||||||
case currency(interval: Double, from: String, to: String, full: Bool)
|
case currency(interval: Double, from: String, to: String, full: Bool)
|
||||||
case inputsource()
|
case inputsource
|
||||||
case music(interval: Double, disableMarquee: Bool)
|
case music(interval: Double, disableMarquee: Bool)
|
||||||
case groupBar(items: [BarItemDefinition])
|
case group(items: [BarItemDefinition])
|
||||||
case nightShift()
|
case nightShift
|
||||||
case dnd()
|
case dnd
|
||||||
case pomodoro(workTime: Double, restTime: Double)
|
case pomodoro(workTime: Double, restTime: Double)
|
||||||
case network(flip: Bool)
|
case network(flip: Bool, units: String)
|
||||||
case darkMode()
|
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 {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case type
|
case type
|
||||||
@ -373,6 +292,7 @@ enum ItemType: Decodable {
|
|||||||
case api_key
|
case api_key
|
||||||
case icon_type
|
case icon_type
|
||||||
case formatTemplate
|
case formatTemplate
|
||||||
|
case locale
|
||||||
case image
|
case image
|
||||||
case url
|
case url
|
||||||
case longUrl
|
case longUrl
|
||||||
@ -381,14 +301,24 @@ enum ItemType: Decodable {
|
|||||||
case restTime
|
case restTime
|
||||||
case flip
|
case flip
|
||||||
case autoResize
|
case autoResize
|
||||||
|
case filter
|
||||||
case disableMarquee
|
case disableMarquee
|
||||||
|
case alternativeImages
|
||||||
|
case sourceApple
|
||||||
|
case sourceBash
|
||||||
|
case direction
|
||||||
|
case fingers
|
||||||
|
case minOffset
|
||||||
|
case maxToShow
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ItemTypeRaw: String, Decodable {
|
enum ItemTypeRaw: String, Decodable {
|
||||||
case staticButton
|
case staticButton
|
||||||
case appleScriptTitledButton
|
case appleScriptTitledButton
|
||||||
|
case shellScriptTitledButton
|
||||||
case timeButton
|
case timeButton
|
||||||
case battery
|
case battery
|
||||||
|
case cpu
|
||||||
case dock
|
case dock
|
||||||
case volume
|
case volume
|
||||||
case brightness
|
case brightness
|
||||||
@ -397,12 +327,14 @@ enum ItemType: Decodable {
|
|||||||
case currency
|
case currency
|
||||||
case inputsource
|
case inputsource
|
||||||
case music
|
case music
|
||||||
case groupBar
|
case group
|
||||||
case nightShift
|
case nightShift
|
||||||
case dnd
|
case dnd
|
||||||
case pomodoro
|
case pomodoro
|
||||||
case network
|
case network
|
||||||
case darkMode
|
case darkMode
|
||||||
|
case swipe
|
||||||
|
case upnext
|
||||||
}
|
}
|
||||||
|
|
||||||
init(from decoder: Decoder) throws {
|
init(from decoder: Decoder) throws {
|
||||||
@ -412,7 +344,13 @@ enum ItemType: Decodable {
|
|||||||
case .appleScriptTitledButton:
|
case .appleScriptTitledButton:
|
||||||
let source = try container.decode(Source.self, forKey: .source)
|
let source = try container.decode(Source.self, forKey: .source)
|
||||||
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 1800.0
|
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)
|
||||||
|
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 1800.0
|
||||||
|
self = .shellScriptTitledButton(source: source, refreshInterval: interval)
|
||||||
|
|
||||||
case .staticButton:
|
case .staticButton:
|
||||||
let title = try container.decode(String.self, forKey: .title)
|
let title = try container.decode(String.self, forKey: .title)
|
||||||
@ -421,17 +359,23 @@ enum ItemType: Decodable {
|
|||||||
case .timeButton:
|
case .timeButton:
|
||||||
let template = try container.decodeIfPresent(String.self, forKey: .formatTemplate) ?? "HH:mm"
|
let template = try container.decodeIfPresent(String.self, forKey: .formatTemplate) ?? "HH:mm"
|
||||||
let timeZone = try container.decodeIfPresent(String.self, forKey: .timeZone) ?? nil
|
let timeZone = try container.decodeIfPresent(String.self, forKey: .timeZone) ?? nil
|
||||||
self = .timeButton(formatTemplate: template, timeZone: timeZone)
|
let locale = try container.decodeIfPresent(String.self, forKey: .locale) ?? nil
|
||||||
|
self = .timeButton(formatTemplate: template, timeZone: timeZone, locale: locale)
|
||||||
|
|
||||||
case .battery:
|
case .battery:
|
||||||
self = .battery()
|
self = .battery
|
||||||
|
|
||||||
|
case .cpu:
|
||||||
|
let refreshInterval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 5.0
|
||||||
|
self = .cpu(refreshInterval: refreshInterval)
|
||||||
|
|
||||||
case .dock:
|
case .dock:
|
||||||
let autoResize = try container.decodeIfPresent(Bool.self, forKey: .autoResize) ?? false
|
let autoResize = try container.decodeIfPresent(Bool.self, forKey: .autoResize) ?? false
|
||||||
self = .dock(autoResize: autoResize)
|
let filterRegexString = try container.decodeIfPresent(String.self, forKey: .filter)
|
||||||
|
self = .dock(autoResize: autoResize, filter: filterRegexString)
|
||||||
|
|
||||||
case .volume:
|
case .volume:
|
||||||
self = .volume()
|
self = .volume
|
||||||
|
|
||||||
case .brightness:
|
case .brightness:
|
||||||
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 0.5
|
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 0.5
|
||||||
@ -456,22 +400,22 @@ enum ItemType: Decodable {
|
|||||||
self = .currency(interval: interval, from: from, to: to, full: full)
|
self = .currency(interval: interval, from: from, to: to, full: full)
|
||||||
|
|
||||||
case .inputsource:
|
case .inputsource:
|
||||||
self = .inputsource()
|
self = .inputsource
|
||||||
|
|
||||||
case .music:
|
case .music:
|
||||||
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 5.0
|
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 5.0
|
||||||
let disableMarquee = try container.decodeIfPresent(Bool.self, forKey: .disableMarquee) ?? false
|
let disableMarquee = try container.decodeIfPresent(Bool.self, forKey: .disableMarquee) ?? false
|
||||||
self = .music(interval: interval, disableMarquee: disableMarquee)
|
self = .music(interval: interval, disableMarquee: disableMarquee)
|
||||||
|
|
||||||
case .groupBar:
|
case .group:
|
||||||
let items = try container.decode([BarItemDefinition].self, forKey: .items)
|
let items = try container.decode([BarItemDefinition].self, forKey: .items)
|
||||||
self = .groupBar(items: items)
|
self = .group(items: items)
|
||||||
|
|
||||||
case .nightShift:
|
case .nightShift:
|
||||||
self = .nightShift()
|
self = .nightShift
|
||||||
|
|
||||||
case .dnd:
|
case .dnd:
|
||||||
self = .dnd()
|
self = .dnd
|
||||||
|
|
||||||
case .pomodoro:
|
case .pomodoro:
|
||||||
let workTime = try container.decodeIfPresent(Double.self, forKey: .workTime) ?? 1500.0
|
let workTime = try container.decodeIfPresent(Double.self, forKey: .workTime) ?? 1500.0
|
||||||
@ -480,15 +424,119 @@ enum ItemType: Decodable {
|
|||||||
|
|
||||||
case .network:
|
case .network:
|
||||||
let flip = try container.decodeIfPresent(Bool.self, forKey: .flip) ?? false
|
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:
|
case .darkMode:
|
||||||
self = .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 none
|
||||||
case hidKey(keycode: Int32)
|
case hidKey(keycode: Int32)
|
||||||
case keyPress(keycode: Int)
|
case keyPress(keycode: Int)
|
||||||
@ -546,7 +594,7 @@ enum ActionType: Decodable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum LongActionType: Decodable {
|
enum LegacyLongActionType: Decodable {
|
||||||
case none
|
case none
|
||||||
case hidKey(keycode: Int32)
|
case hidKey(keycode: Int32)
|
||||||
case keyPress(keycode: Int)
|
case keyPress(keycode: Int)
|
||||||
@ -611,6 +659,7 @@ enum GeneralParameter {
|
|||||||
case bordered(_: Bool)
|
case bordered(_: Bool)
|
||||||
case background(_: NSColor)
|
case background(_: NSColor)
|
||||||
case title(_: String)
|
case title(_: String)
|
||||||
|
case matchAppId(_: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct GeneralParameters: Decodable {
|
struct GeneralParameters: Decodable {
|
||||||
@ -623,6 +672,7 @@ struct GeneralParameters: Decodable {
|
|||||||
case bordered
|
case bordered
|
||||||
case background
|
case background
|
||||||
case title
|
case title
|
||||||
|
case matchAppId
|
||||||
}
|
}
|
||||||
|
|
||||||
init(from decoder: Decoder) throws {
|
init(from decoder: Decoder) throws {
|
||||||
@ -652,6 +702,10 @@ struct GeneralParameters: Decodable {
|
|||||||
result[.title] = .title(title)
|
result[.title] = .title(title)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let matchAppId = try container.decodeIfPresent(String.self, forKey: .matchAppId) {
|
||||||
|
result[.matchAppId] = .matchAppId(matchAppId)
|
||||||
|
}
|
||||||
|
|
||||||
parameters = result
|
parameters = result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -725,12 +779,20 @@ extension String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var fileData: Data? {
|
var fileData: Data? {
|
||||||
return try? Data(contentsOf: URL(fileURLWithPath: self))
|
return try? Data(contentsOf: URL(fileURLWithPath: (self as NSString).expandingTildeInPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileString: String? {
|
var fileString: String? {
|
||||||
var encoding: String.Encoding = .utf8
|
var encoding: String.Encoding = .utf8
|
||||||
return try? String(contentsOfFile: self, usedEncoding: &encoding)
|
return try? String(contentsOf: URL(fileURLWithPath: (self as NSString).expandingTildeInPath), usedEncoding: &encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileURL: URL {
|
||||||
|
return URL(fileURLWithPath: (self as NSString).expandingTildeInPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
var appleScript: NSAppleScript? {
|
||||||
|
return NSAppleScript(source: self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -750,16 +812,6 @@ enum Align: String, Decodable {
|
|||||||
case right
|
case right
|
||||||
}
|
}
|
||||||
|
|
||||||
extension String {
|
|
||||||
var fileURL: URL {
|
|
||||||
return URL(fileURLWithPath: self)
|
|
||||||
}
|
|
||||||
|
|
||||||
var appleScript: NSAppleScript? {
|
|
||||||
return NSAppleScript(source: self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension URL {
|
extension URL {
|
||||||
var appleScript: NSAppleScript? {
|
var appleScript: NSAppleScript? {
|
||||||
guard FileManager.default.fileExists(atPath: path) else { return nil }
|
guard FileManager.default.fileExists(atPath: path) else { return nil }
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class ScrollViewItem: NSCustomTouchBarItem, NSGestureRecognizerDelegate {
|
class ScrollViewItem: NSCustomTouchBarItem/*, NSGestureRecognizerDelegate*/ {
|
||||||
var twofingersPrev: CGFloat = 0.0
|
|
||||||
var threefingersPrev: CGFloat = 0.0
|
|
||||||
|
|
||||||
init(identifier: NSTouchBarItem.Identifier, items: [NSTouchBarItem]) {
|
init(identifier: NSTouchBarItem.Identifier, items: [NSTouchBarItem]) {
|
||||||
super.init(identifier: identifier)
|
super.init(identifier: identifier)
|
||||||
@ -13,63 +11,10 @@ class ScrollViewItem: NSCustomTouchBarItem, NSGestureRecognizerDelegate {
|
|||||||
let scrollView = NSScrollView(frame: CGRect(origin: .zero, size: stackView.fittingSize))
|
let scrollView = NSScrollView(frame: CGRect(origin: .zero, size: stackView.fittingSize))
|
||||||
scrollView.documentView = stackView
|
scrollView.documentView = stackView
|
||||||
view = scrollView
|
view = scrollView
|
||||||
|
|
||||||
let twofingers = NSPanGestureRecognizer(target: self, action: #selector(twofingersHandler(_:)))
|
|
||||||
twofingers.allowedTouchTypes = .direct
|
|
||||||
twofingers.numberOfTouchesRequired = 2
|
|
||||||
view.addGestureRecognizer(twofingers)
|
|
||||||
|
|
||||||
let threefingers = NSPanGestureRecognizer(target: self, action: #selector(threefingersHandler(_:)))
|
|
||||||
threefingers.allowedTouchTypes = .direct
|
|
||||||
threefingers.numberOfTouchesRequired = 3
|
|
||||||
view.addGestureRecognizer(threefingers)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder _: NSCoder) {
|
required init?(coder _: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
113
MTMR/ShellScriptTouchBarItem.swift
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
//
|
||||||
|
// ShellScriptTouchBarItem.swift
|
||||||
|
// MTMR
|
||||||
|
//
|
||||||
|
// Created by bobr on 08/08/2019.
|
||||||
|
// Copyright © 2019 Anton Palgunov. All rights reserved.
|
||||||
|
//
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class ShellScriptTouchBarItem: CustomButtonTouchBarItem {
|
||||||
|
private let interval: TimeInterval
|
||||||
|
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\""
|
||||||
|
super.init(identifier: identifier, title: "⏳")
|
||||||
|
|
||||||
|
forceHideConstraint = view.widthAnchor.constraint(equalToConstant: 0)
|
||||||
|
|
||||||
|
DispatchQueue.shellScriptQueue.async {
|
||||||
|
self.refreshAndSchedule()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder _: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
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: 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
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
DispatchQueue.main.async { [weak self, newBackgoundColor] in
|
||||||
|
if (newBackgoundColor != self?.backgroundColor) { // performance optimization because of reinstallButton
|
||||||
|
self?.backgroundColor = newBackgoundColor
|
||||||
|
}
|
||||||
|
self?.attributedTitle = title
|
||||||
|
if json {
|
||||||
|
self?.image = image
|
||||||
|
}
|
||||||
|
self?.forceHideConstraint.isActive = scriptResult == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule next update
|
||||||
|
DispatchQueue.shellScriptQueue.asyncAfter(deadline: .now() + interval) { [weak self] in
|
||||||
|
self?.refreshAndSchedule()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.replacingOccurrences(of: "\\n+$", with: "", options: .regularExpression)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DispatchQueue {
|
||||||
|
static let shellScriptQueue = DispatchQueue(label: "mtmr.shellscript")
|
||||||
|
}
|
||||||
78
MTMR/SwipeItem.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -23,13 +23,17 @@ extension ItemType {
|
|||||||
return "com.toxblh.mtmr.staticButton."
|
return "com.toxblh.mtmr.staticButton."
|
||||||
case .appleScriptTitledButton(source: _):
|
case .appleScriptTitledButton(source: _):
|
||||||
return "com.toxblh.mtmr.appleScriptButton."
|
return "com.toxblh.mtmr.appleScriptButton."
|
||||||
case .timeButton(formatTemplate: _, timeZone: _):
|
case .shellScriptTitledButton(source: _):
|
||||||
|
return "com.toxblh.mtmr.shellScriptButton."
|
||||||
|
case .timeButton(formatTemplate: _, timeZone: _, locale: _):
|
||||||
return "com.toxblh.mtmr.timeButton."
|
return "com.toxblh.mtmr.timeButton."
|
||||||
case .battery():
|
case .battery:
|
||||||
return "com.toxblh.mtmr.battery."
|
return "com.toxblh.mtmr.battery."
|
||||||
case .dock(autoResize: _):
|
case .cpu(refreshInterval: _):
|
||||||
|
return "com.toxblh.mtmr.cpu."
|
||||||
|
case .dock(autoResize: _, filter: _):
|
||||||
return "com.toxblh.mtmr.dock"
|
return "com.toxblh.mtmr.dock"
|
||||||
case .volume():
|
case .volume:
|
||||||
return "com.toxblh.mtmr.volume"
|
return "com.toxblh.mtmr.volume"
|
||||||
case .brightness(refreshInterval: _):
|
case .brightness(refreshInterval: _):
|
||||||
return "com.toxblh.mtmr.brightness"
|
return "com.toxblh.mtmr.brightness"
|
||||||
@ -39,22 +43,26 @@ extension ItemType {
|
|||||||
return "com.toxblh.mtmr.yandexWeather"
|
return "com.toxblh.mtmr.yandexWeather"
|
||||||
case .currency(interval: _, from: _, to: _, full: _):
|
case .currency(interval: _, from: _, to: _, full: _):
|
||||||
return "com.toxblh.mtmr.currency"
|
return "com.toxblh.mtmr.currency"
|
||||||
case .inputsource():
|
case .inputsource:
|
||||||
return "com.toxblh.mtmr.inputsource."
|
return "com.toxblh.mtmr.inputsource."
|
||||||
case .music(interval: _):
|
case .music(interval: _):
|
||||||
return "com.toxblh.mtmr.music."
|
return "com.toxblh.mtmr.music."
|
||||||
case .groupBar(items: _):
|
case .group(items: _):
|
||||||
return "com.toxblh.mtmr.groupBar."
|
return "com.toxblh.mtmr.groupBar."
|
||||||
case .nightShift(items: _):
|
case .nightShift:
|
||||||
return "com.toxblh.mtmr.nightShift."
|
return "com.toxblh.mtmr.nightShift."
|
||||||
case .dnd(items: _):
|
case .dnd:
|
||||||
return "com.toxblh.mtmr.dnd."
|
return "com.toxblh.mtmr.dnd."
|
||||||
case .pomodoro(interval: _):
|
case .pomodoro(interval: _):
|
||||||
return PomodoroBarItem.identifier
|
return PomodoroBarItem.identifier
|
||||||
case .network(flip: _):
|
case .network(flip: _):
|
||||||
return NetworkBarItem.identifier
|
return NetworkBarItem.identifier
|
||||||
case .darkMode(items: _):
|
case .darkMode:
|
||||||
return DarkModeBarItem.identifier
|
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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -74,40 +82,43 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
|||||||
var items: [NSTouchBarItem.Identifier: NSTouchBarItem] = [:]
|
var items: [NSTouchBarItem.Identifier: NSTouchBarItem] = [:]
|
||||||
var leftIdentifiers: [NSTouchBarItem.Identifier] = []
|
var leftIdentifiers: [NSTouchBarItem.Identifier] = []
|
||||||
var centerIdentifiers: [NSTouchBarItem.Identifier] = []
|
var centerIdentifiers: [NSTouchBarItem.Identifier] = []
|
||||||
var centerItems: [NSTouchBarItem] = []
|
|
||||||
var rightIdentifiers: [NSTouchBarItem.Identifier] = []
|
var rightIdentifiers: [NSTouchBarItem.Identifier] = []
|
||||||
var scrollArea: NSCustomTouchBarItem?
|
var basicViewIdentifier = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollView.".appending(UUID().uuidString))
|
||||||
var centerScrollArea = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollArea.".appending(UUID().uuidString))
|
var basicView: BasicView?
|
||||||
|
var swipeItems: [SwipeItem] = []
|
||||||
var showControlStripState: Bool {
|
|
||||||
get {
|
|
||||||
return UserDefaults.standard.bool(forKey: "com.toxblh.mtmr.settings.showControlStrip")
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
UserDefaults.standard.set(newValue, forKey: "com.toxblh.mtmr.settings.showControlStrip")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var blacklistAppIdentifiers: [String] = []
|
var blacklistAppIdentifiers: [String] = []
|
||||||
var frontmostApplicationIdentifier: String? {
|
var frontmostApplicationIdentifier: String? {
|
||||||
guard let frontmostId = NSWorkspace.shared.frontmostApplication?.bundleIdentifier else { return nil }
|
return NSWorkspace.shared.frontmostApplication?.bundleIdentifier
|
||||||
return frontmostId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private override init() {
|
private override init() {
|
||||||
super.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
|
SupportedTypesHolder.sharedInstance.register(typename: "close") { _ in
|
||||||
(item: .staticButton(title: ""), action: .custom(closure: { [weak self] in
|
(
|
||||||
guard let `self` = self else { return }
|
item: .staticButton(title: ""),
|
||||||
self.reloadPreset(path: self.lastPresetPath)
|
actions: [
|
||||||
}), longAction: .none, parameters: [.width: .width(30), .image: .image(source: (NSImage(named: NSImage.stopProgressFreestandingTemplateName))!)])
|
Action(trigger: .singleTap, value: .custom(closure: { [weak self] in
|
||||||
|
guard let `self` = self else { return }
|
||||||
|
self.reloadPreset(path: self.lastPresetPath)
|
||||||
|
}))
|
||||||
|
],
|
||||||
|
legacyAction: .none,
|
||||||
|
legacyLongAction: .none,
|
||||||
|
parameters: [.width: .width(30), .image: .image(source: (NSImage(named: NSImage.stopProgressFreestandingTemplateName))!)])
|
||||||
}
|
}
|
||||||
|
|
||||||
if let blackListed = UserDefaults.standard.stringArray(forKey: "com.toxblh.mtmr.blackListedApps") {
|
blacklistAppIdentifiers = AppSettings.blacklistedAppIds
|
||||||
blacklistAppIdentifiers = blackListed
|
|
||||||
}
|
|
||||||
|
|
||||||
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(activeApplicationChanged), name: NSWorkspace.didLaunchApplicationNotification, object: nil)
|
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(activeApplicationChanged), name: NSWorkspace.didLaunchApplicationNotification, object: nil)
|
||||||
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(activeApplicationChanged), name: NSWorkspace.didTerminateApplicationNotification, object: nil)
|
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(activeApplicationChanged), name: NSWorkspace.didTerminateApplicationNotification, object: nil)
|
||||||
@ -123,26 +134,69 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
|||||||
touchBar = NSTouchBar()
|
touchBar = NSTouchBar()
|
||||||
jsonItems = newJsonItems
|
jsonItems = newJsonItems
|
||||||
itemDefinitions = [:]
|
itemDefinitions = [:]
|
||||||
items = [:]
|
|
||||||
leftIdentifiers = []
|
|
||||||
centerItems = []
|
|
||||||
rightIdentifiers = []
|
|
||||||
|
|
||||||
loadItemDefinitions(jsonItems: jsonItems)
|
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()
|
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]
|
items[identifier]
|
||||||
})
|
})
|
||||||
|
|
||||||
centerScrollArea = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollArea.".appending(UUID().uuidString))
|
let centerScrollArea = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollArea.".appending(UUID().uuidString))
|
||||||
scrollArea = ScrollViewItem(identifier: centerScrollArea, items: centerItems)
|
let scrollArea = ScrollViewItem(identifier: centerScrollArea, items: centerItems)
|
||||||
|
|
||||||
|
basicViewIdentifier = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollView.".appending(UUID().uuidString))
|
||||||
|
|
||||||
touchBar.delegate = self
|
touchBar.delegate = self
|
||||||
touchBar.defaultItemIdentifiers = []
|
touchBar.defaultItemIdentifiers = [basicViewIdentifier]
|
||||||
touchBar.defaultItemIdentifiers = leftIdentifiers + [centerScrollArea] + rightIdentifiers
|
|
||||||
|
|
||||||
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) {
|
@objc func activeApplicationChanged(_: Notification) {
|
||||||
@ -150,12 +204,21 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateActiveApp() {
|
func updateActiveApp() {
|
||||||
if frontmostApplicationIdentifier != nil && blacklistAppIdentifiers.index(of: frontmostApplicationIdentifier!) != nil {
|
if frontmostApplicationIdentifier != nil && blacklistAppIdentifiers.firstIndex(of: frontmostApplicationIdentifier!) != nil {
|
||||||
dismissTouchBar()
|
dismissTouchBar()
|
||||||
} else {
|
} else {
|
||||||
presentTouchBar()
|
prepareTouchBar()
|
||||||
|
if touchBarContainsAnyItems() {
|
||||||
|
presentTouchBar()
|
||||||
|
} else {
|
||||||
|
dismissTouchBar()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func touchBarContainsAnyItems() -> Bool {
|
||||||
|
return items.count != 0 || swipeItems.count != 0
|
||||||
|
}
|
||||||
|
|
||||||
func reloadStandardConfig() {
|
func reloadStandardConfig() {
|
||||||
let presetPath = standardConfigPath
|
let presetPath = standardConfigPath
|
||||||
@ -170,7 +233,7 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
|||||||
|
|
||||||
func reloadPreset(path: String) {
|
func reloadPreset(path: String) {
|
||||||
lastPresetPath = path
|
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)
|
createAndUpdatePreset(newJsonItems: items)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,8 +258,30 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func createItems() {
|
func createItems() {
|
||||||
|
items = [:]
|
||||||
|
swipeItems = []
|
||||||
|
|
||||||
for (identifier, definition) in itemDefinitions {
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,39 +294,37 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateControlStripPresence() {
|
func updateControlStripPresence() {
|
||||||
DFRElementSetControlStripPresenceForIdentifier(.controlStripItem, true)
|
let showMtmrButtonOnControlStrip = touchBarContainsAnyItems()
|
||||||
|
DFRElementSetControlStripPresenceForIdentifier(.controlStripItem, showMtmrButtonOnControlStrip)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func presentTouchBar() {
|
@objc private func presentTouchBar() {
|
||||||
if showControlStripState {
|
if AppSettings.showControlStripState {
|
||||||
updateControlStripPresence()
|
|
||||||
presentSystemModal(touchBar, systemTrayItemIdentifier: .controlStripItem)
|
presentSystemModal(touchBar, systemTrayItemIdentifier: .controlStripItem)
|
||||||
} else {
|
} else {
|
||||||
presentSystemModal(touchBar, placement: 1, systemTrayItemIdentifier: .controlStripItem)
|
presentSystemModal(touchBar, placement: 1, systemTrayItemIdentifier: .controlStripItem)
|
||||||
}
|
}
|
||||||
|
updateControlStripPresence()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func dismissTouchBar() {
|
@objc private func dismissTouchBar() {
|
||||||
minimizeSystemModal(touchBar)
|
if touchBarContainsAnyItems() {
|
||||||
|
minimizeSystemModal(touchBar)
|
||||||
|
}
|
||||||
updateControlStripPresence()
|
updateControlStripPresence()
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func resetControlStrip() {
|
@objc func resetControlStrip() {
|
||||||
dismissTouchBar()
|
dismissTouchBar()
|
||||||
presentTouchBar()
|
updateActiveApp()
|
||||||
}
|
}
|
||||||
|
|
||||||
func touchBar(_: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? {
|
func touchBar(_: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? {
|
||||||
if identifier == centerScrollArea {
|
if identifier == basicViewIdentifier {
|
||||||
return scrollArea
|
return basicView
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let item = self.items[identifier],
|
return nil
|
||||||
let definition = self.itemDefinitions[identifier],
|
|
||||||
definition.align != .center else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createItem(forIdentifier identifier: NSTouchBarItem.Identifier, definition item: BarItemDefinition) -> NSTouchBarItem? {
|
func createItem(forIdentifier identifier: NSTouchBarItem.Identifier, definition item: BarItemDefinition) -> NSTouchBarItem? {
|
||||||
@ -249,14 +332,26 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
|||||||
switch item.type {
|
switch item.type {
|
||||||
case let .staticButton(title: title):
|
case let .staticButton(title: title):
|
||||||
barItem = CustomButtonTouchBarItem(identifier: identifier, title: title)
|
barItem = CustomButtonTouchBarItem(identifier: identifier, title: title)
|
||||||
case let .appleScriptTitledButton(source: source, refreshInterval: interval):
|
case let .appleScriptTitledButton(source: source, refreshInterval: interval, alternativeImages: alternativeImages):
|
||||||
barItem = AppleScriptTouchBarItem(identifier: identifier, source: source, interval: interval)
|
barItem = AppleScriptTouchBarItem(identifier: identifier, source: source, interval: interval, alternativeImages: alternativeImages)
|
||||||
case let .timeButton(formatTemplate: template, timeZone: timeZone):
|
case let .shellScriptTitledButton(source: source, refreshInterval: interval):
|
||||||
barItem = TimeTouchBarItem(identifier: identifier, formatTemplate: template, timeZone: timeZone)
|
barItem = ShellScriptTouchBarItem(identifier: identifier, source: source, interval: interval)
|
||||||
case .battery():
|
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)
|
barItem = BatteryBarItem(identifier: identifier)
|
||||||
case let .dock(autoResize: autoResize):
|
case let .cpu(refreshInterval: refreshInterval):
|
||||||
barItem = AppScrubberTouchBarItem(identifier: identifier, autoResize: autoResize)
|
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 {
|
||||||
|
barItem = CustomButtonTouchBarItem(identifier: identifier, title: "Bad regex")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
barItem = AppScrubberTouchBarItem(identifier: identifier, autoResize: autoResize, filter: regex)
|
||||||
|
} else {
|
||||||
|
barItem = AppScrubberTouchBarItem(identifier: identifier, autoResize: autoResize)
|
||||||
|
}
|
||||||
case .volume:
|
case .volume:
|
||||||
if case let .image(source)? = item.additionalParameters[.image] {
|
if case let .image(source)? = item.additionalParameters[.image] {
|
||||||
barItem = VolumeViewController(identifier: identifier, image: source.image)
|
barItem = VolumeViewController(identifier: identifier, image: source.image)
|
||||||
@ -275,29 +370,39 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
|||||||
barItem = YandexWeatherBarItem(identifier: identifier, interval: interval)
|
barItem = YandexWeatherBarItem(identifier: identifier, interval: interval)
|
||||||
case let .currency(interval: interval, from: from, to: to, full: full):
|
case let .currency(interval: interval, from: from, to: to, full: full):
|
||||||
barItem = CurrencyBarItem(identifier: identifier, interval: interval, from: from, to: to, full: full)
|
barItem = CurrencyBarItem(identifier: identifier, interval: interval, from: from, to: to, full: full)
|
||||||
case .inputsource():
|
case .inputsource:
|
||||||
barItem = InputSourceBarItem(identifier: identifier)
|
barItem = InputSourceBarItem(identifier: identifier)
|
||||||
case let .music(interval: interval, disableMarquee: disableMarquee):
|
case let .music(interval: interval, disableMarquee: disableMarquee):
|
||||||
barItem = MusicBarItem(identifier: identifier, interval: interval, disableMarquee: disableMarquee)
|
barItem = MusicBarItem(identifier: identifier, interval: interval, disableMarquee: disableMarquee)
|
||||||
case let .groupBar(items: items):
|
case let .group(items: items):
|
||||||
barItem = GroupBarItem(identifier: identifier, items: items)
|
barItem = GroupBarItem(identifier: identifier, items: items)
|
||||||
case .nightShift():
|
case .nightShift:
|
||||||
barItem = NightShiftBarItem(identifier: identifier)
|
barItem = NightShiftBarItem(identifier: identifier)
|
||||||
case .dnd():
|
case .dnd:
|
||||||
barItem = DnDBarItem(identifier: identifier)
|
barItem = DnDBarItem(identifier: identifier)
|
||||||
case let .pomodoro(workTime: workTime, restTime: restTime):
|
case let .pomodoro(workTime: workTime, restTime: restTime):
|
||||||
barItem = PomodoroBarItem(identifier: identifier, workTime: workTime, restTime: restTime)
|
barItem = PomodoroBarItem(identifier: identifier, workTime: workTime, restTime: restTime)
|
||||||
case let .network(flip: flip):
|
case let .network(flip: flip, units: units):
|
||||||
barItem = NetworkBarItem(identifier: identifier, flip: flip)
|
barItem = NetworkBarItem(identifier: identifier, flip: flip, units: units)
|
||||||
case .darkMode():
|
case .darkMode:
|
||||||
barItem = DarkModeBarItem(identifier: identifier)
|
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 {
|
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 {
|
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 {
|
if case let .bordered(bordered)? = item.additionalParameters[.bordered], let item = barItem as? CustomButtonTouchBarItem {
|
||||||
item.isBordered = bordered
|
item.isBordered = bordered
|
||||||
@ -320,9 +425,53 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
|||||||
}
|
}
|
||||||
return barItem
|
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)? {
|
func action(forItem item: BarItemDefinition) -> (() -> Void)? {
|
||||||
switch item.action {
|
switch item.legacyAction {
|
||||||
case let .hidKey(keycode: keycode):
|
case let .hidKey(keycode: keycode):
|
||||||
return { HIDPostAuxKey(keycode) }
|
return { HIDPostAuxKey(keycode) }
|
||||||
case let .keyPress(keycode: keycode):
|
case let .keyPress(keycode: keycode):
|
||||||
@ -366,7 +515,7 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func longAction(forItem item: BarItemDefinition) -> (() -> Void)? {
|
func longAction(forItem item: BarItemDefinition) -> (() -> Void)? {
|
||||||
switch item.longAction {
|
switch item.legacyLongAction {
|
||||||
case let .hidKey(keycode: keycode):
|
case let .hidKey(keycode: keycode):
|
||||||
return { HIDPostAuxKey(keycode) }
|
return { HIDPostAuxKey(keycode) }
|
||||||
case let .keyPress(keycode: keycode):
|
case let .keyPress(keycode: keycode):
|
||||||
@ -418,6 +567,12 @@ extension NSCustomTouchBarItem: CanSetWidth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension NSPopoverTouchBarItem: CanSetWidth {
|
||||||
|
func setWidth(value: CGFloat) {
|
||||||
|
view?.widthAnchor.constraint(equalToConstant: value).isActive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension BarItemDefinition {
|
extension BarItemDefinition {
|
||||||
var align: Align {
|
var align: Align {
|
||||||
if case let .align(result)? = additionalParameters[.align] {
|
if case let .align(result)? = additionalParameters[.align] {
|
||||||
|
|||||||
@ -7,238 +7,158 @@
|
|||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class AppScrubberTouchBarItem: NSCustomTouchBarItem, NSScrubberDelegate, NSScrubberDataSource {
|
class AppScrubberTouchBarItem: NSCustomTouchBarItem {
|
||||||
private var scrubber: NSScrubber!
|
private var scrollView = NSScrollView()
|
||||||
|
|
||||||
private var timer: Timer!
|
|
||||||
private var ticks: Int = 0
|
|
||||||
private let minTicks: Int = 5
|
|
||||||
private let maxTicks: Int = 20
|
|
||||||
private var lastSelected: Int = 0
|
|
||||||
private var autoResize: Bool = false
|
private var autoResize: Bool = false
|
||||||
private var widthConstraint: NSLayoutConstraint?
|
private var widthConstraint: NSLayoutConstraint?
|
||||||
|
private let filter: NSRegularExpression?
|
||||||
|
|
||||||
private var persistentAppIdentifiers: [String] = []
|
private var persistentAppIdentifiers: [String] = []
|
||||||
private var runningAppsIdentifiers: [String] = []
|
private var runningAppsIdentifiers: [String] = []
|
||||||
|
|
||||||
private var frontmostApplicationIdentifier: String? {
|
private var frontmostApplicationIdentifier: String? {
|
||||||
guard let frontmostId = NSWorkspace.shared.frontmostApplication?.bundleIdentifier else { return nil }
|
return NSWorkspace.shared.frontmostApplication?.bundleIdentifier
|
||||||
return frontmostId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var applications: [DockItem] = []
|
private var applications: [DockItem] = []
|
||||||
|
private var items: [DockBarItem] = []
|
||||||
convenience override init(identifier: NSTouchBarItem.Identifier) {
|
|
||||||
self.init(identifier: identifier, autoResize: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
static var iconWidth = 36
|
|
||||||
static var spacingWidth = 2
|
|
||||||
|
|
||||||
init(identifier: NSTouchBarItem.Identifier, autoResize: Bool) {
|
init(identifier: NSTouchBarItem.Identifier, autoResize: Bool = false, filter: NSRegularExpression? = nil) {
|
||||||
|
self.filter = filter
|
||||||
super.init(identifier: identifier)
|
super.init(identifier: identifier)
|
||||||
self.autoResize = autoResize
|
self.autoResize = autoResize
|
||||||
|
view = scrollView
|
||||||
scrubber = NSScrubber()
|
|
||||||
scrubber.delegate = self
|
|
||||||
scrubber.dataSource = self
|
|
||||||
scrubber.mode = .free // .fixed
|
|
||||||
let layout = NSScrubberFlowLayout()
|
|
||||||
layout.itemSize = NSSize(width: AppScrubberTouchBarItem.iconWidth, height: 32)
|
|
||||||
layout.itemSpacing = CGFloat(AppScrubberTouchBarItem.spacingWidth)
|
|
||||||
scrubber.scrubberLayout = layout
|
|
||||||
scrubber.selectionBackgroundStyle = .roundedBackground
|
|
||||||
scrubber.showsAdditionalContentIndicators = true
|
|
||||||
|
|
||||||
view = scrubber
|
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(hardReloadItems), name: NSWorkspace.didLaunchApplicationNotification, object: nil)
|
||||||
|
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(hardReloadItems), name: NSWorkspace.didTerminateApplicationNotification, object: nil)
|
||||||
|
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(softReloadItems), name: NSWorkspace.didActivateApplicationNotification, object: nil)
|
||||||
|
|
||||||
scrubber.register(NSScrubberImageItemView.self, forItemIdentifier: NSUserInterfaceItemIdentifier(rawValue: "ScrubberApplicationsItemReuseIdentifier"))
|
persistentAppIdentifiers = AppSettings.dockPersistentAppIds
|
||||||
|
hardReloadItems()
|
||||||
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(activeApplicationChanged), name: NSWorkspace.didLaunchApplicationNotification, object: nil)
|
|
||||||
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(activeApplicationChanged), name: NSWorkspace.didTerminateApplicationNotification, object: nil)
|
|
||||||
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(activeApplicationChanged), name: NSWorkspace.didActivateApplicationNotification, object: nil)
|
|
||||||
|
|
||||||
if let persistent = UserDefaults.standard.stringArray(forKey: "com.toxblh.mtmr.dock.persistent") {
|
|
||||||
persistentAppIdentifiers = persistent
|
|
||||||
}
|
|
||||||
|
|
||||||
updateRunningApplication()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder _: NSCoder) {
|
required init?(coder _: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func activeApplicationChanged(n _: Notification) {
|
@objc func hardReloadItems() {
|
||||||
updateRunningApplication()
|
applications = launchedApplications()
|
||||||
}
|
|
||||||
|
|
||||||
override func observeValue(forKeyPath _: String?, of _: Any?, change _: [NSKeyValueChangeKey: Any]?, context _: UnsafeMutableRawPointer?) {
|
|
||||||
updateRunningApplication()
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateRunningApplication() {
|
|
||||||
let newApplications = launchedApplications()
|
|
||||||
|
|
||||||
let index = newApplications.index {
|
|
||||||
$0.bundleIdentifier == frontmostApplicationIdentifier
|
|
||||||
}
|
|
||||||
|
|
||||||
applications = newApplications
|
|
||||||
applications += getDockPersistentAppsList()
|
applications += getDockPersistentAppsList()
|
||||||
scrubber.reloadData()
|
reloadData()
|
||||||
|
softReloadItems()
|
||||||
updateSize()
|
updateSize()
|
||||||
|
}
|
||||||
scrubber.selectedIndex = index ?? 0
|
|
||||||
|
@objc func softReloadItems() {
|
||||||
|
let frontMostAppId = self.frontmostApplicationIdentifier
|
||||||
|
let runningAppsIds = NSWorkspace.shared.runningApplications.map { $0.bundleIdentifier }
|
||||||
|
for barItem in items {
|
||||||
|
let bundleId = barItem.dockItem.bundleIdentifier
|
||||||
|
barItem.isRunning = runningAppsIds.contains(bundleId)
|
||||||
|
barItem.isFrontmost = frontMostAppId == bundleId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateSize() {
|
func updateSize() {
|
||||||
if self.autoResize {
|
if self.autoResize {
|
||||||
if let constraint: NSLayoutConstraint = self.widthConstraint {
|
self.widthConstraint?.isActive = false
|
||||||
constraint.isActive = false
|
|
||||||
self.scrubber.removeConstraint(constraint)
|
let width = self.scrollView.documentView?.fittingSize.width ?? 0
|
||||||
}
|
self.widthConstraint = self.scrollView.widthAnchor.constraint(equalToConstant: width)
|
||||||
let width = (AppScrubberTouchBarItem.iconWidth + AppScrubberTouchBarItem.spacingWidth) * self.applications.count - AppScrubberTouchBarItem.spacingWidth
|
|
||||||
self.widthConstraint = self.scrubber.widthAnchor.constraint(equalToConstant: CGFloat(width))
|
|
||||||
self.widthConstraint!.isActive = true
|
self.widthConstraint!.isActive = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func numberOfItems(for _: NSScrubber) -> Int {
|
func reloadData() {
|
||||||
return applications.count
|
items = applications.map { self.createAppButton(for: $0) }
|
||||||
|
let stackView = NSStackView(views: items.compactMap { $0.view })
|
||||||
|
stackView.spacing = 1
|
||||||
|
stackView.orientation = .horizontal
|
||||||
|
let visibleRect = self.scrollView.documentVisibleRect
|
||||||
|
scrollView.documentView = stackView
|
||||||
|
stackView.scroll(visibleRect.origin)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func scrubber(_ scrubber: NSScrubber, viewForItemAt index: Int) -> NSScrubberItemView {
|
public func createAppButton(for app: DockItem) -> DockBarItem {
|
||||||
let item = scrubber.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "ScrubberApplicationsItemReuseIdentifier"), owner: self) as? NSScrubberImageItemView ?? NSScrubberImageItemView()
|
let item = DockBarItem(app)
|
||||||
item.imageView.imageScaling = .scaleProportionallyDown
|
item.isBordered = false
|
||||||
|
item.actions.append(contentsOf: [
|
||||||
let app = applications[index]
|
ItemAction(trigger: .singleTap) { [weak self] in
|
||||||
|
self?.switchToApp(app: app)
|
||||||
if let icon = app.icon {
|
},
|
||||||
item.image = icon
|
ItemAction(trigger: .longTap) { [weak self] in
|
||||||
item.image.size = NSSize(width: 26, height: 26)
|
self?.handleHalfLongPress(item: app)
|
||||||
|
}
|
||||||
|
])
|
||||||
|
item.killAppClosure = {[weak self] in
|
||||||
|
self?.handleLongPress(item: app)
|
||||||
}
|
}
|
||||||
|
|
||||||
item.removeFromSuperview()
|
|
||||||
|
|
||||||
let dotView = NSView(frame: .zero)
|
|
||||||
dotView.wantsLayer = true
|
|
||||||
if runningAppsIdentifiers.contains(app.bundleIdentifier!) {
|
|
||||||
dotView.layer?.backgroundColor = NSColor.white.cgColor
|
|
||||||
} else {
|
|
||||||
dotView.layer?.backgroundColor = NSColor.black.cgColor
|
|
||||||
}
|
|
||||||
dotView.layer?.cornerRadius = 1.5
|
|
||||||
dotView.setFrameOrigin(NSPoint(x: 17, y: 1))
|
|
||||||
dotView.frame.size = NSSize(width: 3, height: 3)
|
|
||||||
item.addSubview(dotView)
|
|
||||||
|
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
public func didBeginInteracting(with _: NSScrubber) {
|
public func switchToApp(app: DockItem) {
|
||||||
stopTimer()
|
let bundleIdentifier = app.bundleIdentifier
|
||||||
ticks = 0
|
|
||||||
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(checkTimer), userInfo: nil, repeats: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func checkTimer() {
|
|
||||||
ticks += 1
|
|
||||||
|
|
||||||
if ticks == minTicks {
|
|
||||||
HapticFeedback.shared.tap(strong: 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ticks > maxTicks {
|
|
||||||
stopTimer()
|
|
||||||
HapticFeedback.shared.tap(strong: 6)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func stopTimer() {
|
|
||||||
timer?.invalidate()
|
|
||||||
timer = nil
|
|
||||||
lastSelected = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
public func didCancelInteracting(with _: NSScrubber) {
|
|
||||||
stopTimer()
|
|
||||||
}
|
|
||||||
|
|
||||||
public func didFinishInteracting(with scrubber: NSScrubber) {
|
|
||||||
stopTimer()
|
|
||||||
|
|
||||||
if ticks == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ticks >= minTicks && scrubber.selectedIndex > 0 {
|
|
||||||
longPress(selected: scrubber.selectedIndex)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let bundleIdentifier = applications[scrubber.selectedIndex].bundleIdentifier
|
|
||||||
if bundleIdentifier!.contains("file://") {
|
if bundleIdentifier!.contains("file://") {
|
||||||
NSWorkspace.shared.openFile(bundleIdentifier!.replacingOccurrences(of: "file://", with: ""))
|
NSWorkspace.shared.openFile(bundleIdentifier!.replacingOccurrences(of: "file://", with: ""))
|
||||||
} else {
|
} else {
|
||||||
NSWorkspace.shared.launchApplication(withBundleIdentifier: bundleIdentifier!, options: [.default], additionalEventParamDescriptor: nil, launchIdentifier: nil)
|
NSWorkspace.shared.launchApplication(withBundleIdentifier: bundleIdentifier!, options: [.default], additionalEventParamDescriptor: nil, launchIdentifier: nil)
|
||||||
HapticFeedback.shared.tap(strong: 6)
|
|
||||||
}
|
}
|
||||||
updateRunningApplication()
|
softReloadItems()
|
||||||
|
|
||||||
// NB: if you can't open app which on another space, try to check mark
|
// 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"
|
// "When switching to an application, switch to a Space with open windows for the application"
|
||||||
// in Mission control settings
|
// in Mission control settings
|
||||||
}
|
}
|
||||||
|
|
||||||
private func longPress(selected: Int) {
|
//todo
|
||||||
let bundleIdentifier = applications[selected].bundleIdentifier
|
private func handleLongPress(item: DockItem) {
|
||||||
|
if let pid = item.pid, let app = NSRunningApplication(processIdentifier: pid) {
|
||||||
if ticks > maxTicks {
|
if !app.terminate() {
|
||||||
if let processIdentifier = applications[selected].pid {
|
app.forceTerminate()
|
||||||
if !(NSRunningApplication(processIdentifier: processIdentifier)?.terminate())! {
|
|
||||||
NSRunningApplication(processIdentifier: processIdentifier)?.forceTerminate()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
hardReloadItems()
|
||||||
HapticFeedback.shared.tap(strong: 6)
|
|
||||||
if let index = self.persistentAppIdentifiers.index(of: bundleIdentifier!) {
|
|
||||||
persistentAppIdentifiers.remove(at: index)
|
|
||||||
} else {
|
|
||||||
persistentAppIdentifiers.append(bundleIdentifier!)
|
|
||||||
}
|
|
||||||
|
|
||||||
UserDefaults.standard.set(persistentAppIdentifiers, forKey: "com.toxblh.mtmr.dock.persistent")
|
|
||||||
UserDefaults.standard.synchronize()
|
|
||||||
}
|
}
|
||||||
ticks = 0
|
|
||||||
updateRunningApplication()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func handleHalfLongPress(item: DockItem) {
|
||||||
|
if let index = self.persistentAppIdentifiers.firstIndex(of: item.bundleIdentifier) {
|
||||||
|
persistentAppIdentifiers.remove(at: index)
|
||||||
|
hardReloadItems()
|
||||||
|
} else {
|
||||||
|
persistentAppIdentifiers.append(item.bundleIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
AppSettings.dockPersistentAppIds = persistentAppIdentifiers
|
||||||
|
}
|
||||||
|
|
||||||
private func launchedApplications() -> [DockItem] {
|
private func launchedApplications() -> [DockItem] {
|
||||||
runningAppsIdentifiers = []
|
runningAppsIdentifiers = []
|
||||||
var returnable: [DockItem] = []
|
var returnable: [DockItem] = []
|
||||||
for app in NSWorkspace.shared.runningApplications {
|
for app in NSWorkspace.shared.runningApplications {
|
||||||
guard app.activationPolicy == NSApplication.ActivationPolicy.regular else { continue }
|
guard app.activationPolicy == NSApplication.ActivationPolicy.regular else { continue }
|
||||||
guard let bundleIdentifier = app.bundleIdentifier else { continue }
|
guard let bundleIdentifier = app.bundleIdentifier else { continue }
|
||||||
|
if let filter = self.filter,
|
||||||
|
let name = app.localizedName,
|
||||||
|
filter.numberOfMatches(in: name, options: [], range: NSRange(location: 0, length: name.count)) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
runningAppsIdentifiers.append(bundleIdentifier)
|
runningAppsIdentifiers.append(bundleIdentifier)
|
||||||
|
|
||||||
let dockItem = DockItem(bundleIdentifier: bundleIdentifier, icon: getIcon(forBundleIdentifier: bundleIdentifier), pid: app.processIdentifier)
|
let dockItem = DockItem(bundleIdentifier: bundleIdentifier, icon: app.icon ?? getIcon(forBundleIdentifier: bundleIdentifier), pid: app.processIdentifier)
|
||||||
returnable.append(dockItem)
|
returnable.append(dockItem)
|
||||||
}
|
}
|
||||||
return returnable
|
return returnable
|
||||||
}
|
}
|
||||||
|
|
||||||
public func getIcon(forBundleIdentifier bundleIdentifier: String? = nil, orPath path: String? = nil, orType _: String? = nil) -> NSImage {
|
public func getIcon(forBundleIdentifier bundleIdentifier: String? = nil, orPath path: String? = nil) -> NSImage {
|
||||||
if bundleIdentifier != nil {
|
if let bundleIdentifier = bundleIdentifier, let appPath = NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: bundleIdentifier) {
|
||||||
if let appPath = NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: bundleIdentifier!) {
|
return NSWorkspace.shared.icon(forFile: appPath)
|
||||||
return NSWorkspace.shared.icon(forFile: appPath)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if path != nil {
|
if let path = path {
|
||||||
return NSWorkspace.shared.icon(forFile: path!)
|
return NSWorkspace.shared.icon(forFile: path)
|
||||||
}
|
}
|
||||||
|
|
||||||
let genericIcon = NSImage(contentsOfFile: "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericDocumentIcon.icns")
|
let genericIcon = NSImage(contentsOfFile: "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericDocumentIcon.icns")
|
||||||
@ -269,3 +189,60 @@ public class DockItem: NSObject {
|
|||||||
self.pid = pid
|
self.pid = pid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let iconWidth = 32.0
|
||||||
|
class DockBarItem: CustomButtonTouchBarItem {
|
||||||
|
let dotView = NSView(frame: .zero)
|
||||||
|
let dockItem: DockItem
|
||||||
|
fileprivate var killGestureRecognizer: LongPressGestureRecognizer!
|
||||||
|
var killAppClosure: () -> Void = { }
|
||||||
|
|
||||||
|
var isRunning = false {
|
||||||
|
didSet {
|
||||||
|
redrawDotView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isFrontmost = false {
|
||||||
|
didSet {
|
||||||
|
redrawDotView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ app: DockItem) {
|
||||||
|
self.dockItem = app
|
||||||
|
super.init(identifier: .init(app.bundleIdentifier), title: "")
|
||||||
|
dotView.wantsLayer = true
|
||||||
|
|
||||||
|
image = app.icon
|
||||||
|
image?.size = NSSize(width: iconWidth, height: iconWidth)
|
||||||
|
|
||||||
|
killGestureRecognizer = LongPressGestureRecognizer(target: self, action: #selector(firePanGestureRecognizer))
|
||||||
|
killGestureRecognizer.allowedTouchTypes = .direct
|
||||||
|
killGestureRecognizer.recognizeTimeout = 1.5
|
||||||
|
killGestureRecognizer.minimumPressDuration = 1.5
|
||||||
|
killGestureRecognizer.isEnabled = isRunning
|
||||||
|
|
||||||
|
self.finishViewConfiguration = { [weak self] in
|
||||||
|
guard let selfie = self else { return }
|
||||||
|
selfie.dotView.layer?.cornerRadius = 1.5
|
||||||
|
selfie.view.addSubview(selfie.dotView)
|
||||||
|
selfie.redrawDotView()
|
||||||
|
selfie.view.addGestureRecognizer(selfie.killGestureRecognizer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func redrawDotView() {
|
||||||
|
dotView.layer?.backgroundColor = isRunning ? NSColor.white.cgColor : NSColor.clear.cgColor
|
||||||
|
dotView.frame.size = NSSize(width: isFrontmost ? iconWidth - 14 : 3, height: 3)
|
||||||
|
dotView.setFrameOrigin(NSPoint(x: 18.0 - Double(dotView.frame.size.width) / 2.0, y: iconWidth - 5))
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func firePanGestureRecognizer() {
|
||||||
|
self.killAppClosure()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder _: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
82
MTMR/Widgets/CPUBarItem.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,6 +15,9 @@ class CurrencyBarItem: CustomButtonTouchBarItem {
|
|||||||
private var postfix: String
|
private var postfix: String
|
||||||
private var from: String
|
private var from: String
|
||||||
private var to: String
|
private var to: String
|
||||||
|
private var decimal: Int
|
||||||
|
private var decimalValue: Float32!
|
||||||
|
private var decimalString: String!
|
||||||
private var oldValue: Float32!
|
private var oldValue: Float32!
|
||||||
private var full: Bool = false
|
private var full: Bool = false
|
||||||
|
|
||||||
@ -32,10 +35,39 @@ class CurrencyBarItem: CustomButtonTouchBarItem {
|
|||||||
"IDR": "Rp",
|
"IDR": "Rp",
|
||||||
"MXN": "$",
|
"MXN": "$",
|
||||||
"SGD": "$",
|
"SGD": "$",
|
||||||
"CHF": "Fr.",
|
|
||||||
"BTC": "฿",
|
"BTC": "฿",
|
||||||
"LTC": "Ł",
|
"LTC": "Ł",
|
||||||
"ETH": "Ξ",
|
"ETH": "Ξ",
|
||||||
|
"SOL": "◎",
|
||||||
|
"DOT": "●",
|
||||||
|
"DOGE": "Ð",
|
||||||
|
"XMR": "ɱ",
|
||||||
|
"ADA": "₳",
|
||||||
|
"PLN": "zł",
|
||||||
|
"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) {
|
init(identifier: NSTouchBarItem.Identifier, interval: TimeInterval, from: String, to: String, full: Bool) {
|
||||||
@ -57,6 +89,14 @@ class CurrencyBarItem: CustomButtonTouchBarItem {
|
|||||||
postfix = to
|
postfix = to
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if let decimal = decimals[to] {
|
||||||
|
self.decimal = decimal
|
||||||
|
} else {
|
||||||
|
decimal = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
super.init(identifier: identifier, title: "⏳")
|
super.init(identifier: identifier, title: "⏳")
|
||||||
|
|
||||||
activity.repeats = true
|
activity.repeats = true
|
||||||
@ -114,10 +154,12 @@ class CurrencyBarItem: CustomButtonTouchBarItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
oldValue = value
|
oldValue = value
|
||||||
|
decimalValue = (value * pow(10,Float(decimal))).rounded() / pow(10,Float(decimal))
|
||||||
|
decimalString = String(decimalValue)
|
||||||
|
|
||||||
var title = ""
|
var title = ""
|
||||||
if full {
|
if full {
|
||||||
title = String(format: "%@‣%.2f%@", prefix, value, postfix)
|
title = String(format: "%@%@‣%@", prefix, postfix, decimalString)
|
||||||
} else {
|
} else {
|
||||||
title = String(format: "%@%.2f", prefix, value)
|
title = String(format: "%@%.2f", prefix, value)
|
||||||
}
|
}
|
||||||
@ -127,4 +169,8 @@ class CurrencyBarItem: CustomButtonTouchBarItem {
|
|||||||
newTitle.setAlignment(.center, range: NSRange(location: 0, length: title.count))
|
newTitle.setAlignment(.center, range: NSRange(location: 0, length: title.count))
|
||||||
attributedTitle = newTitle
|
attributedTitle = newTitle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
activity.invalidate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ class DarkModeBarItem: CustomButtonTouchBarItem, Widget {
|
|||||||
isBordered = false
|
isBordered = false
|
||||||
setWidth(value: 24)
|
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)
|
timer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(refresh), userInfo: nil, repeats: true)
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ struct DarkMode {
|
|||||||
|
|
||||||
static func toggle(force: Bool? = nil) {
|
static func toggle(force: Bool? = nil) {
|
||||||
let value = force.map(String.init) ?? "not dark mode"
|
let value = force.map(String.init) ?? "not dark mode"
|
||||||
runAppleScript("\(prefix) set dark mode to \(value)")
|
_ = runAppleScript("\(prefix) set dark mode to \(value)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@ class DnDBarItem: CustomButtonTouchBarItem {
|
|||||||
isBordered = false
|
isBordered = false
|
||||||
setWidth(value: 32)
|
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)
|
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(refresh), userInfo: nil, repeats: true)
|
||||||
|
|
||||||
|
|||||||
@ -52,7 +52,7 @@ class GroupBarItem: NSPopoverTouchBarItem, NSTouchBarDelegate {
|
|||||||
TouchBarController.shared.touchBar.defaultItemIdentifiers = []
|
TouchBarController.shared.touchBar.defaultItemIdentifiers = []
|
||||||
TouchBarController.shared.touchBar.defaultItemIdentifiers = leftIdentifiers + [centerScrollArea] + rightIdentifiers
|
TouchBarController.shared.touchBar.defaultItemIdentifiers = leftIdentifiers + [centerScrollArea] + rightIdentifiers
|
||||||
|
|
||||||
if TouchBarController.shared.showControlStripState {
|
if AppSettings.showControlStripState {
|
||||||
presentSystemModal(TouchBarController.shared.touchBar, systemTrayItemIdentifier: .controlStripItem)
|
presentSystemModal(TouchBarController.shared.touchBar, systemTrayItemIdentifier: .controlStripItem)
|
||||||
} else {
|
} else {
|
||||||
presentSystemModal(TouchBarController.shared.touchBar, placement: 1, systemTrayItemIdentifier: .controlStripItem)
|
presentSystemModal(TouchBarController.shared.touchBar, placement: 1, systemTrayItemIdentifier: .controlStripItem)
|
||||||
|
|||||||
@ -18,9 +18,10 @@ class InputSourceBarItem: CustomButtonTouchBarItem {
|
|||||||
|
|
||||||
observeIputSourceChangedNotification()
|
observeIputSourceChangedNotification()
|
||||||
textInputSourceDidChange()
|
textInputSourceDidChange()
|
||||||
tapClosure = { [weak self] in
|
|
||||||
|
actions.append(ItemAction(trigger: .singleTap) { [weak self] in
|
||||||
self?.switchInputSource()
|
self?.switchInputSource()
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder _: NSCoder) {
|
required init?(coder _: NSCoder) {
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import ScriptingBridge
|
|||||||
|
|
||||||
class MusicBarItem: CustomButtonTouchBarItem {
|
class MusicBarItem: CustomButtonTouchBarItem {
|
||||||
private enum Player: String {
|
private enum Player: String {
|
||||||
|
case Music = "com.apple.Music"
|
||||||
case iTunes = "com.apple.iTunes"
|
case iTunes = "com.apple.iTunes"
|
||||||
case Spotify = "com.spotify.client"
|
case Spotify = "com.spotify.client"
|
||||||
case VOX = "com.coppertino.Vox"
|
case VOX = "com.coppertino.Vox"
|
||||||
@ -19,6 +20,7 @@ class MusicBarItem: CustomButtonTouchBarItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private let playerBundleIdentifiers = [
|
private let playerBundleIdentifiers = [
|
||||||
|
Player.Music,
|
||||||
Player.iTunes,
|
Player.iTunes,
|
||||||
Player.Spotify,
|
Player.Spotify,
|
||||||
Player.VOX,
|
Player.VOX,
|
||||||
@ -39,9 +41,12 @@ class MusicBarItem: CustomButtonTouchBarItem {
|
|||||||
|
|
||||||
super.init(identifier: identifier, title: "⏳")
|
super.init(identifier: identifier, title: "⏳")
|
||||||
isBordered = false
|
isBordered = false
|
||||||
|
|
||||||
tapClosure = { [weak self] in self?.playPause() }
|
actions = [
|
||||||
longTapClosure = { [weak self] in self?.nextTrack() }
|
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()
|
refreshAndSchedule()
|
||||||
}
|
}
|
||||||
@ -71,6 +76,10 @@ class MusicBarItem: CustomButtonTouchBarItem {
|
|||||||
let mp = (musicPlayer as iTunesApplication)
|
let mp = (musicPlayer as iTunesApplication)
|
||||||
mp.playpause!()
|
mp.playpause!()
|
||||||
return
|
return
|
||||||
|
} else if ident == .Music {
|
||||||
|
let mp = (musicPlayer as MusicApplication)
|
||||||
|
mp.playpause!()
|
||||||
|
return
|
||||||
} else if ident == .VOX {
|
} else if ident == .VOX {
|
||||||
let mp = (musicPlayer as VoxApplication)
|
let mp = (musicPlayer as VoxApplication)
|
||||||
mp.playpause!()
|
mp.playpause!()
|
||||||
@ -134,6 +143,11 @@ class MusicBarItem: CustomButtonTouchBarItem {
|
|||||||
mp.nextTrack!()
|
mp.nextTrack!()
|
||||||
updatePlayer()
|
updatePlayer()
|
||||||
return
|
return
|
||||||
|
} else if ident == .Music {
|
||||||
|
let mp = (musicPlayer as MusicApplication)
|
||||||
|
mp.nextTrack!()
|
||||||
|
updatePlayer()
|
||||||
|
return
|
||||||
} else if ident == .VOX {
|
} else if ident == .VOX {
|
||||||
let mp = (musicPlayer as VoxApplication)
|
let mp = (musicPlayer as VoxApplication)
|
||||||
mp.next!()
|
mp.next!()
|
||||||
@ -166,6 +180,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() {
|
func refreshAndSchedule() {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
@ -180,7 +219,7 @@ class MusicBarItem: CustomButtonTouchBarItem {
|
|||||||
var iconUpdated = false
|
var iconUpdated = false
|
||||||
var titleUpdated = false
|
var titleUpdated = false
|
||||||
|
|
||||||
for var ident in playerBundleIdentifiers {
|
for ident in playerBundleIdentifiers {
|
||||||
if let musicPlayer = SBApplication(bundleIdentifier: ident.rawValue) {
|
if let musicPlayer = SBApplication(bundleIdentifier: ident.rawValue) {
|
||||||
if musicPlayer.isRunning {
|
if musicPlayer.isRunning {
|
||||||
var tempTitle = ""
|
var tempTitle = ""
|
||||||
@ -188,6 +227,8 @@ class MusicBarItem: CustomButtonTouchBarItem {
|
|||||||
tempTitle = (musicPlayer as SpotifyApplication).title
|
tempTitle = (musicPlayer as SpotifyApplication).title
|
||||||
} else if ident == .iTunes {
|
} else if ident == .iTunes {
|
||||||
tempTitle = (musicPlayer as iTunesApplication).title
|
tempTitle = (musicPlayer as iTunesApplication).title
|
||||||
|
} else if ident == .Music {
|
||||||
|
tempTitle = (musicPlayer as MusicApplication).title
|
||||||
} else if ident == .VOX {
|
} else if ident == .VOX {
|
||||||
tempTitle = (musicPlayer as VoxApplication).title
|
tempTitle = (musicPlayer as VoxApplication).title
|
||||||
} else if ident == .Safari {
|
} 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 protocol VoxApplication {
|
||||||
@objc optional func playpause()
|
@objc optional func playpause()
|
||||||
@objc optional func next()
|
@objc optional func next()
|
||||||
|
|||||||
@ -13,9 +13,11 @@ class NetworkBarItem: CustomButtonTouchBarItem, Widget {
|
|||||||
static var identifier: String = "com.toxblh.mtmr.network"
|
static var identifier: String = "com.toxblh.mtmr.network"
|
||||||
|
|
||||||
private let flip: Bool
|
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.flip = flip
|
||||||
|
self.units = units
|
||||||
super.init(identifier: identifier, title: " ")
|
super.init(identifier: identifier, title: " ")
|
||||||
startMonitoringProcess()
|
startMonitoringProcess()
|
||||||
}
|
}
|
||||||
@ -31,7 +33,7 @@ class NetworkBarItem: CustomButtonTouchBarItem, Widget {
|
|||||||
var dSpeed: UInt64?
|
var dSpeed: UInt64?
|
||||||
var uSpeed: UInt64?
|
var uSpeed: UInt64?
|
||||||
var curr: Array<Substring>?
|
var curr: Array<Substring>?
|
||||||
var dataAvailable: NSObjectProtocol!
|
var dataAvailable: NSObjectProtocol?
|
||||||
|
|
||||||
pipe = Pipe()
|
pipe = Pipe()
|
||||||
bandwidthProcess = Process()
|
bandwidthProcess = Process()
|
||||||
@ -64,19 +66,21 @@ class NetworkBarItem: CustomButtonTouchBarItem, Widget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
outputHandle.waitForDataInBackgroundAndNotify()
|
outputHandle.waitForDataInBackgroundAndNotify()
|
||||||
} else {
|
} else if let dataAvailable = dataAvailable {
|
||||||
NotificationCenter.default.removeObserver(dataAvailable)
|
NotificationCenter.default.removeObserver(dataAvailable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var dataReady: NSObjectProtocol!
|
var dataReady: NSObjectProtocol?
|
||||||
dataReady = NotificationCenter.default.addObserver(
|
dataReady = NotificationCenter.default.addObserver(
|
||||||
forName: Process.didTerminateNotification,
|
forName: Process.didTerminateNotification,
|
||||||
object: outputHandle,
|
object: outputHandle,
|
||||||
queue: nil
|
queue: nil
|
||||||
) { _ -> Void in
|
) { _ -> Void in
|
||||||
print("Task terminated!")
|
print("Task terminated!")
|
||||||
NotificationCenter.default.removeObserver(dataReady)
|
if let observer = dataReady {
|
||||||
|
NotificationCenter.default.removeObserver(observer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bandwidthProcess?.launch()
|
bandwidthProcess?.launch()
|
||||||
@ -84,15 +88,42 @@ class NetworkBarItem: CustomButtonTouchBarItem, Widget {
|
|||||||
|
|
||||||
func getHumanizeSize(speed: UInt64) -> String {
|
func getHumanizeSize(speed: UInt64) -> String {
|
||||||
let humanText: String
|
let humanText: String
|
||||||
|
|
||||||
if speed < 1024 {
|
func speedB(speed: UInt64)-> String {
|
||||||
humanText = String(format: "%.0f", Double(speed)) + " B/s"
|
return String(format: "%.0f", Double(speed)) + " B/s"
|
||||||
} else if speed < (1024 * 1024) {
|
}
|
||||||
humanText = String(format: "%.1f", Double(speed) / 1024) + " KB/s"
|
|
||||||
} else if speed < (1024 * 1024 * 1024) {
|
func speedKB(speed: UInt64)-> String {
|
||||||
humanText = String(format: "%.1f", Double(speed) / (1024 * 1024)) + " MB/s"
|
return String(format: "%.1f", Double(speed) / 1024) + " KB/s"
|
||||||
} else {
|
}
|
||||||
humanText = String(format: "%.2f", Double(speed) / (1024 * 1024 * 1024)) + " GB/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 = speedB(speed: speed)
|
||||||
|
} else if speed < (1024 * 1024) {
|
||||||
|
humanText = speedKB(speed: speed)
|
||||||
|
} else if speed < (1024 * 1024 * 1024) {
|
||||||
|
humanText = speedMB(speed: speed)
|
||||||
|
} else {
|
||||||
|
humanText = speedGB(speed: speed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return humanText
|
return humanText
|
||||||
|
|||||||
@ -30,8 +30,8 @@ class NightShiftBarItem: CustomButtonTouchBarItem {
|
|||||||
super.init(identifier: identifier, title: "")
|
super.init(identifier: identifier, title: "")
|
||||||
isBordered = false
|
isBordered = false
|
||||||
setWidth(value: 28)
|
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)
|
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(refresh), userInfo: nil, repeats: true)
|
||||||
|
|
||||||
|
|||||||
@ -23,8 +23,9 @@ class PomodoroBarItem: CustomButtonTouchBarItem, Widget {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
item: .pomodoro(workTime: workTime ?? 1500.00, restTime: restTime ?? 300),
|
item: .pomodoro(workTime: workTime ?? 1500.00, restTime: restTime ?? 300),
|
||||||
action: .none,
|
actions: [],
|
||||||
longAction: .none,
|
legacyAction: .none,
|
||||||
|
legacyLongAction: .none,
|
||||||
parameters: [:]
|
parameters: [:]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -35,7 +36,7 @@ class PomodoroBarItem: CustomButtonTouchBarItem, Widget {
|
|||||||
case none
|
case none
|
||||||
}
|
}
|
||||||
|
|
||||||
private let defaultTitle = "🍅"
|
private let defaultTitle = "🍅 "
|
||||||
private let workTime: TimeInterval
|
private let workTime: TimeInterval
|
||||||
private let restTime: TimeInterval
|
private let restTime: TimeInterval
|
||||||
private var typeTime: TimeTypes = .none
|
private var typeTime: TimeTypes = .none
|
||||||
@ -50,8 +51,10 @@ class PomodoroBarItem: CustomButtonTouchBarItem, Widget {
|
|||||||
self.workTime = workTime
|
self.workTime = workTime
|
||||||
self.restTime = restTime
|
self.restTime = restTime
|
||||||
super.init(identifier: identifier, title: defaultTitle)
|
super.init(identifier: identifier, title: defaultTitle)
|
||||||
tapClosure = { [weak self] in self?.startStopWork() }
|
actions.append(contentsOf: [
|
||||||
longTapClosure = { [weak self] in self?.startStopRest() }
|
ItemAction(trigger: .singleTap) { [weak self] in self?.startStopWork() },
|
||||||
|
ItemAction(trigger: .longTap) { [weak self] in self?.startStopRest() }
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder _: NSCoder) {
|
required init?(coder _: NSCoder) {
|
||||||
|
|||||||
@ -4,8 +4,11 @@ class TimeTouchBarItem: CustomButtonTouchBarItem {
|
|||||||
private let dateFormatter = DateFormatter()
|
private let dateFormatter = DateFormatter()
|
||||||
private var timer: Timer!
|
private var timer: Timer!
|
||||||
|
|
||||||
init(identifier: NSTouchBarItem.Identifier, formatTemplate: String, timeZone: String? = nil) {
|
init(identifier: NSTouchBarItem.Identifier, formatTemplate: String, timeZone: String? = nil, locale: String? = nil) {
|
||||||
dateFormatter.dateFormat = formatTemplate
|
dateFormatter.dateFormat = formatTemplate
|
||||||
|
if let locale = locale {
|
||||||
|
dateFormatter.locale = Locale(identifier: locale)
|
||||||
|
}
|
||||||
if let abbr = timeZone {
|
if let abbr = timeZone {
|
||||||
dateFormatter.timeZone = TimeZone(abbreviation: abbr)
|
dateFormatter.timeZone = TimeZone(abbreviation: abbr)
|
||||||
}
|
}
|
||||||
|
|||||||
256
MTMR/Widgets/UpNextScrubberTouchBarItem.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,18 +5,11 @@ import CoreAudio
|
|||||||
|
|
||||||
class VolumeViewController: NSCustomTouchBarItem {
|
class VolumeViewController: NSCustomTouchBarItem {
|
||||||
private(set) var sliderItem: CustomSlider!
|
private(set) var sliderItem: CustomSlider!
|
||||||
|
private var currentDeviceId: AudioObjectID = AudioObjectID(0)
|
||||||
|
|
||||||
init(identifier: NSTouchBarItem.Identifier, image: NSImage? = nil) {
|
init(identifier: NSTouchBarItem.Identifier, image: NSImage? = nil) {
|
||||||
super.init(identifier: identifier)
|
super.init(identifier: identifier)
|
||||||
|
|
||||||
var forPropertyAddress = AudioObjectPropertyAddress(
|
|
||||||
mSelector: kAudioHardwareServiceDeviceProperty_VirtualMasterVolume,
|
|
||||||
mScope: kAudioDevicePropertyScopeOutput,
|
|
||||||
mElement: kAudioObjectPropertyElementMaster
|
|
||||||
)
|
|
||||||
|
|
||||||
AudioObjectAddPropertyListenerBlock(defaultDeviceID, &forPropertyAddress, nil, audioObjectPropertyListenerBlock)
|
|
||||||
|
|
||||||
if image == nil {
|
if image == nil {
|
||||||
sliderItem = CustomSlider()
|
sliderItem = CustomSlider()
|
||||||
} else {
|
} else {
|
||||||
@ -29,6 +22,49 @@ class VolumeViewController: NSCustomTouchBarItem {
|
|||||||
sliderItem.floatValue = getInputGain() * 100
|
sliderItem.floatValue = getInputGain() * 100
|
||||||
|
|
||||||
view = sliderItem
|
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>) {
|
func audioObjectPropertyListenerBlock(numberAddresses _: UInt32, addresses _: UnsafePointer<AudioObjectPropertyAddress>) {
|
||||||
@ -66,7 +102,7 @@ class VolumeViewController: NSCustomTouchBarItem {
|
|||||||
var volume: Float32 = 0.5
|
var volume: Float32 = 0.5
|
||||||
var size: UInt32 = UInt32(MemoryLayout.size(ofValue: volume))
|
var size: UInt32 = UInt32(MemoryLayout.size(ofValue: volume))
|
||||||
var address: AudioObjectPropertyAddress = AudioObjectPropertyAddress()
|
var address: AudioObjectPropertyAddress = AudioObjectPropertyAddress()
|
||||||
address.mSelector = AudioObjectPropertySelector(kAudioHardwareServiceDeviceProperty_VirtualMasterVolume)
|
address.mSelector = AudioObjectPropertySelector(kAudioHardwareServiceDeviceProperty_VirtualMainVolume)
|
||||||
address.mScope = AudioObjectPropertyScope(kAudioDevicePropertyScopeOutput)
|
address.mScope = AudioObjectPropertyScope(kAudioDevicePropertyScopeOutput)
|
||||||
address.mElement = AudioObjectPropertyElement(kAudioObjectPropertyElementMaster)
|
address.mElement = AudioObjectPropertyElement(kAudioObjectPropertyElementMaster)
|
||||||
AudioObjectGetPropertyData(defaultDeviceID, &address, 0, nil, &size, &volume)
|
AudioObjectGetPropertyData(defaultDeviceID, &address, 0, nil, &size, &volume)
|
||||||
@ -86,7 +122,7 @@ class VolumeViewController: NSCustomTouchBarItem {
|
|||||||
var address: AudioObjectPropertyAddress = AudioObjectPropertyAddress()
|
var address: AudioObjectPropertyAddress = AudioObjectPropertyAddress()
|
||||||
address.mScope = AudioObjectPropertyScope(kAudioDevicePropertyScopeOutput)
|
address.mScope = AudioObjectPropertyScope(kAudioDevicePropertyScopeOutput)
|
||||||
address.mElement = AudioObjectPropertyElement(kAudioObjectPropertyElementMaster)
|
address.mElement = AudioObjectPropertyElement(kAudioObjectPropertyElementMaster)
|
||||||
address.mSelector = AudioObjectPropertySelector(kAudioHardwareServiceDeviceProperty_VirtualMasterVolume)
|
address.mSelector = AudioObjectPropertySelector(kAudioHardwareServiceDeviceProperty_VirtualMainVolume)
|
||||||
return AudioObjectSetPropertyData(defaultDeviceID, &address, 0, nil, size, &inputVolume)
|
return AudioObjectSetPropertyData(defaultDeviceID, &address, 0, nil, size, &inputVolume)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -135,4 +135,8 @@ class WeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate {
|
|||||||
// print("inside didChangeAuthorization ");
|
// print("inside didChangeAuthorization ");
|
||||||
updateWeather()
|
updateWeather()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
activity.invalidate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,28 +12,45 @@ import CoreLocation
|
|||||||
class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate {
|
class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate {
|
||||||
private let activity: NSBackgroundActivityScheduler
|
private let activity: NSBackgroundActivityScheduler
|
||||||
private let unitsStr = "°C"
|
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 location: CLLocation!
|
||||||
private var prevLocation: CLLocation!
|
private var prevLocation: CLLocation!
|
||||||
private var manager: CLLocationManager!
|
private var manager: CLLocationManager!
|
||||||
|
private var updateWeatherTask: URLSessionDataTask?
|
||||||
|
|
||||||
init(identifier: NSTouchBarItem.Identifier, interval: TimeInterval) {
|
init(identifier: NSTouchBarItem.Identifier, interval: TimeInterval) {
|
||||||
activity = NSBackgroundActivityScheduler(identifier: "\(identifier.rawValue).updatecheck")
|
activity = NSBackgroundActivityScheduler(identifier: "\(identifier.rawValue).updatecheck")
|
||||||
activity.interval = interval
|
activity.interval = interval
|
||||||
|
|
||||||
super.init(identifier: identifier, title: "⏳")
|
super.init(identifier: identifier, title: "⏳")
|
||||||
|
|
||||||
let status = CLLocationManager.authorizationStatus()
|
let status = CLLocationManager.authorizationStatus()
|
||||||
if status == .restricted || status == .denied {
|
if status == .restricted || status == .denied {
|
||||||
print("User permission not given")
|
print("User permission not given")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !CLLocationManager.locationServicesEnabled() {
|
if !CLLocationManager.locationServicesEnabled() {
|
||||||
print("Location services not enabled")
|
print("Location services not enabled")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
activity.repeats = true
|
activity.repeats = true
|
||||||
activity.qualityOfService = .utility
|
activity.qualityOfService = .utility
|
||||||
activity.schedule { (completion: NSBackgroundActivityScheduler.CompletionHandler) in
|
activity.schedule { (completion: NSBackgroundActivityScheduler.CompletionHandler) in
|
||||||
@ -41,70 +58,76 @@ class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate
|
|||||||
completion(NSBackgroundActivityScheduler.Result.finished)
|
completion(NSBackgroundActivityScheduler.Result.finished)
|
||||||
}
|
}
|
||||||
updateWeather()
|
updateWeather()
|
||||||
|
|
||||||
manager = CLLocationManager()
|
manager = CLLocationManager()
|
||||||
manager.delegate = self
|
manager.delegate = self
|
||||||
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
|
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
|
||||||
manager.startUpdatingLocation()
|
manager.startUpdatingLocation()
|
||||||
|
|
||||||
tapClosure = tapClosure ?? defaultTapAction
|
if actions.filter({ $0.trigger == .singleTap }).isEmpty {
|
||||||
|
actions.append(ItemAction(
|
||||||
|
trigger: .singleTap,
|
||||||
|
defaultTapAction
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder _: NSCoder) {
|
required init?(coder _: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func updateWeather() {
|
@objc func updateWeather() {
|
||||||
var urlRequest = URLRequest(url: URL(string: getWeatherUrl())!)
|
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
|
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 {
|
guard error == nil, let response = data?.utf8string else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// print(response)
|
// print(response)
|
||||||
|
|
||||||
var matches: [[String]]
|
var matches: [[String]]
|
||||||
var temperature: String?
|
var temperature: String?
|
||||||
matches = response.matchingStrings(regex: "fact__temp.*?temp__value.*?>(.*?)<")
|
matches = response.matchingStrings(regex: "fact__temp.*?temp__value.*?>(.*?)<")
|
||||||
temperature = matches.first?.item(at: 1)
|
temperature = matches.first?.item(at: 1)
|
||||||
|
|
||||||
var icon: String?
|
var icon: String?
|
||||||
matches = response.matchingStrings(regex: "link__condition.*?>(.*?)<")
|
matches = response.matchingStrings(regex: "\"condition\":\"(.*?)\"")
|
||||||
icon = matches.first?.item(at: 1)
|
icon = matches.first?.item(at: 1)
|
||||||
if let _ = icon, let test = self.iconsSource[icon!] {
|
if let _ = icon, let test = self.iconsSource[icon!] {
|
||||||
icon = test
|
icon = test
|
||||||
}
|
}
|
||||||
|
|
||||||
if temperature != nil {
|
if temperature != nil {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.setWeather(text: "\(icon ?? "?") \(temperature!)\(self.unitsStr)")
|
self.setWeather(text: "\(icon ?? "?") \(temperature!)\(self.unitsStr)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task.resume()
|
updateWeatherTask?.resume()
|
||||||
}
|
}
|
||||||
|
|
||||||
func getWeatherUrl() -> String {
|
func getWeatherUrl() -> String {
|
||||||
if location != nil {
|
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 {
|
} 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setWeather(text: String) {
|
func setWeather(text: String) {
|
||||||
title = text
|
title = text
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultTapAction() {
|
func defaultTapAction() {
|
||||||
print(getWeatherUrl())
|
print(getWeatherUrl())
|
||||||
if let url = URL(string: getWeatherUrl()) {
|
if let url = URL(string: getWeatherUrl()) {
|
||||||
NSWorkspace.shared.open(url)
|
NSWorkspace.shared.open(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||||
let lastLocation = locations.last!
|
let lastLocation = locations.last!
|
||||||
location = lastLocation
|
location = lastLocation
|
||||||
@ -113,23 +136,27 @@ class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate
|
|||||||
}
|
}
|
||||||
prevLocation = lastLocation
|
prevLocation = lastLocation
|
||||||
}
|
}
|
||||||
|
|
||||||
func locationManager(_: CLLocationManager, didFailWithError error: Error) {
|
func locationManager(_: CLLocationManager, didFailWithError error: Error) {
|
||||||
print(error)
|
print(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func locationManager(_: CLLocationManager, didChangeAuthorization _: CLAuthorizationStatus) {
|
func locationManager(_: CLLocationManager, didChangeAuthorization _: CLAuthorizationStatus) {
|
||||||
updateWeather()
|
updateWeather()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
activity.invalidate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
func matchingStrings(regex: String) -> [[String]] {
|
func matchingStrings(regex: String) -> [[String]] {
|
||||||
guard let regex = try? NSRegularExpression(pattern: regex, options: []) else { return [] }
|
guard let regex = try? NSRegularExpression(pattern: regex, options: []) else { return [] }
|
||||||
let nsString = self as NSString
|
let nsString = self as NSString
|
||||||
let results = regex.matches(in: self, options: [], range: NSMakeRange(0, nsString.length))
|
let results = regex.matches(in: self, options: [], range: NSMakeRange(0, nsString.length))
|
||||||
return results.map { result in
|
return results.map { result in
|
||||||
(0..<result.numberOfRanges).map {
|
(0 ..< result.numberOfRanges).map {
|
||||||
result.range(at: $0).location != NSNotFound
|
result.range(at: $0).location != NSNotFound
|
||||||
? nsString.substring(with: result.range(at: $0))
|
? nsString.substring(with: result.range(at: $0))
|
||||||
: ""
|
: ""
|
||||||
|
|||||||
@ -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
|
// iTunes
|
||||||
{
|
{
|
||||||
"type": "appleScriptTitledButton",
|
"type": "appleScriptTitledButton",
|
||||||
|
|||||||
@ -6,7 +6,7 @@ class AppleScriptDefinitionTests: XCTestCase {
|
|||||||
[ { "type": "appleScriptTitledButton", "source": { "inline": "tell everything fine" } } ]
|
[ { "type": "appleScriptTitledButton", "source": { "inline": "tell everything fine" } } ]
|
||||||
""".data(using: .utf8)!
|
""".data(using: .utf8)!
|
||||||
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonNoActionFixture)
|
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()
|
XCTFail()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -18,7 +18,7 @@ class AppleScriptDefinitionTests: XCTestCase {
|
|||||||
[ { "type": "appleScriptTitledButton", "source": { "filePath": "/ololo/pew" } } ]
|
[ { "type": "appleScriptTitledButton", "source": { "filePath": "/ololo/pew" } } ]
|
||||||
""".data(using: .utf8)!
|
""".data(using: .utf8)!
|
||||||
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonNoActionFixture)
|
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()
|
XCTFail()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -26,12 +26,39 @@ class AppleScriptDefinitionTests: XCTestCase {
|
|||||||
XCTAssertEqual(sourceStruct?.filePath, "/ololo/pew")
|
XCTAssertEqual(sourceStruct?.filePath, "/ololo/pew")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This tests that users can pass paths to files with ~ in them
|
||||||
|
func testUserPath() {
|
||||||
|
let buttonNoActionFixture = """
|
||||||
|
[ { "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 {
|
||||||
|
XCTFail()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let sourceStruct = source as? Source
|
||||||
|
// gives you a string in the format of file:///Users/your_uer_name/pew
|
||||||
|
let expandedPath = URL(fileURLWithPath: NSString("~/pew").expandingTildeInPath) as URL
|
||||||
|
XCTAssertEqual(sourceStruct?.filePath?.fileURL, expandedPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This tests that users can pass paths to images with ~ in them
|
||||||
|
func testUserImagePath() {
|
||||||
|
let relativeImagePath = """
|
||||||
|
[ { "filePath": "~/pew/image.png" } ]
|
||||||
|
""".data(using: .utf8)!
|
||||||
|
let result = try? JSONDecoder().decode([Source].self, from: relativeImagePath)
|
||||||
|
// gives you a string in the format of file:///Users/your_uer_name/pew/image.png
|
||||||
|
let expandedPath = URL(fileURLWithPath: NSString("~/pew/image.png").expandingTildeInPath) as URL
|
||||||
|
XCTAssertEqual(result?.first?.filePath?.fileURL, expandedPath)
|
||||||
|
}
|
||||||
|
|
||||||
func testRefreshInterval() {
|
func testRefreshInterval() {
|
||||||
let buttonNoActionFixture = """
|
let buttonNoActionFixture = """
|
||||||
[ { "type": "appleScriptTitledButton", "source": { "inline": "tell everything fine" }, "refreshInterval": 305} ]
|
[ { "type": "appleScriptTitledButton", "source": { "inline": "tell everything fine" }, "refreshInterval": 305} ]
|
||||||
""".data(using: .utf8)!
|
""".data(using: .utf8)!
|
||||||
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonNoActionFixture)
|
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()
|
XCTFail()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ class ParseConfig: XCTestCase {
|
|||||||
XCTFail()
|
XCTFail()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard case .none? = result?.first?.action else {
|
guard result?.first?.actions.count == 0 else {
|
||||||
XCTFail()
|
XCTFail()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -18,14 +18,29 @@ class ParseConfig: XCTestCase {
|
|||||||
|
|
||||||
func testButtonKeyCodeAction() {
|
func testButtonKeyCodeAction() {
|
||||||
let buttonKeycodeFixture = """
|
let buttonKeycodeFixture = """
|
||||||
[ { "type": "staticButton", "title": "Pew", "action": "hidKey", "keycode": 123} ]
|
[ { "type": "staticButton", "title": "Pew", "actions": [ { "trigger": "singleTap", "action": "hidKey", "keycode": 123 } ] } ]
|
||||||
""".data(using: .utf8)!
|
""".data(using: .utf8)!
|
||||||
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonKeycodeFixture)
|
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonKeycodeFixture)
|
||||||
guard case .staticButton("Pew")? = result?.first?.type else {
|
guard case .staticButton("Pew")? = result?.first?.type else {
|
||||||
XCTFail()
|
XCTFail()
|
||||||
return
|
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()
|
XCTFail()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -40,7 +55,7 @@ class ParseConfig: XCTestCase {
|
|||||||
XCTFail()
|
XCTFail()
|
||||||
return
|
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()
|
XCTFail()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -55,7 +70,7 @@ class ParseConfig: XCTestCase {
|
|||||||
XCTFail()
|
XCTFail()
|
||||||
return
|
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()
|
XCTFail()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
303
README.md
@ -1,4 +1,4 @@
|
|||||||
# My touchbar. My rules. [](https://github.com/Toxblh/MTMR/releases) [](https://github.com/Toxblh/MTMR/blob/master/LICENSE) 
|
# My touchbar. My rules. [](https://github.com/Toxblh/MTMR/releases) [](https://github.com/Toxblh/MTMR/blob/master/LICENSE)  
|
||||||
|
|
||||||
<img src="Resources/logo.png" align="right"
|
<img src="Resources/logo.png" align="right"
|
||||||
title="MTMR by Toxblh" width="110" height="110">
|
title="MTMR by Toxblh" width="110" height="110">
|
||||||
@ -14,11 +14,11 @@ My idea is to create a platform for creating plugins to customize the TouchBar.
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<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>
|
<a href="https://t.me/joinchat/AmVYGg8vW38c13_3MxdE_g"><img height="20px" src="https://telegram.org/img/t_logo.png" /> Telegram</a>
|
||||||
</p>
|
</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.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.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">
|
<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
|
## Installation
|
||||||
|
|
||||||
- Download lastest [release](https://github.com/Toxblh/MTMR/releases) (.dmg) from github
|
- Download latest [release](https://github.com/Toxblh/MTMR/releases) (.dmg) from github
|
||||||
- Or via Homebrew `brew cask install mtmr`
|
- 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
|
- [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">
|
<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">
|
<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">
|
||||||
@ -43,10 +43,7 @@ My idea is to create a platform for creating plugins to customize the TouchBar.
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
- [@Toxblh preset](Resources/toxblh.json)
|
[MTMR presets](https://github.com/Toxblh/MTMR-presets)
|
||||||
- [@ReDetection preset](Resources/ReDetection.json)
|
|
||||||
- [@luongvo209 preset](Resources/luongvo209.json)
|
|
||||||
- [aadi_vs_anand preset](Resources/aadi_vs_anand.json)
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="./Resources/Artboard.png" alt="Presets for touchbar" width="800">
|
<img src="./Resources/Artboard.png" alt="Presets for touchbar" width="800">
|
||||||
@ -54,7 +51,7 @@ My idea is to create a platform for creating plugins to customize the TouchBar.
|
|||||||
|
|
||||||
## Customization
|
## 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:
|
The pre-installed configuration contains less or more than you'll probably want, try to configure:
|
||||||
|
|
||||||
@ -74,7 +71,9 @@ The pre-installed configuration contains less or more than you'll probably want,
|
|||||||
|
|
||||||
> Native Plugins
|
> Native Plugins
|
||||||
|
|
||||||
|
- timeButton
|
||||||
- battery
|
- battery
|
||||||
|
- cpu
|
||||||
- currency
|
- currency
|
||||||
- weather
|
- weather
|
||||||
- yandexWeather
|
- yandexWeather
|
||||||
@ -86,6 +85,7 @@ The pre-installed configuration contains less or more than you'll probably want,
|
|||||||
- darkMode
|
- darkMode
|
||||||
- pomodoro
|
- pomodoro
|
||||||
- network
|
- network
|
||||||
|
- upnext (Calendar events)
|
||||||
|
|
||||||
> Media Keys
|
> Media Keys
|
||||||
|
|
||||||
@ -98,11 +98,37 @@ The pre-installed configuration contains less or more than you'll probably want,
|
|||||||
- sleep
|
- sleep
|
||||||
- displaySleep
|
- displaySleep
|
||||||
|
|
||||||
## Gestures on central part:
|
> Custom buttons
|
||||||
|
|
||||||
|
- staticButton
|
||||||
|
- appleScriptTitledButton
|
||||||
|
- shellScriptTitledButton
|
||||||
|
|
||||||
|
## Gestures
|
||||||
|
|
||||||
|
By default you can enable basic gestures from application menu (status bar -> MTMR icon -> Volume/Brightness gestures):
|
||||||
- two finger slide: change you Volume
|
- two finger slide: change you Volume
|
||||||
- three finger slide: change you Brightness
|
- 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:
|
## Built-in slider types:
|
||||||
|
|
||||||
- brightness
|
- brightness
|
||||||
@ -110,44 +136,103 @@ The pre-installed configuration contains less or more than you'll probably want,
|
|||||||
|
|
||||||
### You can also make custom buttons using these types
|
### You can also make custom buttons using these types
|
||||||
|
|
||||||
- `staticButton`
|
#### `staticButton`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"type": "staticButton",
|
"type": "staticButton",
|
||||||
"title": "esc",
|
"title": "esc",
|
||||||
```
|
```
|
||||||
|
|
||||||
- `appleScriptTitledButton`
|
#### `appleScriptTitledButton`
|
||||||
|
|
||||||
```js
|
```js
|
||||||
|
{
|
||||||
"type": "appleScriptTitledButton",
|
"type": "appleScriptTitledButton",
|
||||||
"refreshInterval": 60, //optional
|
"refreshInterval": 60, //optional
|
||||||
"source": {
|
"source": {
|
||||||
"filePath": "/Users/toxblh/Library/Application Support/MTMR/iTunes.nowPlaying.scpt",
|
"filePath": "~/Library/Application Support/MTMR/iTunes.nowPlaying.scpt",
|
||||||
// or
|
// or
|
||||||
"inline": "tell application \"Finder\"\rmake new Finder window\rset target of front window to path to home folder as string\ractivate\rend tell",
|
"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
|
// or
|
||||||
"base64": "StringInbase64"
|
"base64": "StringInbase64"
|
||||||
},
|
},
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `timeButton`
|
> 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:
|
||||||
```js
|
```js
|
||||||
"type": "timeButton",
|
{
|
||||||
"formatTemplate": "HH:mm" //optional
|
"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",
|
||||||
|
"width": 80,
|
||||||
|
"refreshInterval": 2,
|
||||||
|
"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.
|
||||||
|
// Images will be resized to 24x24.
|
||||||
|
// "filePath": "~/myproject/myimage.jpg" // or "/fixed/path/to/the.png"
|
||||||
|
"base64":
|
||||||
|
"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAA/1BMVEUAAADaACbYACfYACfjABzXACjYACfXACjYACfYACfYACfYACfdACLYACfXACjYACfVACv/AADXACjYACfYACfXACjYACfXACjaACXYACfYACfVACvYACfYACfZACbZACbYACfYACfZACb/AADYACfYACfVACrXACjVACu/AEDYACfYACfYACfXACjXACjYACfXACjYACfYACfYACfXACjYACfXACjYACfYACfZACbYACfYACfMADPYACfYACfYACfYACfYACfZACbXACjYACfYACfRAC7XACjYACfZACbWACnXACjXACjYACfTACzZACb/AADYACfYACfYACcAAAA+zneGAAAAU3RSTlMAItK+CVPjh3xUxPwPiGDQGAMtSKmN3Vk+wPQG/e26oIJBnwJCdiuAHgTmw+6BX+IgfaqLUvKOW8VKnagK+vBwYrhlc/urCznvhSyUbOEXPAFjGh/ektAAAAABYktHRACIBR1IAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH4ggWETQWgEDcSgAAAqVJREFUWMPtl4ly2jAQhsUNNlcw5r4SICEHLSQhCQRyX73T/u//LpUlLIyxbMAznWmn/0ywo5U+27tr7ZoQuwLBUJidRKIxPhKLRtgxHAoGiLfiQIKdKFCTxjGpQmEDCSC+BiAFpNlJBsgaxyyQYQNpIPUf8AcAOzktD+iaoQJQNI5FoMAGdCCv5XZclpfKFXiqUi5Jllf1mvdyQzW96gigd4h6o+mhRp1O0x3vvwa1VSWeqrZU1Jyeogy01ggSVQsoO/i/gjq9/u6u+2LDXq2jshqLHNCgdsCVwO0NILdi0oDmuoAmoImhQDzFRPNnb36L7U43NVfc2EH2D9h5t9OePyIF5IU9uIhvkyN7iiXmQUIOj8x/lB6f0bTaQ3ZA+9iaNCH2Lpg6btsBIRJOpJl0E9ABTvof5kqEGeCjMaN/AnRMgM5XJcI2J1J1gf6S48Tb2Ae6JkAjdgmAeJ1XAOJ1Xg8wGJ6elXwAzkeGjy62BgxG3MuXnoCIkmEq8EQyAUPgajyhPxJAga9SIiRqzwMOuAbGZDrDjQRgKkpiqiPgFphM74B7d4BKy2cyy1RcBvSodUb/HiSAIl+VlEfh8cm4wvPL9nnw+gbc+kkkUVioO95etwe8PBuP8vQoBzg7UQAe5t7syZwoCaMA3AN30wlzh3MYJYkkADeYTckYuJYlkiSVBeCKZtSY/gxlqezlxEt+pdFg6zBesPXn1ih8Aj5vkAels9PhYCkPsl++kg0AQu4dyuqmugIQm+qS5Nv6N+D7wm7d1skPc4xu666Fhd6BxU6r+jub8tNaWNxK29EhsdpR/sVn7FlLm0txPdgni+JrFNd3p+K67MQtyrsp3w2G7xbHd5Plv83z3Wj6b3V9N9ssFv7afaa//ZPn3wD4/vje8PP/N7TebS0hgZhEAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE4LTA4LTIyVDE3OjUyOjIyKzAyOjAwc2qUYAAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxOC0wOC0yMlQxNzo1MjoyMiswMjowMAI3LNwAAAAZdEVYdFNvZnR3YXJlAHd3dy5pbmtzY2FwZS5vcmeb7jwaAAAAAElFTkSuQmCC"
|
||||||
|
},
|
||||||
|
"bordered": false
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Groups
|
## Groups
|
||||||
|
|
||||||
```js
|
```js
|
||||||
{
|
{
|
||||||
"type": "group",
|
"type": "group",
|
||||||
"align": "center",
|
"align": "center",
|
||||||
"bordered": true,
|
"bordered": true,
|
||||||
"title": "stats",
|
"title": "stats",
|
||||||
"items": [
|
"items": [
|
||||||
{ "type": "play" }, { "type": "mute" }, ...]
|
{ "type": "play" },
|
||||||
|
{ "type": "mute" },
|
||||||
|
...
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -155,21 +240,39 @@ To close a group, use the button:
|
|||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
"type": "close",
|
"type": "close",
|
||||||
"width": 64
|
"width": 64
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
## Native plugins
|
## 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`
|
#### `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/
|
||||||
|
|
||||||
|
> locale examples: https://gist.github.com/jacobbubu/1836273
|
||||||
|
|
||||||
```js
|
```js
|
||||||
{
|
{
|
||||||
"type": "timeButton",
|
"type": "timeButton",
|
||||||
"formatTemplate": "dd HH:mm",
|
"formatTemplate": "dd HH:mm",
|
||||||
|
"locale": "en_GB",
|
||||||
"timeZone": "UTC"
|
"timeZone": "UTC"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -177,22 +280,22 @@ To close a group, use the button:
|
|||||||
#### `weather`
|
#### `weather`
|
||||||
|
|
||||||
> Provider: https://openweathermap.org \
|
> Provider: https://openweathermap.org \
|
||||||
> Note: you need to register on https://openweathermap.org to get your API key \
|
> Note: Register at 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: Wait for 20 minutes or so for Openweathermap to activate your API key.\
|
||||||
> 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
|
```js
|
||||||
"type": "weather",
|
"type": "weather",
|
||||||
"refreshInterval": 600, // in seconds
|
"refreshInterval": 600, // in seconds
|
||||||
"units": "metric", // or imperial
|
"units": "metric", // or imperial
|
||||||
"icon_type": "text" // or images
|
"icon_type": "text", // or images
|
||||||
"api_key": "" // you can get the key on openweather
|
"api_key": "" // you can get the key on openweather
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `yandexWeather` (experimental)
|
#### `yandexWeather` (experimental)
|
||||||
|
|
||||||
> Provider: https://yandex.ru/pogoda. One click to open up weather forecast in your browser. \
|
> 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
|
```js
|
||||||
"type": "yandexWeather",
|
"type": "yandexWeather",
|
||||||
@ -227,7 +330,7 @@ To close a group, use the button:
|
|||||||
|
|
||||||
#### `pomodoro`
|
#### `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
|
```js
|
||||||
{
|
{
|
||||||
@ -239,12 +342,13 @@ To close a group, use the button:
|
|||||||
|
|
||||||
#### `network`
|
#### `network`
|
||||||
|
|
||||||
> Network plugin. The plugin to show usage a network
|
> Network plugin. The plugin to show network usage
|
||||||
|
|
||||||
```js
|
```js
|
||||||
{
|
{
|
||||||
"type": "network",
|
"type": "network",
|
||||||
"flip": true
|
"flip": true,
|
||||||
|
"units": "dynamic" // or B/s, KB/s, MB/s, GB/s
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -255,12 +359,51 @@ To close a group, use the button:
|
|||||||
```js
|
```js
|
||||||
{
|
{
|
||||||
"type": "dock",
|
"type": "dock",
|
||||||
|
"filter": "(^Xcode$)|(Safari)|(.*player)",
|
||||||
"autoResize": true
|
"autoResize": true
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### `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:
|
## Actions:
|
||||||
|
|
||||||
|
### Example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"trigger": "singleTap",
|
||||||
|
"action": "hidKey",
|
||||||
|
"keycode": 53
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Triggers:
|
||||||
|
|
||||||
|
- `singleTap`
|
||||||
|
- `doubleTap`
|
||||||
|
- `tripleTap`
|
||||||
|
- `longTap`
|
||||||
|
|
||||||
|
### Types
|
||||||
|
|
||||||
- `hidKey`
|
- `hidKey`
|
||||||
> https://github.com/aosm/IOHIDFamily/blob/master/IOHIDSystem/IOKit/hidsystem/ev_keymap.h use only numbers
|
> https://github.com/aosm/IOHIDFamily/blob/master/IOHIDSystem/IOKit/hidsystem/ev_keymap.h use only numbers
|
||||||
|
|
||||||
@ -270,6 +413,7 @@ To close a group, use the button:
|
|||||||
```
|
```
|
||||||
|
|
||||||
- `keyPress`
|
- `keyPress`
|
||||||
|
> https://eastmanreference.com/complete-list-of-applescript-key-codes
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"action": "keyPress",
|
"action": "keyPress",
|
||||||
@ -281,7 +425,7 @@ To close a group, use the button:
|
|||||||
```js
|
```js
|
||||||
"action": "appleScript",
|
"action": "appleScript",
|
||||||
"actionAppleScript": {
|
"actionAppleScript": {
|
||||||
"inline": "tell application \"Finder\"\rmake new Finder window\rset target of front window to path to home folder as string\ractivate\rend tell"
|
"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",
|
||||||
// "filePath" or "base64" will work as well
|
// "filePath" or "base64" will work as well
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
@ -302,22 +446,6 @@ To close a group, use the button:
|
|||||||
"url": "https://google.com",
|
"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:
|
## Additional parameters:
|
||||||
|
|
||||||
- `width` restrict how much room a particular button will take
|
- `width` restrict how much room a particular button will take
|
||||||
@ -338,39 +466,52 @@ If you want to longPress for some operations, it is similar to the configuration
|
|||||||
"bordered": "false" // "true" or "false"
|
"bordered": "false" // "true" or "false"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Roadmap
|
- `background` allow to specify you button background color
|
||||||
|
|
||||||
- [x] Create the first prototype with TouchBar in Storyboard
|
```js
|
||||||
- [x] Put in stripe menu on startup the application
|
"background": "#FF0000",
|
||||||
- [x] Find how to simulate real buttons like brightness, volume, night shift and etc.
|
```
|
||||||
- [x] Time in touchbar!
|
by using background with color "#000000" and bordered == false you can create button without gray background but with background when the button is pressed
|
||||||
- [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
|
|
||||||
|
|
||||||
Settings:
|
- `title` specify button title
|
||||||
|
|
||||||
- [ ] Interface for plugins and export like presets
|
```js
|
||||||
- [x] Startup at login
|
"title": "hello"
|
||||||
- [ ] Show on/off in Dock
|
```
|
||||||
- [ ] Show on/off in StatusBar
|
|
||||||
- [ ] On/off Haptic Feedback
|
|
||||||
|
|
||||||
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
|
## Credits
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 68 KiB |
@ -3,7 +3,7 @@
|
|||||||
{
|
{
|
||||||
"type": "exitTouchbar",
|
"type": "exitTouchbar",
|
||||||
"image": {
|
"image": {
|
||||||
"filePath": "/Users/toxblh/git/selfProjects/MTMR/Resources/logo.png"
|
"filePath": "~/git/selfProjects/MTMR/Resources/logo.png"
|
||||||
},
|
},
|
||||||
"width": 36, "align": "left" },
|
"width": 36, "align": "left" },
|
||||||
{ "type": "brightnessDown", "width": 36, "align": "left" },
|
{ "type": "brightnessDown", "width": 36, "align": "left" },
|
||||||
@ -12,46 +12,46 @@
|
|||||||
"type": "appleScriptTitledButton",
|
"type": "appleScriptTitledButton",
|
||||||
"source": {
|
"source": {
|
||||||
"filePath":
|
"filePath":
|
||||||
"/Users/toxblh/git/selfProjects/MTMR/MTMR/AppleScripts/iTunes.nowPlaying.scpt"
|
"~/git/selfProjects/MTMR/MTMR/AppleScripts/iTunes.nowPlaying.scpt"
|
||||||
},
|
},
|
||||||
"action": "appleScript",
|
"action": "appleScript",
|
||||||
"actionAppleScript": {
|
"actionAppleScript": {
|
||||||
"filePath": "/Users/toxblh/git/selfProjects/MTMR/MTMR/AppleScripts/iTunes.next.scpt"
|
"filePath": "~/git/selfProjects/MTMR/MTMR/AppleScripts/iTunes.next.scpt"
|
||||||
},
|
},
|
||||||
"refreshInterval": 1,
|
"refreshInterval": 1,
|
||||||
"image": {
|
"image": {
|
||||||
"filePath": "/Users/toxblh/git/selfProjects/btt-touchbar-preset/icons/iTunes.png"
|
"filePath": "~/git/selfProjects/btt-touchbar-preset/icons/iTunes.png"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "appleScriptTitledButton",
|
"type": "appleScriptTitledButton",
|
||||||
"source": {
|
"source": {
|
||||||
"filePath":
|
"filePath":
|
||||||
"/Users/toxblh/git/selfProjects/MTMR/MTMR/AppleScripts/Spotify.nowPlaying.scpt"
|
"~/git/selfProjects/MTMR/MTMR/AppleScripts/Spotify.nowPlaying.scpt"
|
||||||
},
|
},
|
||||||
"action": "appleScript",
|
"action": "appleScript",
|
||||||
"actionAppleScript": {
|
"actionAppleScript": {
|
||||||
"filePath":
|
"filePath":
|
||||||
"/Users/toxblh/git/selfProjects/MTMR/MTMR/AppleScripts/Spotify.next.scpt"
|
"~/git/selfProjects/MTMR/MTMR/AppleScripts/Spotify.next.scpt"
|
||||||
},
|
},
|
||||||
"refreshInterval": 1,
|
"refreshInterval": 1,
|
||||||
"image": {
|
"image": {
|
||||||
"filePath": "/Users/toxblh/git/selfProjects/btt-touchbar-preset/icons/Spotify.png"
|
"filePath": "~/git/selfProjects/btt-touchbar-preset/icons/Spotify.png"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "appleScriptTitledButton",
|
"type": "appleScriptTitledButton",
|
||||||
"source": {
|
"source": {
|
||||||
"filePath":
|
"filePath":
|
||||||
"/Users/toxblh/git/selfProjects/MTMR/MTMR/AppleScripts/Vox.nowPlaying.scpt"
|
"~/git/selfProjects/MTMR/MTMR/AppleScripts/Vox.nowPlaying.scpt"
|
||||||
},
|
},
|
||||||
"action": "appleScript",
|
"action": "appleScript",
|
||||||
"actionAppleScript": {
|
"actionAppleScript": {
|
||||||
"filePath": "/Users/toxblh/git/selfProjects/MTMR/MTMR/AppleScripts/Vox.next.scpt"
|
"filePath": "~/git/selfProjects/MTMR/MTMR/AppleScripts/Vox.next.scpt"
|
||||||
},
|
},
|
||||||
"refreshInterval": 1,
|
"refreshInterval": 1,
|
||||||
"image": {
|
"image": {
|
||||||
"filePath": "/Users/toxblh/git/selfProjects/btt-touchbar-preset/icons/Vox.png"
|
"filePath": "~/git/selfProjects/btt-touchbar-preset/icons/Vox.png"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
1
Sparkle.framework/Autoupdate
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
Versions/Current/Autoupdate
|
||||||
1
Sparkle.framework/Updater.app
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
Versions/Current/Updater.app
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||