mirror of
https://github.com/Toxblh/MTMR.git
synced 2026-10-31 18:38:35 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
95271dba0e | ||
|
|
67aaa2abf4 | ||
|
|
58e4160649 | ||
|
|
77846d0436 | ||
|
|
bc66b375d3 | ||
|
|
ce8004641d | ||
|
|
155e48a693 | ||
|
|
76a7d59fe5 | ||
|
|
6a85bea5b5 | ||
|
|
000b825ec9 | ||
|
|
3432e24a55 | ||
|
|
705d0a64b5 | ||
|
|
9f0944b06b | ||
|
|
efc52293a8 | ||
|
|
7b853b5d47 | ||
|
|
439246e85b | ||
|
|
c254ee430d | ||
|
|
cfcda6e46f | ||
|
|
144ff9cf79 | ||
|
|
91a4e5bded | ||
|
|
68dfcddb29 | ||
|
|
82ec231700 | ||
|
|
ed261c2349 | ||
|
|
f06d1f4329 | ||
|
|
3dabd598f4 | ||
|
|
0a09ea7117 | ||
|
|
02cf911336 | ||
|
|
d831069025 | ||
|
|
33206ab457 | ||
|
|
c6c808369f | ||
|
|
cbad06ac07 | ||
|
|
64171d5c51 | ||
|
|
164295820b | ||
|
|
ae469bb92a | ||
|
|
57b5129135 | ||
|
|
7cc72de66f | ||
|
|
2e5db4ffa0 | ||
|
|
65e5a52383 | ||
|
|
03304bd030 | ||
|
|
f264425ba7 | ||
|
|
c377b9494c | ||
|
|
f5b36ec012 | ||
|
|
999c4cf6e2 | ||
|
|
b8d5d0a8a5 | ||
|
|
6bb5aade07 | ||
|
|
b6bcc31456 | ||
|
|
9d09646742 | ||
|
|
55f988e393 | ||
|
|
2f4cf9e5f6 | ||
|
|
327d38e47d | ||
|
|
cd380b289d | ||
|
|
6a26b2f5ca | ||
|
|
d7186aac18 | ||
|
|
cef07dc965 | ||
|
|
471c0c5d57 | ||
|
|
5b93525806 | ||
|
|
b87bc632ab | ||
|
|
1763a24942 | ||
|
|
627e6951c1 | ||
|
|
918054df78 | ||
|
|
25fb0d4f04 | ||
|
|
61a357ffc3 | ||
|
|
f4116a95df | ||
|
|
c952505c1c | ||
|
|
f10b58b996 | ||
|
|
88a8ae3ae8 | ||
|
|
d783448567 | ||
|
|
595ae165d3 | ||
|
|
da406713dd | ||
|
|
938b83f37e | ||
|
|
a8a085df1e | ||
|
|
b188f45c8e | ||
|
|
3319c5d45a | ||
|
|
6174d14925 | ||
|
|
c8197239d7 | ||
|
|
d2854e3dae | ||
|
|
4f9c3258ae | ||
|
|
dfeda1e874 | ||
|
|
82f82039ae | ||
|
|
324c3f711e | ||
|
|
a81d6fc595 | ||
|
|
3bde1fe4b1 |
@@ -0,0 +1,6 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
issuehunt: Toxblh
|
||||||
|
patreon: toxblh
|
||||||
|
ko_fi: toxblh
|
||||||
|
custom: https://www.paypal.me/toxblh
|
||||||
@@ -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]}
|
||||||
@@ -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,6 +18,12 @@
|
|||||||
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 */; };
|
||||||
|
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 */; };
|
||||||
@@ -63,6 +69,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 */
|
||||||
@@ -91,6 +100,13 @@
|
|||||||
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>"; };
|
||||||
|
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>"; };
|
||||||
@@ -146,6 +162,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 */
|
||||||
@@ -218,9 +237,13 @@
|
|||||||
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 */,
|
||||||
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 */,
|
||||||
@@ -247,6 +270,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 */,
|
||||||
@@ -264,6 +289,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 */,
|
||||||
@@ -294,8 +321,10 @@
|
|||||||
36C2ECD6207B6DAE003CDA33 /* TimeTouchBarItem.swift */,
|
36C2ECD6207B6DAE003CDA33 /* TimeTouchBarItem.swift */,
|
||||||
6027D1B82080E52A004FFDC7 /* VolumeViewController.swift */,
|
6027D1B82080E52A004FFDC7 /* VolumeViewController.swift */,
|
||||||
607EEA4A2087835F009DA5F0 /* WeatherBarItem.swift */,
|
607EEA4A2087835F009DA5F0 /* WeatherBarItem.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>";
|
||||||
@@ -351,7 +380,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 = {
|
||||||
@@ -361,7 +390,7 @@
|
|||||||
};
|
};
|
||||||
B082B260205C7D8000BC04DC = {
|
B082B260205C7D8000BC04DC = {
|
||||||
CreatedOnToolsVersion = 9.2;
|
CreatedOnToolsVersion = 9.2;
|
||||||
LastSwiftMigration = 1000;
|
LastSwiftMigration = 1020;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -393,6 +422,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 */,
|
||||||
@@ -443,11 +474,13 @@
|
|||||||
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 */,
|
||||||
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 */,
|
||||||
@@ -456,11 +489,15 @@
|
|||||||
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 */,
|
||||||
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 */,
|
||||||
@@ -468,6 +505,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 */,
|
||||||
@@ -626,7 +664,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;
|
||||||
};
|
};
|
||||||
@@ -649,7 +687,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;
|
||||||
};
|
};
|
||||||
@@ -662,7 +700,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;
|
||||||
};
|
};
|
||||||
@@ -677,7 +715,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>
|
||||||
|
|||||||
+34
-21
@@ -25,6 +25,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true] as NSDictionary)
|
AXIsProcessTrustedWithOptions([kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: true] as NSDictionary)
|
||||||
|
|
||||||
TouchBarController.shared.setupControlStripPresence()
|
TouchBarController.shared.setupControlStripPresence()
|
||||||
|
HapticFeedbackUpdate()
|
||||||
|
|
||||||
if let button = statusItem.button {
|
if let button = statusItem.button {
|
||||||
button.image = #imageLiteral(resourceName: "StatusImage")
|
button.image = #imageLiteral(resourceName: "StatusImage")
|
||||||
@@ -40,18 +41,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
|
|
||||||
func applicationWillTerminate(_: Notification) {}
|
func applicationWillTerminate(_: Notification) {}
|
||||||
|
|
||||||
@objc func updateIsBlockedApp() {
|
func HapticFeedbackUpdate() {
|
||||||
var blacklistAppIdentifiers: [String] = []
|
HapticFeedback.shared = AppSettings.hapticFeedbackState ? HapticFeedback() : nil
|
||||||
if let blackListed = UserDefaults.standard.stringArray(forKey: "com.toxblh.mtmr.blackListedApps") {
|
|
||||||
blacklistAppIdentifiers = blackListed
|
|
||||||
}
|
|
||||||
var frontmostApplicationIdentifier: String? {
|
|
||||||
guard let frontmostId = NSWorkspace.shared.frontmostApplication?.bundleIdentifier else { return nil }
|
|
||||||
return frontmostId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if frontmostApplicationIdentifier != nil {
|
@objc func updateIsBlockedApp() {
|
||||||
isBlockedApp = blacklistAppIdentifiers.index(of: frontmostApplicationIdentifier!) != nil
|
if let frontmostAppId = TouchBarController.shared.frontmostApplicationIdentifier {
|
||||||
|
isBlockedApp = AppSettings.blacklistedAppIds.firstIndex(of: frontmostAppId) != nil
|
||||||
} else {
|
} else {
|
||||||
isBlockedApp = false
|
isBlockedApp = false
|
||||||
}
|
}
|
||||||
@@ -67,29 +63,38 @@ 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
|
||||||
|
HapticFeedbackUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 +127,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 +144,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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
if not (exists window 1) then
|
||||||
make new Finder window
|
make new Finder window
|
||||||
set target of front window to path to home folder as string
|
set target of front window to path to home folder as string
|
||||||
|
end if
|
||||||
activate
|
activate
|
||||||
end tell
|
end tell
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="macosx"/>
|
<deployment identifier="macosx"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14313.18"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<scenes>
|
<scenes>
|
||||||
<!--Application-->
|
<!--Application-->
|
||||||
@@ -619,7 +619,7 @@
|
|||||||
<menuItem title="Show Sidebar" keyEquivalent="s" id="kIP-vf-haE">
|
<menuItem title="Show Sidebar" keyEquivalent="s" id="kIP-vf-haE">
|
||||||
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
|
||||||
<connections>
|
<connections>
|
||||||
<action selector="toggleSourceList:" target="Ady-hI-5gd" id="iwa-gc-5KM"/>
|
<action selector="toggleSidebar:" target="Ady-hI-5gd" id="iwa-gc-5KM"/>
|
||||||
</connections>
|
</connections>
|
||||||
</menuItem>
|
</menuItem>
|
||||||
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
|
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -8,13 +8,32 @@
|
|||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegate {
|
struct ItemAction {
|
||||||
var tapClosure: (() -> Void)?
|
typealias TriggerClosure = (() -> Void)?
|
||||||
var longTapClosure: (() -> Void)?
|
|
||||||
private var button: NSButton!
|
|
||||||
|
|
||||||
private var singleClick: NSClickGestureRecognizer!
|
let trigger: Action.Trigger
|
||||||
private var longClick: NSPressGestureRecognizer!
|
let closure: TriggerClosure
|
||||||
|
|
||||||
|
init(trigger: Action.Trigger, _ closure: TriggerClosure) {
|
||||||
|
self.trigger = trigger
|
||||||
|
self.closure = closure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegate {
|
||||||
|
|
||||||
|
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 longClick: LongPressGestureRecognizer!
|
||||||
|
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 = NSClickGestureRecognizer(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,40 +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) {
|
||||||
let hf: HapticFeedback = HapticFeedback()
|
let itemActions = self.actions.filter { $0.trigger == trigger }
|
||||||
switch gr.state {
|
for itemAction in itemActions {
|
||||||
case .ended:
|
itemAction.closure?()
|
||||||
hf.tap(strong: 2)
|
|
||||||
tapClosure?()
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func handleGestureLong(gr: NSPressGestureRecognizer) {
|
@objc func handleGestureSingleTap() {
|
||||||
let hf: HapticFeedback = HapticFeedback()
|
callActions(for: .singleTap)
|
||||||
switch gr.state {
|
|
||||||
case .began:
|
|
||||||
if let closure = self.longTapClosure {
|
|
||||||
hf.tap(strong: 2)
|
|
||||||
closure()
|
|
||||||
} else if let closure = self.tapClosure {
|
|
||||||
hf.tap(strong: 6)
|
|
||||||
closure()
|
|
||||||
print("long click")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func handleGestureDoubleTap() {
|
||||||
|
callActions(for: .doubleTap)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func handleGestureTripleTap() {
|
||||||
|
callActions(for: .tripleTap)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func handleGestureLong(gr: NSPressGestureRecognizer) {
|
||||||
|
switch gr.state {
|
||||||
|
case .possible: // tiny hack because we're calling action manually
|
||||||
|
callActions(for: .longTap)
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
@@ -161,6 +192,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")
|
||||||
}
|
}
|
||||||
@@ -172,6 +207,130 @@ class CustomButtonCell: NSButtonCell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Thanks to https://stackoverflow.com/a/49843893
|
||||||
|
final class MultiClickGestureRecognizer: NSClickGestureRecognizer {
|
||||||
|
|
||||||
|
private let _action: Selector
|
||||||
|
private let _doubleAction: Selector
|
||||||
|
private let _tripleAction: Selector
|
||||||
|
private var _clickCount: Int = 0
|
||||||
|
|
||||||
|
public var isDoubleClickEnabled = true
|
||||||
|
public var isTripleClickEnabled = true
|
||||||
|
|
||||||
|
override var action: Selector? {
|
||||||
|
get {
|
||||||
|
return nil /// prevent base class from performing any actions
|
||||||
|
} set {
|
||||||
|
if newValue != nil { // if they are trying to assign an actual action
|
||||||
|
fatalError("Only use init(target:action:doubleAction) for assigning actions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(target: AnyObject, action: Selector, doubleAction: Selector, tripleAction: Selector) {
|
||||||
|
_action = action
|
||||||
|
_doubleAction = doubleAction
|
||||||
|
_tripleAction = tripleAction
|
||||||
|
super.init(target: target, action: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(target:action:doubleAction:tripleAction) is only support atm")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesBegan(with event: NSEvent) {
|
||||||
|
HapticFeedback.shared?.tap(strong: 2)
|
||||||
|
super.touchesBegan(with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func touchesEnded(with event: NSEvent) {
|
||||||
|
HapticFeedback.shared?.tap(strong: 1)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
timerInvalidate()
|
||||||
|
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.shared?.tap(strong: 6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
timerInvalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension String {
|
extension String {
|
||||||
var defaultTouchbarAttributedString: NSAttributedString {
|
var defaultTouchbarAttributedString: NSAttributedString {
|
||||||
let attrTitle = NSMutableAttributedString(string: self, attributes: [.foregroundColor: NSColor.white, .font: NSFont.systemFont(ofSize: 15, weight: .regular), .baselineOffset: 1])
|
let attrTitle = NSMutableAttributedString(string: self, attributes: [.foregroundColor: NSColor.white, .font: NSFont.systemFont(ofSize: 15, weight: .regular), .baselineOffset: 1])
|
||||||
|
|||||||
+47
-13
@@ -9,9 +9,23 @@
|
|||||||
import IOKit
|
import IOKit
|
||||||
|
|
||||||
class HapticFeedback {
|
class HapticFeedback {
|
||||||
|
static var shared: HapticFeedback?
|
||||||
|
|
||||||
|
// Here we have list of possible IDs for Haptic Generator Device. They are not constant
|
||||||
|
// To find deviceID, you will need IORegistryExplorer app from Additional Tools for Xcode dmg
|
||||||
|
// which you can download from https://developer.apple.com/download/more/?=Additional%20Tools
|
||||||
|
// Open IORegistryExplorer app, search for AppleMultitouchDevice and get "Multitouch ID"
|
||||||
|
// There should be programmatic way to get it but I can't find, no docs for macOS :(
|
||||||
|
private let possibleDeviceIDs: [UInt64] = [
|
||||||
|
0x200_0000_0100_0000, // MacBook Pro 2016/2017
|
||||||
|
0x300000080500000 // MacBook Pro 2019 (possibly 2018 as well)
|
||||||
|
]
|
||||||
|
private var correctDeviceID: UInt64?
|
||||||
private var actuatorRef: CFTypeRef?
|
private var actuatorRef: CFTypeRef?
|
||||||
private var deviceID: UInt64 = 0x200_0000_0100_0000
|
|
||||||
private var error: IOReturn = 0
|
init() {
|
||||||
|
recreateDevice()
|
||||||
|
}
|
||||||
|
|
||||||
// Don't know how to do strong is enum one of
|
// Don't know how to do strong is enum one of
|
||||||
// 1 like back Click
|
// 1 like back Click
|
||||||
@@ -25,31 +39,51 @@ class HapticFeedback {
|
|||||||
// 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`
|
// 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) {
|
func tap(strong: Int32) {
|
||||||
actuatorRef = MTActuatorCreateFromDeviceID(deviceID).takeRetainedValue()
|
guard correctDeviceID != nil, actuatorRef != nil else {
|
||||||
|
print("guard actuatorRef == nil (no haptic device found?)")
|
||||||
guard actuatorRef != nil else {
|
|
||||||
print("guard actuatorRef == nil")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
error = MTActuatorOpen(actuatorRef!)
|
var result: IOReturn
|
||||||
guard error == kIOReturnSuccess else {
|
|
||||||
|
result = MTActuatorOpen(actuatorRef!)
|
||||||
|
guard result == kIOReturnSuccess else {
|
||||||
print("guard MTActuatorOpen")
|
print("guard MTActuatorOpen")
|
||||||
|
recreateDevice()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
error = MTActuatorActuate(actuatorRef!, strong, 0, 0.0, 0.0)
|
result = MTActuatorActuate(actuatorRef!, strong, 0, 0, 0)
|
||||||
guard error == kIOReturnSuccess else {
|
guard result == kIOReturnSuccess else {
|
||||||
print("guard MTActuatorActuate")
|
print("guard MTActuatorActuate")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
error = MTActuatorClose(actuatorRef!)
|
result = MTActuatorClose(actuatorRef!)
|
||||||
guard error == kIOReturnSuccess else {
|
guard result == kIOReturnSuccess else {
|
||||||
print("guard MTActuatorClose")
|
print("guard MTActuatorClose")
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -17,9 +17,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.21</string>
|
<string>0.27</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>207</string>
|
<string>429</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>
|
||||||
|
|||||||
+268
-201
@@ -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,151 +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: [:]
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
"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": { _ in
|
|
||||||
(
|
|
||||||
item: .dock(),
|
|
||||||
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 }
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
||||||
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval)
|
|
||||||
return (
|
|
||||||
item: .music(interval: interval ?? 1800.00),
|
|
||||||
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: [:]
|
||||||
) }
|
) }
|
||||||
}
|
}
|
||||||
@@ -310,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: [:]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -324,22 +257,26 @@ 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()
|
case battery
|
||||||
case volume()
|
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 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)
|
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)
|
||||||
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
|
||||||
@@ -354,6 +291,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
|
||||||
@@ -361,26 +299,40 @@ enum ItemType: Decodable {
|
|||||||
case workTime
|
case workTime
|
||||||
case restTime
|
case restTime
|
||||||
case flip
|
case flip
|
||||||
|
case autoResize
|
||||||
|
case filter
|
||||||
|
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 dock
|
case dock
|
||||||
case volume
|
case volume
|
||||||
case brightness
|
case brightness
|
||||||
case weather
|
case weather
|
||||||
|
case yandexWeather
|
||||||
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 {
|
||||||
@@ -390,7 +342,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)
|
||||||
@@ -399,16 +357,19 @@ 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 .dock:
|
case .dock:
|
||||||
self = .dock()
|
let autoResize = try container.decodeIfPresent(Bool.self, forKey: .autoResize) ?? false
|
||||||
|
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
|
||||||
@@ -421,6 +382,10 @@ enum ItemType: Decodable {
|
|||||||
let icon_type = try container.decodeIfPresent(String.self, forKey: .icon_type) ?? "text"
|
let icon_type = try container.decodeIfPresent(String.self, forKey: .icon_type) ?? "text"
|
||||||
self = .weather(interval: interval, units: units, api_key: api_key, icon_type: icon_type)
|
self = .weather(interval: interval, units: units, api_key: api_key, icon_type: icon_type)
|
||||||
|
|
||||||
|
case .yandexWeather:
|
||||||
|
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 1800.0
|
||||||
|
self = .yandexWeather(interval: interval)
|
||||||
|
|
||||||
case .currency:
|
case .currency:
|
||||||
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 600.0
|
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 600.0
|
||||||
let from = try container.decodeIfPresent(String.self, forKey: .from) ?? "RUB"
|
let from = try container.decodeIfPresent(String.self, forKey: .from) ?? "RUB"
|
||||||
@@ -429,21 +394,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) ?? 1800.0
|
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 5.0
|
||||||
self = .music(interval: interval)
|
let disableMarquee = try container.decodeIfPresent(Bool.self, forKey: .disableMarquee) ?? false
|
||||||
|
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
|
||||||
@@ -455,12 +421,115 @@ enum ItemType: Decodable {
|
|||||||
self = .network(flip: flip)
|
self = .network(flip: flip)
|
||||||
|
|
||||||
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)
|
||||||
@@ -518,7 +587,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)
|
||||||
@@ -697,12 +766,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -722,16 +799,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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
//
|
||||||
|
// 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!
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// Apply returned text attributes (if they were returned) to our result string
|
||||||
|
let helper = AMR_ANSIEscapeHelper.init()
|
||||||
|
helper.defaultStringColor = NSColor.white
|
||||||
|
helper.font = "1".defaultTouchbarAttributedString.attribute(.font, at: 0, effectiveRange: nil) as? NSFont
|
||||||
|
let title = NSMutableAttributedString.init(attributedString: helper.attributedString(withANSIEscapedString: scriptResult) ?? NSAttributedString(string: ""))
|
||||||
|
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
|
||||||
|
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()
|
||||||
|
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")
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
//
|
||||||
|
// 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()
|
||||||
|
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)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+155
-66
@@ -23,36 +23,44 @@ 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():
|
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"
|
||||||
case .weather(interval: _, units: _, api_key: _, icon_type: _):
|
case .weather(interval: _, units: _, api_key: _, icon_type: _):
|
||||||
return "com.toxblh.mtmr.weather"
|
return "com.toxblh.mtmr.weather"
|
||||||
|
case .yandexWeather(interval: _):
|
||||||
|
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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,40 +80,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
|
(
|
||||||
|
item: .staticButton(title: ""),
|
||||||
|
actions: [
|
||||||
|
Action(trigger: .singleTap, value: .custom(closure: { [weak self] in
|
||||||
guard let `self` = self else { return }
|
guard let `self` = self else { return }
|
||||||
self.reloadPreset(path: self.lastPresetPath)
|
self.reloadPreset(path: self.lastPresetPath)
|
||||||
}), longAction: .none, parameters: [.width: .width(30), .image: .image(source: (NSImage(named: NSImage.stopProgressFreestandingTemplateName))!)])
|
}))
|
||||||
|
],
|
||||||
|
legacyAction: .none,
|
||||||
|
legacyLongAction: .none,
|
||||||
|
parameters: [.width: .width(30), .image: .image(source: (NSImage(named: NSImage.stopProgressFreestandingTemplateName))!)])
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
@@ -122,23 +133,29 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
|||||||
jsonItems = newJsonItems
|
jsonItems = newJsonItems
|
||||||
itemDefinitions = [:]
|
itemDefinitions = [:]
|
||||||
items = [:]
|
items = [:]
|
||||||
leftIdentifiers = []
|
|
||||||
centerItems = []
|
|
||||||
rightIdentifiers = []
|
|
||||||
|
|
||||||
loadItemDefinitions(jsonItems: jsonItems)
|
loadItemDefinitions(jsonItems: jsonItems)
|
||||||
createItems()
|
createItems()
|
||||||
|
|
||||||
centerItems = centerIdentifiers.compactMap({ (identifier) -> NSTouchBarItem? in
|
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)
|
||||||
|
|
||||||
touchBar.delegate = self
|
touchBar.delegate = self
|
||||||
touchBar.defaultItemIdentifiers = []
|
touchBar.defaultItemIdentifiers = [basicViewIdentifier]
|
||||||
touchBar.defaultItemIdentifiers = leftIdentifiers + [centerScrollArea] + rightIdentifiers
|
|
||||||
|
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
|
||||||
|
|
||||||
updateActiveApp()
|
updateActiveApp()
|
||||||
}
|
}
|
||||||
@@ -148,7 +165,7 @@ 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()
|
presentTouchBar()
|
||||||
@@ -168,7 +185,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +211,12 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
|||||||
|
|
||||||
func createItems() {
|
func createItems() {
|
||||||
for (identifier, definition) in itemDefinitions {
|
for (identifier, definition) in itemDefinitions {
|
||||||
items[identifier] = createItem(forIdentifier: identifier, definition: definition)
|
let item = createItem(forIdentifier: identifier, definition: definition)
|
||||||
|
if item is SwipeItem {
|
||||||
|
swipeItems.append(item as! SwipeItem)
|
||||||
|
} else {
|
||||||
|
items[identifier] = item
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,7 +233,7 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc private func presentTouchBar() {
|
@objc private func presentTouchBar() {
|
||||||
if showControlStripState {
|
if AppSettings.showControlStripState {
|
||||||
updateControlStripPresence()
|
updateControlStripPresence()
|
||||||
presentSystemModal(touchBar, systemTrayItemIdentifier: .controlStripItem)
|
presentSystemModal(touchBar, systemTrayItemIdentifier: .controlStripItem)
|
||||||
} else {
|
} else {
|
||||||
@@ -230,31 +252,36 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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],
|
|
||||||
let definition = self.itemDefinitions[identifier],
|
|
||||||
definition.align != .center else {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return item
|
|
||||||
}
|
|
||||||
|
|
||||||
func createItem(forIdentifier identifier: NSTouchBarItem.Identifier, definition item: BarItemDefinition) -> NSTouchBarItem? {
|
func createItem(forIdentifier identifier: NSTouchBarItem.Identifier, definition item: BarItemDefinition) -> NSTouchBarItem? {
|
||||||
var barItem: NSTouchBarItem!
|
var barItem: NSTouchBarItem!
|
||||||
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 .dock:
|
case let .dock(autoResize: autoResize, filter: regexString):
|
||||||
barItem = AppScrubberTouchBarItem(identifier: identifier)
|
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)
|
||||||
@@ -269,31 +296,43 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
|||||||
}
|
}
|
||||||
case let .weather(interval: interval, units: units, api_key: api_key, icon_type: icon_type):
|
case let .weather(interval: interval, units: units, api_key: api_key, icon_type: icon_type):
|
||||||
barItem = WeatherBarItem(identifier: identifier, interval: interval, units: units, api_key: api_key, icon_type: icon_type)
|
barItem = WeatherBarItem(identifier: identifier, interval: interval, units: units, api_key: api_key, icon_type: icon_type)
|
||||||
|
case let .yandexWeather(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):
|
case let .music(interval: interval, disableMarquee: disableMarquee):
|
||||||
barItem = MusicBarItem(identifier: identifier, interval: interval)
|
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):
|
||||||
barItem = NetworkBarItem(identifier: identifier, flip: flip)
|
barItem = NetworkBarItem(identifier: identifier, flip: flip)
|
||||||
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
|
||||||
@@ -317,8 +356,52 @@ 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):
|
||||||
@@ -362,7 +445,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):
|
||||||
@@ -414,6 +497,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,191 +7,129 @@
|
|||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class AppScrubberTouchBarItem: NSCustomTouchBarItem, NSScrubberDelegate, NSScrubberDataSource {
|
class AppScrubberTouchBarItem: NSCustomTouchBarItem {
|
||||||
private var scrubber: NSScrubber!
|
private var scrollView = NSScrollView()
|
||||||
|
private var autoResize: Bool = false
|
||||||
private let hf: HapticFeedback = HapticFeedback()
|
private var widthConstraint: NSLayoutConstraint?
|
||||||
|
private let filter: NSRegularExpression?
|
||||||
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 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] = []
|
||||||
|
|
||||||
override init(identifier: NSTouchBarItem.Identifier) {
|
init(identifier: NSTouchBarItem.Identifier, autoResize: Bool = false, filter: NSRegularExpression? = nil) {
|
||||||
|
self.filter = filter
|
||||||
super.init(identifier: identifier)
|
super.init(identifier: identifier)
|
||||||
|
self.autoResize = autoResize
|
||||||
|
view = scrollView
|
||||||
|
|
||||||
scrubber = NSScrubber()
|
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(hardReloadItems), name: NSWorkspace.didLaunchApplicationNotification, object: nil)
|
||||||
scrubber.delegate = self
|
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(hardReloadItems), name: NSWorkspace.didTerminateApplicationNotification, object: nil)
|
||||||
scrubber.dataSource = self
|
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(softReloadItems), name: NSWorkspace.didActivateApplicationNotification, object: nil)
|
||||||
scrubber.mode = .free // .fixed
|
|
||||||
let layout = NSScrubberFlowLayout()
|
|
||||||
layout.itemSize = NSSize(width: 36, height: 32)
|
|
||||||
layout.itemSpacing = 2
|
|
||||||
scrubber.scrubberLayout = layout
|
|
||||||
scrubber.selectionBackgroundStyle = .roundedBackground
|
|
||||||
scrubber.showsAdditionalContentIndicators = true
|
|
||||||
|
|
||||||
view = scrubber
|
persistentAppIdentifiers = AppSettings.dockPersistentAppIds
|
||||||
|
hardReloadItems()
|
||||||
scrubber.register(NSScrubberImageItemView.self, forItemIdentifier: NSUserInterfaceItemIdentifier(rawValue: "ScrubberApplicationsItemReuseIdentifier"))
|
|
||||||
|
|
||||||
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()
|
||||||
scrubber.selectedIndex = index ?? 0
|
updateSize()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func numberOfItems(for _: NSScrubber) -> Int {
|
@objc func softReloadItems() {
|
||||||
return applications.count
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func scrubber(_ scrubber: NSScrubber, viewForItemAt index: Int) -> NSScrubberItemView {
|
func updateSize() {
|
||||||
let item = scrubber.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "ScrubberApplicationsItemReuseIdentifier"), owner: self) as? NSScrubberImageItemView ?? NSScrubberImageItemView()
|
if self.autoResize {
|
||||||
item.imageView.imageScaling = .scaleProportionallyDown
|
self.widthConstraint?.isActive = false
|
||||||
|
|
||||||
let app = applications[index]
|
let width = self.scrollView.documentView?.fittingSize.width ?? 0
|
||||||
|
self.widthConstraint = self.scrollView.widthAnchor.constraint(equalToConstant: width)
|
||||||
if let icon = app.icon {
|
self.widthConstraint!.isActive = true
|
||||||
item.image = icon
|
}
|
||||||
item.image.size = NSSize(width: 26, height: 26)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
item.removeFromSuperview()
|
func reloadData() {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
let dotView = NSView(frame: .zero)
|
public func createAppButton(for app: DockItem) -> DockBarItem {
|
||||||
dotView.wantsLayer = true
|
let item = DockBarItem(app)
|
||||||
if runningAppsIdentifiers.contains(app.bundleIdentifier!) {
|
item.isBordered = false
|
||||||
dotView.layer?.backgroundColor = NSColor.white.cgColor
|
item.actions.append(contentsOf: [
|
||||||
} else {
|
ItemAction(trigger: .singleTap) { [weak self] in
|
||||||
dotView.layer?.backgroundColor = NSColor.black.cgColor
|
self?.switchToApp(app: app)
|
||||||
|
},
|
||||||
|
ItemAction(trigger: .longTap) { [weak self] in
|
||||||
|
self?.handleHalfLongPress(item: app)
|
||||||
|
}
|
||||||
|
])
|
||||||
|
item.killAppClosure = {[weak self] in
|
||||||
|
self?.handleLongPress(item: app)
|
||||||
}
|
}
|
||||||
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 {
|
|
||||||
hf.tap(strong: 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ticks > maxTicks {
|
|
||||||
stopTimer()
|
|
||||||
hf.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)
|
||||||
hf.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 !app.terminate() {
|
||||||
|
app.forceTerminate()
|
||||||
|
}
|
||||||
|
hardReloadItems()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ticks > maxTicks {
|
private func handleHalfLongPress(item: DockItem) {
|
||||||
if let processIdentifier = applications[selected].pid {
|
if let index = self.persistentAppIdentifiers.firstIndex(of: item.bundleIdentifier) {
|
||||||
if !(NSRunningApplication(processIdentifier: processIdentifier)?.terminate())! {
|
|
||||||
NSRunningApplication(processIdentifier: processIdentifier)?.forceTerminate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
hf.tap(strong: 6)
|
|
||||||
if let index = self.persistentAppIdentifiers.index(of: bundleIdentifier!) {
|
|
||||||
persistentAppIdentifiers.remove(at: index)
|
persistentAppIdentifiers.remove(at: index)
|
||||||
|
hardReloadItems()
|
||||||
} else {
|
} else {
|
||||||
persistentAppIdentifiers.append(bundleIdentifier!)
|
persistentAppIdentifiers.append(item.bundleIdentifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
UserDefaults.standard.set(persistentAppIdentifiers, forKey: "com.toxblh.mtmr.dock.persistent")
|
AppSettings.dockPersistentAppIds = persistentAppIdentifiers
|
||||||
UserDefaults.standard.synchronize()
|
|
||||||
}
|
|
||||||
ticks = 0
|
|
||||||
updateRunningApplication()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func launchedApplications() -> [DockItem] {
|
private func launchedApplications() -> [DockItem] {
|
||||||
@@ -200,24 +138,27 @@ class AppScrubberTouchBarItem: NSCustomTouchBarItem, NSScrubberDelegate, NSScrub
|
|||||||
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")
|
||||||
@@ -248,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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,11 +35,29 @@ class CurrencyBarItem: CustomButtonTouchBarItem {
|
|||||||
"IDR": "Rp",
|
"IDR": "Rp",
|
||||||
"MXN": "$",
|
"MXN": "$",
|
||||||
"SGD": "$",
|
"SGD": "$",
|
||||||
"CHF": "Fr.",
|
|
||||||
"BTC": "฿",
|
"BTC": "฿",
|
||||||
"LTC": "Ł",
|
"LTC": "Ł",
|
||||||
"ETH": "Ξ",
|
"ETH": "Ξ",
|
||||||
]
|
]
|
||||||
|
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": 2,
|
||||||
|
"LTC": 2,
|
||||||
|
"ETH": 2,
|
||||||
|
]
|
||||||
|
|
||||||
init(identifier: NSTouchBarItem.Identifier, interval: TimeInterval, from: String, to: String, full: Bool) {
|
init(identifier: NSTouchBarItem.Identifier, interval: TimeInterval, from: String, to: String, full: Bool) {
|
||||||
activity = NSBackgroundActivityScheduler(identifier: "\(identifier.rawValue).updatecheck")
|
activity = NSBackgroundActivityScheduler(identifier: "\(identifier.rawValue).updatecheck")
|
||||||
@@ -57,6 +78,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,17 +143,23 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
let regularFont = attributedTitle.attribute(.font, at: 0, effectiveRange: nil) as? NSFont ?? NSFont.systemFont(ofSize: 15)
|
let regularFont = attributedTitle.attribute(.font, at: 0, effectiveRange: nil) as? NSFont ?? NSFont.systemFont(ofSize: 15)
|
||||||
let newTitle = NSMutableAttributedString(string: title as String, attributes: [.foregroundColor: color, .font: regularFont])
|
let newTitle = NSMutableAttributedString(string: title as String, attributes: [.foregroundColor: color, .font: regularFont, .baselineOffset: 1])
|
||||||
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) {
|
||||||
|
|||||||
+117
-42
@@ -10,27 +10,43 @@ import Cocoa
|
|||||||
import ScriptingBridge
|
import ScriptingBridge
|
||||||
|
|
||||||
class MusicBarItem: CustomButtonTouchBarItem {
|
class MusicBarItem: CustomButtonTouchBarItem {
|
||||||
private let interval: TimeInterval
|
private enum Player: String {
|
||||||
private var songTitle: String?
|
case Music = "com.apple.Music"
|
||||||
private var timer: Timer?
|
case iTunes = "com.apple.iTunes"
|
||||||
let buttonSize = NSSize(width: 21, height: 21)
|
case Spotify = "com.spotify.client"
|
||||||
|
case VOX = "com.coppertino.Vox"
|
||||||
|
case Chrome = "com.google.Chrome"
|
||||||
|
case Safari = "com.apple.Safari"
|
||||||
|
}
|
||||||
|
|
||||||
let playerBundleIdentifiers = [
|
private let playerBundleIdentifiers = [
|
||||||
"com.apple.iTunes",
|
Player.Music,
|
||||||
"com.spotify.client",
|
Player.iTunes,
|
||||||
"com.coppertino.Vox",
|
Player.Spotify,
|
||||||
"com.google.Chrome",
|
Player.VOX,
|
||||||
"com.apple.Safari",
|
Player.Chrome,
|
||||||
|
Player.Safari,
|
||||||
]
|
]
|
||||||
|
|
||||||
init(identifier: NSTouchBarItem.Identifier, interval: TimeInterval) {
|
private let interval: TimeInterval
|
||||||
|
private let disableMarquee: Bool
|
||||||
|
private var songTitle: String?
|
||||||
|
private var timer: Timer?
|
||||||
|
private let iconSize = NSSize(width: 21, height: 21)
|
||||||
|
|
||||||
|
|
||||||
|
init(identifier: NSTouchBarItem.Identifier, interval: TimeInterval, disableMarquee: Bool) {
|
||||||
self.interval = interval
|
self.interval = interval
|
||||||
|
self.disableMarquee = disableMarquee
|
||||||
|
|
||||||
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()
|
||||||
}
|
}
|
||||||
@@ -50,21 +66,25 @@ class MusicBarItem: CustomButtonTouchBarItem {
|
|||||||
|
|
||||||
@objc func playPause() {
|
@objc func playPause() {
|
||||||
for ident in playerBundleIdentifiers {
|
for ident in playerBundleIdentifiers {
|
||||||
if let musicPlayer = SBApplication(bundleIdentifier: ident) {
|
if let musicPlayer = SBApplication(bundleIdentifier: ident.rawValue) {
|
||||||
if musicPlayer.isRunning {
|
if musicPlayer.isRunning {
|
||||||
if musicPlayer.className == "SpotifyApplication" {
|
if ident == .Spotify {
|
||||||
let mp = (musicPlayer as SpotifyApplication)
|
let mp = (musicPlayer as SpotifyApplication)
|
||||||
mp.playpause!()
|
mp.playpause!()
|
||||||
return
|
return
|
||||||
} else if musicPlayer.className == "ITunesApplication" {
|
} else if ident == .iTunes {
|
||||||
let mp = (musicPlayer as iTunesApplication)
|
let mp = (musicPlayer as iTunesApplication)
|
||||||
mp.playpause!()
|
mp.playpause!()
|
||||||
return
|
return
|
||||||
} else if musicPlayer.className == "VOXApplication" {
|
} else if ident == .Music {
|
||||||
|
let mp = (musicPlayer as MusicApplication)
|
||||||
|
mp.playpause!()
|
||||||
|
return
|
||||||
|
} else if ident == .VOX {
|
||||||
let mp = (musicPlayer as VoxApplication)
|
let mp = (musicPlayer as VoxApplication)
|
||||||
mp.playpause!()
|
mp.playpause!()
|
||||||
return
|
return
|
||||||
} else if musicPlayer.className == "SafariApplication" {
|
} else if ident == .Safari {
|
||||||
// You must enable the 'Allow JavaScript from Apple Events' option in Safari's Develop menu to use 'do JavaScript'.
|
// You must enable the 'Allow JavaScript from Apple Events' option in Safari's Develop menu to use 'do JavaScript'.
|
||||||
let safariApplication = musicPlayer as SafariApplication
|
let safariApplication = musicPlayer as SafariApplication
|
||||||
let safariWindows = safariApplication.windows?().compactMap({ $0 as? SafariWindow })
|
let safariWindows = safariApplication.windows?().compactMap({ $0 as? SafariWindow })
|
||||||
@@ -84,7 +104,7 @@ class MusicBarItem: CustomButtonTouchBarItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// else if (musicPlayer.className == "GoogleChromeApplication") {
|
// else if (ident == .Chrome) {
|
||||||
// let chromeApplication = musicPlayer as GoogleChromeApplication
|
// let chromeApplication = musicPlayer as GoogleChromeApplication
|
||||||
// let chromeWindows = chromeApplication.windows?().compactMap({ $0 as? GoogleChromeWindow })
|
// let chromeWindows = chromeApplication.windows?().compactMap({ $0 as? GoogleChromeWindow })
|
||||||
// for window in chromeWindows! {
|
// for window in chromeWindows! {
|
||||||
@@ -111,24 +131,29 @@ class MusicBarItem: CustomButtonTouchBarItem {
|
|||||||
|
|
||||||
@objc func nextTrack() {
|
@objc func nextTrack() {
|
||||||
for ident in playerBundleIdentifiers {
|
for ident in playerBundleIdentifiers {
|
||||||
if let musicPlayer = SBApplication(bundleIdentifier: ident) {
|
if let musicPlayer = SBApplication(bundleIdentifier: ident.rawValue) {
|
||||||
if musicPlayer.isRunning {
|
if musicPlayer.isRunning {
|
||||||
if musicPlayer.className == "SpotifyApplication" {
|
if ident == .Spotify {
|
||||||
let mp = (musicPlayer as SpotifyApplication)
|
let mp = (musicPlayer as SpotifyApplication)
|
||||||
mp.nextTrack!()
|
mp.nextTrack!()
|
||||||
updatePlayer()
|
updatePlayer()
|
||||||
return
|
return
|
||||||
} else if musicPlayer.className == "ITunesApplication" {
|
} else if ident == .iTunes {
|
||||||
let mp = (musicPlayer as iTunesApplication)
|
let mp = (musicPlayer as iTunesApplication)
|
||||||
mp.nextTrack!()
|
mp.nextTrack!()
|
||||||
updatePlayer()
|
updatePlayer()
|
||||||
return
|
return
|
||||||
} else if musicPlayer.className == "VOXApplication" {
|
} else if ident == .Music {
|
||||||
|
let mp = (musicPlayer as MusicApplication)
|
||||||
|
mp.nextTrack!()
|
||||||
|
updatePlayer()
|
||||||
|
return
|
||||||
|
} else if ident == .VOX {
|
||||||
let mp = (musicPlayer as VoxApplication)
|
let mp = (musicPlayer as VoxApplication)
|
||||||
mp.next!()
|
mp.next!()
|
||||||
updatePlayer()
|
updatePlayer()
|
||||||
return
|
return
|
||||||
} else if musicPlayer.className == "SafariApplication" {
|
} else if ident == .Safari {
|
||||||
// You must enable the 'Allow JavaScript from Apple Events' option in Safari's Develop menu to use 'do JavaScript'.
|
// You must enable the 'Allow JavaScript from Apple Events' option in Safari's Develop menu to use 'do JavaScript'.
|
||||||
let safariApplication = musicPlayer as SafariApplication
|
let safariApplication = musicPlayer as SafariApplication
|
||||||
let safariWindows = safariApplication.windows?().compactMap({ $0 as? SafariWindow })
|
let safariWindows = safariApplication.windows?().compactMap({ $0 as? SafariWindow })
|
||||||
@@ -156,6 +181,31 @@ class MusicBarItem: CustomButtonTouchBarItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func previousTrack() {
|
||||||
|
for ident in playerBundleIdentifiers {
|
||||||
|
if let musicPlayer = SBApplication(bundleIdentifier: ident.rawValue) {
|
||||||
|
if musicPlayer.isRunning {
|
||||||
|
if ident == .Spotify {
|
||||||
|
let mp = (musicPlayer as SpotifyApplication)
|
||||||
|
mp.previousTrack!()
|
||||||
|
updatePlayer()
|
||||||
|
return
|
||||||
|
} else if ident == .iTunes {
|
||||||
|
let mp = (musicPlayer as iTunesApplication)
|
||||||
|
mp.previousTrack!()
|
||||||
|
updatePlayer()
|
||||||
|
return
|
||||||
|
} else if ident == .Music {
|
||||||
|
let mp = (musicPlayer as MusicApplication)
|
||||||
|
mp.previousTrack!()
|
||||||
|
updatePlayer()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func refreshAndSchedule() {
|
func refreshAndSchedule() {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.updatePlayer()
|
self.updatePlayer()
|
||||||
@@ -169,17 +219,19 @@ 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) {
|
if let musicPlayer = SBApplication(bundleIdentifier: ident.rawValue) {
|
||||||
if musicPlayer.isRunning {
|
if musicPlayer.isRunning {
|
||||||
var tempTitle = ""
|
var tempTitle = ""
|
||||||
if musicPlayer.className == "SpotifyApplication" {
|
if ident == .Spotify {
|
||||||
tempTitle = (musicPlayer as SpotifyApplication).title
|
tempTitle = (musicPlayer as SpotifyApplication).title
|
||||||
} else if musicPlayer.className == "ITunesApplication" {
|
} else if ident == .iTunes {
|
||||||
tempTitle = (musicPlayer as iTunesApplication).title
|
tempTitle = (musicPlayer as iTunesApplication).title
|
||||||
} else if musicPlayer.className == "VOXApplication" {
|
} else if ident == .Music {
|
||||||
|
tempTitle = (musicPlayer as MusicApplication).title
|
||||||
|
} else if ident == .VOX {
|
||||||
tempTitle = (musicPlayer as VoxApplication).title
|
tempTitle = (musicPlayer as VoxApplication).title
|
||||||
} else if musicPlayer.className == "SafariApplication" {
|
} else if ident == .Safari {
|
||||||
let safariApplication = musicPlayer as SafariApplication
|
let safariApplication = musicPlayer as SafariApplication
|
||||||
let safariWindows = safariApplication.windows?().compactMap({ $0 as? SafariWindow })
|
let safariWindows = safariApplication.windows?().compactMap({ $0 as? SafariWindow })
|
||||||
for window in safariWindows! {
|
for window in safariWindows! {
|
||||||
@@ -199,10 +251,7 @@ class MusicBarItem: CustomButtonTouchBarItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if tempTitle == "" {
|
} else if ident == .Chrome {
|
||||||
ident = ""
|
|
||||||
}
|
|
||||||
} else if musicPlayer.className == "GoogleChromeApplication" {
|
|
||||||
let chromeApplication = musicPlayer as GoogleChromeApplication
|
let chromeApplication = musicPlayer as GoogleChromeApplication
|
||||||
let chromeWindows = chromeApplication.windows?().compactMap({ $0 as? GoogleChromeWindow })
|
let chromeWindows = chromeApplication.windows?().compactMap({ $0 as? GoogleChromeWindow })
|
||||||
for window in chromeWindows! {
|
for window in chromeWindows! {
|
||||||
@@ -222,9 +271,6 @@ class MusicBarItem: CustomButtonTouchBarItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if tempTitle == "" {
|
|
||||||
ident = ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if tempTitle == self.songTitle {
|
if tempTitle == self.songTitle {
|
||||||
@@ -234,16 +280,22 @@ class MusicBarItem: CustomButtonTouchBarItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let songTitle = self.songTitle?.ifNotEmpty {
|
if let songTitle = self.songTitle?.ifNotEmpty {
|
||||||
self.title = " " + songTitle + " "
|
|
||||||
titleUpdated = true
|
|
||||||
self.timer?.invalidate()
|
self.timer?.invalidate()
|
||||||
self.timer = nil
|
self.timer = nil
|
||||||
|
|
||||||
|
if (disableMarquee) {
|
||||||
|
self.title = " " + songTitle
|
||||||
|
} else {
|
||||||
|
self.title = " " + songTitle + " "
|
||||||
self.timer = Timer.scheduledTimer(timeInterval: 0.25, target: self, selector: #selector(self.marquee), userInfo: nil, repeats: true)
|
self.timer = Timer.scheduledTimer(timeInterval: 0.25, target: self, selector: #selector(self.marquee), userInfo: nil, repeats: true)
|
||||||
}
|
}
|
||||||
if let ident = ident.ifNotEmpty,
|
|
||||||
let appPath = NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: ident) {
|
titleUpdated = true
|
||||||
|
}
|
||||||
|
if let _ = tempTitle.ifNotEmpty,
|
||||||
|
let appPath = NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: ident.rawValue) {
|
||||||
let image = NSWorkspace.shared.icon(forFile: appPath)
|
let image = NSWorkspace.shared.icon(forFile: appPath)
|
||||||
image.size = self.buttonSize
|
image.size = self.iconSize
|
||||||
self.image = image
|
self.image = image
|
||||||
iconUpdated = true
|
iconUpdated = true
|
||||||
}
|
}
|
||||||
@@ -310,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()
|
||||||
|
|||||||
@@ -31,7 +31,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 +64,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()
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class NightShiftBarItem: CustomButtonTouchBarItem {
|
|||||||
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: [:]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -135,4 +135,8 @@ class WeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate {
|
|||||||
// print("inside didChangeAuthorization ");
|
// print("inside didChangeAuthorization ");
|
||||||
updateWeather()
|
updateWeather()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
activity.invalidate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
//
|
||||||
|
// YandexWeatherBarItem.swift
|
||||||
|
// MTMR
|
||||||
|
//
|
||||||
|
// Created by bobrosoft on 22/07/2019.
|
||||||
|
// Copyright © 2018 Anton Palgunov. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Cocoa
|
||||||
|
import CoreLocation
|
||||||
|
|
||||||
|
class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate {
|
||||||
|
private let activity: NSBackgroundActivityScheduler
|
||||||
|
private let unitsStr = "°C"
|
||||||
|
private let iconsSource = [
|
||||||
|
"Ясно": "☀️", "Малооблачно": "🌤", "Облачно с прояснениями": "⛅️", "Пасмурно": "☁️", "Небольшой дождь": "🌦", "Дождь": "🌧", "Ливень": "⛈", "Гроза": "🌩", "Дождь с грозой": "⛈", "Дождь со снегом": "☔️", "Небольшой снег": "❄️", "Снег": "🌨", "Туман": "🌫",
|
||||||
|
"Clear": "☀️", "Mostly clear": "🌤", "Partly cloudy": "⛅️", "Overcast": "☁️", "Light rain": "🌦", "Rain": "🌧", "Heavy rain": "⛈", "Storm": "🌩", "Thunderstorm with rain": "⛈", "Sleet": "☔️", "Light snow": "❄️", "Snow": "🌨", "Fog": "🌫"
|
||||||
|
]
|
||||||
|
private var location: CLLocation!
|
||||||
|
private var prevLocation: CLLocation!
|
||||||
|
private var manager: CLLocationManager!
|
||||||
|
private var updateWeatherTask: URLSessionDataTask?
|
||||||
|
|
||||||
|
init(identifier: NSTouchBarItem.Identifier, interval: TimeInterval) {
|
||||||
|
activity = NSBackgroundActivityScheduler(identifier: "\(identifier.rawValue).updatecheck")
|
||||||
|
activity.interval = interval
|
||||||
|
|
||||||
|
super.init(identifier: identifier, title: "⏳")
|
||||||
|
|
||||||
|
let status = CLLocationManager.authorizationStatus()
|
||||||
|
if status == .restricted || status == .denied {
|
||||||
|
print("User permission not given")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !CLLocationManager.locationServicesEnabled() {
|
||||||
|
print("Location services not enabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
activity.repeats = true
|
||||||
|
activity.qualityOfService = .utility
|
||||||
|
activity.schedule { (completion: NSBackgroundActivityScheduler.CompletionHandler) in
|
||||||
|
self.updateWeather()
|
||||||
|
completion(NSBackgroundActivityScheduler.Result.finished)
|
||||||
|
}
|
||||||
|
updateWeather()
|
||||||
|
|
||||||
|
manager = CLLocationManager()
|
||||||
|
manager.delegate = self
|
||||||
|
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
|
||||||
|
manager.startUpdatingLocation()
|
||||||
|
|
||||||
|
if actions.filter({ $0.trigger == .singleTap }).isEmpty {
|
||||||
|
actions.append(ItemAction(
|
||||||
|
trigger: .singleTap,
|
||||||
|
defaultTapAction
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder _: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func updateWeather() {
|
||||||
|
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
|
||||||
|
|
||||||
|
updateWeatherTask?.cancel()
|
||||||
|
updateWeatherTask = URLSession.shared.dataTask(with: urlRequest) { data, _, error in
|
||||||
|
guard error == nil, let response = data?.utf8string else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// print(response)
|
||||||
|
|
||||||
|
var matches: [[String]]
|
||||||
|
var temperature: String?
|
||||||
|
matches = response.matchingStrings(regex: "fact__temp.*?temp__value.*?>(.*?)<")
|
||||||
|
temperature = matches.first?.item(at: 1)
|
||||||
|
|
||||||
|
var icon: String?
|
||||||
|
matches = response.matchingStrings(regex: "link__condition.*?>(.*?)<")
|
||||||
|
icon = matches.first?.item(at: 1)
|
||||||
|
if let _ = icon, let test = self.iconsSource[icon!] {
|
||||||
|
icon = test
|
||||||
|
}
|
||||||
|
|
||||||
|
if temperature != nil {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.setWeather(text: "\(icon ?? "?") \(temperature!)\(self.unitsStr)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWeatherTask?.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWeatherUrl() -> String {
|
||||||
|
if location != nil {
|
||||||
|
return "https://yandex.ru/pogoda/?lat=\(location.coordinate.latitude)&lon=\(location.coordinate.longitude)&lang=ru"
|
||||||
|
} else {
|
||||||
|
return "https://yandex.ru/pogoda/?lang=ru" // Yandex will try to determine your location by default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setWeather(text: String) {
|
||||||
|
title = text
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultTapAction() {
|
||||||
|
print(getWeatherUrl())
|
||||||
|
if let url = URL(string: getWeatherUrl()) {
|
||||||
|
NSWorkspace.shared.open(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||||
|
let lastLocation = locations.last!
|
||||||
|
location = lastLocation
|
||||||
|
if prevLocation == nil {
|
||||||
|
updateWeather()
|
||||||
|
}
|
||||||
|
prevLocation = lastLocation
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationManager(_: CLLocationManager, didFailWithError error: Error) {
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func locationManager(_: CLLocationManager, didChangeAuthorization _: CLAuthorizationStatus) {
|
||||||
|
updateWeather()
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
activity.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension String {
|
||||||
|
func matchingStrings(regex: String) -> [[String]] {
|
||||||
|
guard let regex = try? NSRegularExpression(pattern: regex, options: []) else { return [] }
|
||||||
|
let nsString = self as NSString
|
||||||
|
let results = regex.matches(in: self, options: [], range: NSMakeRange(0, nsString.length))
|
||||||
|
return results.map { result in
|
||||||
|
(0 ..< result.numberOfRanges).map {
|
||||||
|
result.range(at: $0).location != NSNotFound
|
||||||
|
? nsString.substring(with: result.range(at: $0))
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Array {
|
||||||
|
func item(at index: Int) -> Element? {
|
||||||
|
return indices.contains(index) ? self[index] : nil
|
||||||
|
}
|
||||||
|
}
|
||||||
+94
-67
@@ -1,82 +1,109 @@
|
|||||||
[
|
[
|
||||||
{ "type": "escape", "align": "left" },
|
|
||||||
{ "type": "exitTouchbar", "width": 44, "align": "left" },
|
|
||||||
{ "type": "brightnessDown", "width": 44, "align": "left" },
|
|
||||||
{
|
{
|
||||||
"type": "brightness",
|
"type": "escape",
|
||||||
"width": 60,
|
"width": 64,
|
||||||
"align": "left",
|
"align": "left"
|
||||||
"image": {
|
|
||||||
"base64":
|
|
||||||
"iVBORw0KGgoAAAANSUhEUgAAAIAAAACAAQMAAAD58POIAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAGUExURffLOPfLNyaSVzUAAAACdFJOU/kBxOqnWgAAAbJJREFUSMfVljtyhDAQBVulQCFH4CgcDR1NR9ERFBKoeA5GfGddtkNvwFINFKP5tED22+Zxwviv6QVKfIEc/iNoF5gkpLIeYI8SUp4PsAUJiekADQntF6isQjvxCTrhAJlFqMMBeIH9BMsD7DAb2BhvYbIyNAOCZIWqYKGTpDZJFQu9EKVd44RxQRq3IrULWD62C8wSssWUZEsR0k6wcDOrJZmoBpMKI+s5qkBQCQOUJADVOECdOsDS0gDbsgHMfT4rVwHSrZQFIN5ABka8BgDgAeZ+BztBgvUEnSgVlhNsTFJjvoF5HAZorBpdYKAiSRbqNyBIUr6AjZMdPwO72R40MElS+wZUWA+wQ6LAYkFvdIhkmA+wQSDDdIAGAZ6A34H0x0fca11gBZZsIHSIfnE/5+NjCn/OuiuUB+/aunZwDeNayjXdTpDN0wlY+r1PfWu75nfj8RogN2JuCN2Y5qgMwTI0wGPUnQw6Qarx0sVNKA5Mn6VUL22lIbZoYitDbPmlvocc9Umfl2D7adz1reC3pF8av4m+DCenp/ndZuG3E7fhuC3pH2+vnz8V3MfE+bnxBTXuuIMTrLWHAAAAAElFTkSuQmCC"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{ "type": "brightnessUp", "width": 44, "align": "left" },
|
{
|
||||||
// --- iTunes ---
|
"type": "dnd",
|
||||||
|
"align": "left",
|
||||||
|
"width": 38
|
||||||
|
},
|
||||||
|
{ "type": "brightnessDown", "width": 32, "bordered": false, "align": "left" },
|
||||||
|
{ "type": "brightnessUp", "width": 32, "bordered": false, "align": "left" },
|
||||||
|
|
||||||
|
// Spotify
|
||||||
{
|
{
|
||||||
"type": "appleScriptTitledButton",
|
"type": "appleScriptTitledButton",
|
||||||
"source": {
|
"source": {
|
||||||
"inline":
|
"inline": "if application \"Spotify\" is running then\rtell application \"Spotify\"\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"
|
||||||
"if application \"iTunes\" is running then\rtell application \"iTunes\"\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",
|
"action": "appleScript",
|
||||||
"actionAppleScript": {
|
"actionAppleScript": {
|
||||||
"inline":
|
"inline": "if application \"Spotify\" is running then\rtell application \"Spotify\"\rif player state is playing then\rnext track\rend if\rend tell\rend if\r"
|
||||||
"if application \"iTunes\" is running then\rtell application \"iTunes\"\rif player state is playing then\rnext track\rend if\rend tell\rend if\r"
|
|
||||||
},
|
},
|
||||||
"refreshInterval": 1,
|
"refreshInterval": 1,
|
||||||
"image": {
|
"image": {
|
||||||
"base64":
|
"base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAYUExURUdwTB3WXx3UXh3VXx7XYBkXFRpVLRyURmIaeAQAAAAEdFJOUwDDO3fSqUUkAAABbklEQVRIx61VbW6DMAztoAeYNA7ApB6gkzhAWS24wIAL0HABaK6/pHFNEhy8TXu/kPzkPD/8cTj8K7KPAqB+K5NhQPCUrABCXe7HOUYYZxgVRLiG8RfY4DUgFFtC7cffAfZTFBwBdhWEKfgEq4ocEjgj4ZQifO6/QG9kkETp1dDeVWfRKx3XYSW0LoqY5kCElXDrQkyeCCuh6WL0M4nIWQIyzqixdfKU1koFDKvyCA8NJMzU4xiD+b4kfHRpsIyKc6hBwjVptFHVY51EMAINNDFGJITKDNQcdpX74Hz0CQ3rY5qwMp4EIxrlafzrsYZ2Veb0DkRgfNCUok4Y1fqEijfyi2b8RE9beWqa48Y/uvCNMcH9btfUi+/CGLR1vhL6Zz9N/vBlaCU+7lwY/cmJ67Ryen/2tj23PLqJBodZH8vgj544vOL4pxfI5acrSFxi8hrkU9TSKr78ZpnL50A8KPJJEo+afBblwyqf5j/iGys5j6ScrST2AAAAAElFTkSuQmCC"
|
||||||
"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=="
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// --- Spotify ---
|
|
||||||
// {
|
// Music
|
||||||
// "type": "appleScriptTitledButton",
|
{
|
||||||
// "source": {
|
"type": "appleScriptTitledButton",
|
||||||
// "inline":
|
"source": {
|
||||||
// "if application \"Spotify\" is running then\rtell application \"Spotify\"\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"
|
"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",
|
"action": "appleScript",
|
||||||
// "actionAppleScript": {
|
"actionAppleScript": {
|
||||||
// "inline":
|
"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"
|
||||||
// "if application \"Spotify\" is running then\rtell application \"Spotify\"\rif player state is playing then\rnext track\rend if\rend tell\rend if\r"
|
},
|
||||||
// },
|
"longAction": "appleScript",
|
||||||
// "refreshInterval": 1,
|
"longActionAppleScript": {
|
||||||
// "image": {
|
"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"
|
||||||
// "base64":
|
},
|
||||||
// "iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAYUExURUdwTB3WXx3UXh3VXx7XYBkXFRpVLRyURmIaeAQAAAAEdFJOUwDDO3fSqUUkAAABbklEQVRIx61VbW6DMAztoAeYNA7ApB6gkzhAWS24wIAL0HABaK6/pHFNEhy8TXu/kPzkPD/8cTj8K7KPAqB+K5NhQPCUrABCXe7HOUYYZxgVRLiG8RfY4DUgFFtC7cffAfZTFBwBdhWEKfgEq4ocEjgj4ZQifO6/QG9kkETp1dDeVWfRKx3XYSW0LoqY5kCElXDrQkyeCCuh6WL0M4nIWQIyzqixdfKU1koFDKvyCA8NJMzU4xiD+b4kfHRpsIyKc6hBwjVptFHVY51EMAINNDFGJITKDNQcdpX74Hz0CQ3rY5qwMp4EIxrlafzrsYZ2Veb0DkRgfNCUok4Y1fqEijfyi2b8RE9beWqa48Y/uvCNMcH9btfUi+/CGLR1vhL6Zz9N/vBlaCU+7lwY/cmJ67Ryen/2tj23PLqJBodZH8vgj544vOL4pxfI5acrSFxi8hrkU9TSKr78ZpnL50A8KPJJEo+afBblwyqf5j/iGys5j6ScrST2AAAAAElFTkSuQmCC"
|
"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=="
|
||||||
// --- VOX ---
|
}
|
||||||
// {
|
},
|
||||||
// "type": "appleScriptTitledButton",
|
|
||||||
// "source": {
|
// iTunes
|
||||||
// "inline":
|
{
|
||||||
// "if application \"VOX\" is running then\rtell application \"VOX\"\rif player state is 1 then\rreturn (get artist) & \" – \" & (get track)\relse\rreturn \"\"\rend if\rend tell\rend if\rreturn \"\"\r"
|
"type": "appleScriptTitledButton",
|
||||||
// },
|
"source": {
|
||||||
// "action": "appleScript",
|
"inline": "if application \"iTunes\" is running then\rtell application \"iTunes\"\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"
|
||||||
// "actionAppleScript": {
|
},
|
||||||
// "inline":
|
"action": "appleScript",
|
||||||
// "if application \"VOX\" is running then\rtell application \"VOX\"\rif player state is 1 then\rnext\rend if\rend tell\rend if\r"
|
"actionAppleScript": {
|
||||||
// },
|
"inline": "if application \"iTunes\" is running then\rtell application \"iTunes\"\rif player state is playing then\rnext track\rend if\rend tell\rend if\r"
|
||||||
// "refreshInterval": 1,
|
},
|
||||||
// "image": {
|
"longAction": "appleScript",
|
||||||
// "base64":
|
"longActionAppleScript": {
|
||||||
// "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAADAUExURUdwTHd7gqeqtKClrd/k7n+Di7e7xI6TnNHX4cPI0Xp9hJibpOfs9u/1/vX7/rq/yZiborK3wEpOUxgYFlFXYiIhHycpLD9ESjI0NgsLCj09O4SKk19lcbBkALhoAHJ3gT8rAzMvJJugqKhcAL5xAOSxB4I/AJ5SAGpweXuAiZJJAG8zAeXr9WNiYM+NANqeAMd9APHACfr9/e7JPrO5w6mrsA4bKC0gCHhtTyAYBFcpAZduKK2JP/fost/Eb5SUlFFfi9AAAAASdFJOUwDJE9FgWjb+/vyHppTXtbn9mDupvZ0AAAdoSURBVFjDlZcLQ6O6FoWrtgI+Owl5EGgp2DrEgnV6wD4c7f//V3ftAE7nzOj17NGxpayPtXfSZGcw+CjOh1c3N9eIm5ur4fngv8Xp8KZE+F2U5bq8GZ5+WU5qP6jzsRdTeF7e1AEoN8Ovya/LMsg9mxZJErpIksJarwnK8nr4FblfQ52EDOH0zL1KUuvVfnn9eTVOryCPoVZQuGj/FOApMAhx9Uktzq/L2ksT3FuQhIVVVVRVRa+SogA1KbzgExPD0s/xdBOmKdOqODR95IdEaZba0LAkbfzyg0rA/pgekxZKV4cmtxXTEmFUEufNodIKYJjLkcbf9UEM90mqZJU3qeJRxDkXAv/hlbJNXkmVJjCBNP5CGPq1TZSyiWB5XomIi+woRBSJKj8YUVgFQ4E//FMfQM8s40VTyIhDpLVRNIJKaWLwSNqm4mEMl/YPwrkP/9AbER+UkxtWbbcvLrbbkBlCcDa23BABHn4bi1PSM2a1PFgZwbwJty+TybSLyeRlyxQlIuNY6FgpDKd/PB+uXP2tFHEheCtvhW3Q65dtiEyQRixMrBjG4uo4gTpNFApvU9Kr6sWp77qY3BFj8lLBBJdpykOLwaqPkgioAEnFi1RCz7Z3uJ2U37sgCBB3W5ZJLm0VJQmVIXgfgSAvQpYK1ACDx7bTOT2dlA8uWsYEl0EQ3KDUKcOECvqRCGgGpApXf9M/PNx3QQwyMZ9uGWZWiGelsFAHvQFUMEx4WAku2HZC+k6+vF/ip0U4wmSrBJcFZgss9xZuYYAlWhaGC1PhMb1++R6EaAl3lRFcFUI5C7duDlAFwpCzSogsfCH93fdWvm+jRYCASs5fwkyIinGsVrBAc2FYxwVLlKhgQG3n8+kv/WwjhNhsZvveQ5+EqrhKWOHVlMNtgyUkhHkpdPjy1CZA+pl4bkOIfWcBQ/EEC1xWWmKxsi6HussA9WUwQBV0BmbPz3y32+x2nD8/z/okpvM5LAjGeEg51JiFtQeAEqHhWUIGJq0B0m9mLjYdgSxMnQVhGHIIkcP5YNjEAGjJJDfF5AnzzRnY/9K3hP3SjSUAk0oLzaQOwyJGEb41FgWVuOIymHYZCKfPGFaxzBHEcQ5cYrXD3LHNt8E3WkdDbpBY8vMdMHsW0Jui9v26MCDAggNMAfgZoghGdIDbPMXewY0RWfHzqQMsN2QgC/MAkVcZWZi1DuYAJJlQBlVM0vx2cDZOw4QAPEt/AcTzDgkkh3w8Hh8SPZvtKIc2haefKapo2mE4G5x5nQOuLQA0jwggCcAqF1SFnZQEwEwCwGZ0P+sAXQqa69gBJvgeLCUBhMS6arSWogV0NWwB2gHG5KAggDYO0OXQATghJP4QIPsFiJHCEYB2YgBE5gBzNxEyjVHYcOxKKDfquRE6u39wFXh6dADJaccG4HLsANIIEb89PnaEvSYLG2kYAG0G+3YMnh4fDxbfe4wkAS47gAAyShsA2iSWUgsiYHfJMCF2ZKDVA9AkET4WbhgvB6OxxeoiuZZRVT/2BFjQfOemMX53qNn+e5fA42OtAOCYiYXNR4PznL4LJoLA1G8rQjjCTCmxc9+GzU4oNXvonr9avTUZPc7QdyHH0j4mgIqQVZQHq1VvAgRKf7MRKISZPWAG0ONXq1Vg6WYahMIbYz24pJ6ECYIyf7EiBBGm3/da0c6KX/ifTnv9P74iu66G3iUAI4+KoCNpeNS8rha9ifnkfmlcLO+n816+WL3mEVVcY2ew4xEtqsgBjUWEQkWsfFssWhdgPM0xoBg8iKl2JF8s3kpngCtMo3jsNljkUDAmYQFVWC9+tAgHeQ93AR/8WKw9d6MMWWFdBn0OJsJsjERZLn4QomOsWmmrpk9Kn0eYxpGiMfBG7dZ0FrcW8B2N1PoV9zkEUehn1b2hy68lxg/F0owVaXzW7Y0j6kwZuiqhUIaW8M54F9Olcq0irF0R1vAkfTdAFmwaKhjDUgfCur39n+NwF9ZrFhnMWm5YiM737L0/GHkxtYD4RKIWILz1+jf86/Vv69I4fSTRTNojAxgIdPUgCE7VjXhzhHAEJ1/nUUSl4qTHKeDyuMnCycCzjiCwyUYsWK9fofzhxIC8rtcBi2SlqPeEnu7/rekGwcutwhTHhKoYHlWvEa8u6FVN17D/Or3Ncfu/Wu5R7I2bmNECRiYYRpuaYtKiBa+0u0j9K7aguMFZZvTvVvUCFuoce5ygSmA9xlcRDR8iyzaZqVq50Coc1zBw8WezfBEfmqBGX+8QXLs1HY9tV3aMMeQSp4A6aP6qdx4aNHyMWnxBHbpwzTIW9vadkGTf/1BPdYhxsqrR7zuGoD4/6pp+iKVkaY3zWGxHHx55TuJxXZa1RSmwIyAyeoGVlf4wG9CBLD755Nx1ehHHub9e+3lBpxUh+0DuuO4ef/H5AXR0gkpg+EofjQed1ej8VsTuEk6T8cno/54cRyc2zms3Bcr+4Esvazz9C/IOYWlESNseoWtU3n5V3tYCDEA8tAce5j3enFyc/scT/Pno4qSLi9HHhf8f8eOIRf9lFswAAAAASUVORK5CYII="
|
"inline": "if application \"iTunes\" is running then\rtell application \"iTunes\"\rif player state is playing then\rprev track\rend if\rend tell\rend if\r"
|
||||||
// }
|
},
|
||||||
// },
|
"refreshInterval": 2,
|
||||||
{ "type": "previous", "width": 44 },
|
"image": {
|
||||||
{ "type": "play", "width": 44 },
|
"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=="
|
||||||
{ "type": "next", "width": 44 },
|
}
|
||||||
{ "type": "weather", "icon_type": "images", "units": "metric" },
|
},
|
||||||
{ "type": "currency", "from": "BTC", "to": "USD" },
|
{ "type": "displaySleep", "width": 40, "align": "right", "bordered": false },
|
||||||
{ "type": "sleep", "width": 44 },
|
{
|
||||||
{ "type": "mute", "width": 40, "align": "right" },
|
"type": "weather",
|
||||||
{ "type": "volumeDown", "width": 34, "align": "right" },
|
"align": "right",
|
||||||
{ "type": "volume", "width": 60, "align": "right" },
|
"icon_type": "images",
|
||||||
{ "type": "volumeUp", "width": 34, "align": "right" },
|
"api_key": "ca93a0bb8cdb428552660d83249e4bc9",
|
||||||
{ "type": "inputsource", "align": "right" },
|
"bordered": false
|
||||||
{ "type": "battery", "align": "right", "bordered": false },
|
},
|
||||||
{ "type": "timeButton", "align": "right", "bordered": false }
|
{
|
||||||
|
"type": "volumeDown",
|
||||||
|
"bordered": false,
|
||||||
|
"align": "right",
|
||||||
|
"width": 28
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "volumeUp",
|
||||||
|
"bordered": false,
|
||||||
|
"align": "right",
|
||||||
|
"width": 28
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "play",
|
||||||
|
"align": "right",
|
||||||
|
"width": 38
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "battery",
|
||||||
|
"align": "right",
|
||||||
|
"bordered": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "timeButton",
|
||||||
|
"formatTemplate": "HH:mm",
|
||||||
|
"align": "right",
|
||||||
|
"bordered": false,
|
||||||
|
"longAction": "shellScript",
|
||||||
|
"longExecutablePath": "/usr/bin/pmset",
|
||||||
|
"longShellArguments": ["sleepnow"]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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,13 +10,28 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testButtonKeyCodeAction() {
|
func testButtonKeyCodeAction() {
|
||||||
|
let buttonKeycodeFixture = """
|
||||||
|
[ { "type": "staticButton", "title": "Pew", "actions": [ { "trigger": "singleTap", "action": "hidKey", "keycode": 123 } ] } ]
|
||||||
|
""".data(using: .utf8)!
|
||||||
|
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonKeycodeFixture)
|
||||||
|
guard case .staticButton("Pew")? = result?.first?.type else {
|
||||||
|
XCTFail()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard case .hidKey(keycode: 123)? = result?.first?.actions.filter({ $0.trigger == .singleTap }).first?.value else {
|
||||||
|
XCTFail()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testButtonKeyCodeLegacyAction() {
|
||||||
let buttonKeycodeFixture = """
|
let buttonKeycodeFixture = """
|
||||||
[ { "type": "staticButton", "title": "Pew", "action": "hidKey", "keycode": 123 } ]
|
[ { "type": "staticButton", "title": "Pew", "action": "hidKey", "keycode": 123 } ]
|
||||||
""".data(using: .utf8)!
|
""".data(using: .utf8)!
|
||||||
@@ -25,7 +40,7 @@ class ParseConfig: XCTestCase {
|
|||||||
XCTFail()
|
XCTFail()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
guard case .hidKey(keycode: 123)? = result?.first?.action else {
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,64 @@
|
|||||||
<p align="center"><img src="Resources/logo.png" width="120"></p>
|
# My touchbar. My rules. [](https://github.com/Toxblh/MTMR/releases) [](https://github.com/Toxblh/MTMR/blob/master/LICENSE)  
|
||||||
|
|
||||||
# My TouchBar. My rules
|
<img src="Resources/logo.png" align="right"
|
||||||
|
title="MTMR by Toxblh" width="110" height="110">
|
||||||
|
|
||||||
*The TouchBar Customization App for your MacBook Pro*
|
_The TouchBar Customization App for your MacBook Pro_
|
||||||
|
|
||||||
[](https://github.com/Toxblh/MTMR/releases)
|
My idea is to create a platform for creating plugins to customize the TouchBar. I very much like BTT and having a full custom TouchBar (my BTT preset), and I wanted to create it. It's my first Swift project for MacOS :)
|
||||||
[](https://github.com/Toxblh/MTMR/blob/master/LICENSE) [](https://github.com/Toxblh/MTMR/releases/latest)  
|
|
||||||
|
**Share your presets [here](https://github.com/Toxblh/MTMR-presets)**
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="Resources/TouchBar-v0.8.1.png">
|
<img src="./Resources/aaaaa-acc6-17fee7572ed0.png" alt="Mackbook with touchbar" width="800">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
**MTMR** Community:
|
<p align="center">
|
||||||
[<img height="24px" src="https://camo.githubusercontent.com/88f53948f291c54736bf08f5fd7b037a848dfc62/68747470733a2f2f646973636f72646170702e636f6d2f6173736574732f30376463613830613130326434313439653937333664346231363263666636662e69636f"> Discord](https://discord.gg/CmNcDuQ)
|
<a href="https://discord.gg/CmNcDuQ"><img height="20px" src="https://camo.githubusercontent.com/88f53948f291c54736bf08f5fd7b037a848dfc62/68747470733a2f2f646973636f72646170702e636f6d2f6173736574732f30376463613830613130326434313439653937333664346231363263666636662e69636f"> Discord</a>
|
||||||
[<img height="24px" src="https://telegram.org/img/t_logo.png" /> Telegram](https://t.me/joinchat/AmVYGg8vW38c13_3MxdE_g)
|
<a href="https://t.me/joinchat/AmVYGg8vW38c13_3MxdE_g"><img height="20px" src="https://telegram.org/img/t_logo.png" /> Telegram</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<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.me/toxblh/10" 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">
|
||||||
<img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=my-touchbar-my-rules-mtmr&theme=light" alt="My TouchBar My Rules (MTMR)" height="36px" style="max-width:100%">
|
<img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=my-touchbar-my-rules-mtmr&theme=light" alt="My TouchBar My Rules (MTMR)" height="36px" style="max-width:100%">
|
||||||
</a>
|
</a></p>
|
||||||
|
|
||||||
|
|
||||||
My idea is to create a platform for creating plugins to customize the TouchBar. I very much like BTT and having a full custom TouchBar (my [BTT preset](https://github.com/Toxblh/btt-touchbar-preset)), and I wanted to create it. It's my first Swift project for MacOS :)
|
|
||||||
|
|
||||||
#### Share your presets [here](https://github.com/Toxblh/MTMR-presets)
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
- Download lastest [release](https://github.com/Toxblh/MTMR/releases) from github
|
|
||||||
|
- Download lastest [release](https://github.com/Toxblh/MTMR/releases) (.dmg) from github
|
||||||
- Or via Homebrew `brew cask install mtmr`
|
- Or via Homebrew `brew cask install 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
|
||||||
|
|
||||||
> First open: you should allow access for MTMR in Accessibility for correct buttons work like <kbd>Esc</kbd> Volume/Brightness and another system keys
|
**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
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|
||||||
|
<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">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
🍏→ System Preferences → Security and Privacy → tab Privacy → Accessibility → MTMR
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
[MTMR presets](https://github.com/Toxblh/MTMR-presets)
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="./Resources/Artboard.png" alt="Presets for touchbar" width="800">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
MTMR preferences are stored under `~/Library/Application\ Support/MTMR/items.json`.
|
||||||
|
|
||||||
|
The pre-installed configuration contains less or more than you'll probably want, try to configure:
|
||||||
|
|
||||||
## Built-in button types:
|
## Built-in button types:
|
||||||
|
|
||||||
> Buttons
|
> Buttons
|
||||||
|
|
||||||
- escape
|
- escape
|
||||||
- exitTouchbar
|
- exitTouchbar
|
||||||
- brightnessUp
|
- brightnessUp
|
||||||
@@ -62,9 +70,12 @@ Re-tick or check a tick for access 🍏→ System Preferences → Security and P
|
|||||||
- mute
|
- mute
|
||||||
|
|
||||||
> Native Plugins
|
> Native Plugins
|
||||||
|
|
||||||
|
- timeButton
|
||||||
- battery
|
- battery
|
||||||
- currency
|
- currency
|
||||||
- weather
|
- weather
|
||||||
|
- yandexWeather
|
||||||
- inputsource
|
- inputsource
|
||||||
- music (tap for pause, longTap for next)
|
- music (tap for pause, longTap for next)
|
||||||
- dock (half-long click to open app, full-long click to kill app)
|
- dock (half-long click to open app, full-long click to kill app)
|
||||||
@@ -73,52 +84,143 @@ Re-tick or check a tick for access 🍏→ System Preferences → Security and P
|
|||||||
- darkMode
|
- darkMode
|
||||||
- pomodoro
|
- pomodoro
|
||||||
- network
|
- network
|
||||||
|
- upnext (Calendar events)
|
||||||
|
|
||||||
> Media Keys
|
> Media Keys
|
||||||
|
|
||||||
- previous
|
- previous
|
||||||
- play
|
- play
|
||||||
- next
|
- next
|
||||||
|
|
||||||
> AppleScript plugins
|
> AppleScript plugins
|
||||||
|
|
||||||
- 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
|
||||||
- volume
|
- volume
|
||||||
|
|
||||||
### 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: appleScriptTitledButton can change its icon. To do it, you need to do the following things:
|
||||||
|
1. Declarate 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 return also colors using escape sequences (read more here https://misc.flogisoft.com/bash/tip_colors_and_formatting)
|
||||||
|
> Only "16 Colors" mode supported atm. If background color returned, button will pick it up as own background color.
|
||||||
|
|
||||||
|
Example of "CPU load" button which also changes color based on load value.
|
||||||
|
```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",
|
||||||
@@ -126,11 +228,15 @@ Re-tick or check a tick for access 🍏→ System Preferences → Security and P
|
|||||||
"bordered": true,
|
"bordered": true,
|
||||||
"title": "stats",
|
"title": "stats",
|
||||||
"items": [
|
"items": [
|
||||||
{ "type": "play" }, { "type": "mute" }, ...]
|
{ "type": "play" },
|
||||||
|
{ "type": "mute" },
|
||||||
|
...
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
To close a group, use the button:
|
To close a group, use the button:
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
"type": "close",
|
"type": "close",
|
||||||
@@ -139,18 +245,31 @@ To close a group, use the button:
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Native plugins
|
## Native plugins
|
||||||
|
|
||||||
#### `timeButton`
|
#### `timeButton`
|
||||||
|
|
||||||
> Attention! Works not all: https://en.wikipedia.org/wiki/List_of_time_zone_abbreviations
|
> Attention! Works not all: 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"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `weather`
|
#### `weather`
|
||||||
> Provider: https://openweathermap.org Need allowance location service
|
|
||||||
|
> Provider: https://openweathermap.org \
|
||||||
|
> Note: you need to register on https://openweathermap.org to get your API key \
|
||||||
|
> Note: you may need to wait for near 20 mins until your API key will be activated by Openweathermap \
|
||||||
|
> Note: you need to allow using "Location Services" in your Mac OS "Security & Privacy" settings for MTMR
|
||||||
|
|
||||||
```js
|
```js
|
||||||
"type": "weather",
|
"type": "weather",
|
||||||
"refreshInterval": 600, // in seconds
|
"refreshInterval": 600, // in seconds
|
||||||
@@ -159,8 +278,20 @@ To close a group, use the button:
|
|||||||
"api_key": "" // you can get the key on openweather
|
"api_key": "" // you can get the key on openweather
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### `yandexWeather` (experimental)
|
||||||
|
|
||||||
|
> Provider: https://yandex.ru/pogoda. One click to open up weather forecast in your browser. \
|
||||||
|
> Note: you need to allow using "Location Services" in your Mac OS "Security & Privacy" settings for MTMR
|
||||||
|
|
||||||
|
```js
|
||||||
|
"type": "yandexWeather",
|
||||||
|
"refreshInterval": 600 // in seconds
|
||||||
|
```
|
||||||
|
|
||||||
#### `currency`
|
#### `currency`
|
||||||
|
|
||||||
> Provider: https://coinbase.com
|
> Provider: https://coinbase.com
|
||||||
|
|
||||||
```js
|
```js
|
||||||
"type": "currency",
|
"type": "currency",
|
||||||
"refreshInterval": 600, // in seconds
|
"refreshInterval": 600, // in seconds
|
||||||
@@ -171,18 +302,22 @@ To close a group, use the button:
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### `music`
|
#### `music`
|
||||||
|
|
||||||
```js
|
```js
|
||||||
{
|
{
|
||||||
"type": "music",
|
"type": "music",
|
||||||
"align": "center",
|
"align": "center",
|
||||||
"width": 80,
|
"width": 80, // Optional
|
||||||
"bordered": false,
|
"bordered": false, // Optional
|
||||||
"refreshInterval": 2, // in seconds
|
"refreshInterval": 2, // in seconds. Optional. Default 5 seconds
|
||||||
|
"disableMarquee": true // to disable marquee effect. Optional. Default false
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `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 click to start the work timer, longclick to start the rest timer. Click in progress for reset.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
{
|
{
|
||||||
"type": "pomodoro",
|
"type": "pomodoro",
|
||||||
@@ -192,7 +327,9 @@ To close a group, use the button:
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### `network`
|
#### `network`
|
||||||
|
|
||||||
> Network plugin. The plugin to show usage a network
|
> Network plugin. The plugin to show usage a network
|
||||||
|
|
||||||
```js
|
```js
|
||||||
{
|
{
|
||||||
"type": "network",
|
"type": "network",
|
||||||
@@ -200,30 +337,85 @@ To close a group, use the button:
|
|||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### `dock`
|
||||||
|
|
||||||
|
> Dock plugin
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
"type": "dock",
|
||||||
|
"filter": "(^Xcode$)|(Safari)|(.*player)",
|
||||||
|
"autoResize": true
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `upnext`
|
||||||
|
|
||||||
|
> Calender 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
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"action": "hidKey",
|
"action": "hidKey",
|
||||||
"keycode": 53,
|
"keycode": 53,
|
||||||
```
|
```
|
||||||
|
|
||||||
- `keyPress`
|
- `keyPress`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"action": "keyPress",
|
"action": "keyPress",
|
||||||
"keycode": 1,
|
"keycode": 1,
|
||||||
```
|
```
|
||||||
|
|
||||||
- `appleScript`
|
- `appleScript`
|
||||||
|
|
||||||
```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
|
||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
- `shellScript`
|
- `shellScript`
|
||||||
|
|
||||||
```js
|
```js
|
||||||
"action": "shellScript",
|
"action": "shellScript",
|
||||||
"executablePath": "/usr/bin/pmset",
|
"executablePath": "/usr/bin/pmset",
|
||||||
@@ -232,143 +424,71 @@ To close a group, use the button:
|
|||||||
```
|
```
|
||||||
|
|
||||||
- `openUrl`
|
- `openUrl`
|
||||||
|
|
||||||
```js
|
```js
|
||||||
"action": "openUrl",
|
"action": "openUrl",
|
||||||
"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
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"width": 34
|
"width": 34
|
||||||
```
|
```
|
||||||
|
|
||||||
- `align` can stick the item to the side. default is center
|
- `align` can stick the item to the side. default is center
|
||||||
|
|
||||||
```js
|
```js
|
||||||
"align": "left" // "left", "right" or "center"
|
"align": "left" // "left", "right" or "center"
|
||||||
```
|
```
|
||||||
|
|
||||||
- `bordered` you can do button without border
|
- `bordered` you can do button without border
|
||||||
|
|
||||||
```js
|
```js
|
||||||
"bordered": "false" // "true" or "false"
|
"bordered": "false" // "true" or "false"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Example configuration:
|
- `background` allow to specify you button background color
|
||||||
```json
|
|
||||||
[
|
|
||||||
{ "type": "escape", "width": 110 },
|
|
||||||
{ "type": "exitTouchbar", "align": "left" },
|
|
||||||
{
|
|
||||||
"type": "brightnessUp",
|
|
||||||
"align": "left",
|
|
||||||
"width": 36
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "staticButton",
|
|
||||||
"align": "left",
|
|
||||||
"title": "🔆",
|
|
||||||
"action": "keyPress",
|
|
||||||
"keycode": 113,
|
|
||||||
"width": 36
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
```js
|
||||||
"type": "appleScriptTitledButton",
|
"background": "#FF0000",
|
||||||
"source": {
|
```
|
||||||
"filePath": "/Users/toxblh/Library/Application Support/MTMR/iTunes.nowPlaying.scpt"
|
by using background with color "#000000" and bordered == false you can create button without gray background but with background when the button is pressed
|
||||||
},
|
|
||||||
"refreshInterval": 1
|
- `title` specify button title
|
||||||
},
|
|
||||||
{
|
```js
|
||||||
"type": "staticButton",
|
"title": "hello"
|
||||||
"align": "left",
|
|
||||||
"image": { "base64" : "%base64Finder%"},
|
|
||||||
"action": "appleScript",
|
|
||||||
"actionAppleScript": {
|
|
||||||
"inline": "tell application \"Finder\"\rmake new Finder window\rset target of front window to path to home folder as string\ractivate\rend tell"
|
|
||||||
},
|
|
||||||
"width": 36
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "appleScriptTitledButton",
|
|
||||||
"source": {
|
|
||||||
"inline": "if application \"Safari\" is running then\r\ttell application \"Safari\"\r\t\trepeat with t in tabs of windows\r\t\t\ttell t\r\t\t\t\tif URL starts with \"https:\/\/music.yandex.ru\" and name does not end with \"на Яндекс.Музыке\" then\r\t\t\t\t\treturn name of t as text\r\t\t\t\tend if\r\t\t\tend tell\r\t\tend repeat\r\tend tell\rend if\rreturn \"\""
|
|
||||||
},
|
|
||||||
"refreshInterval": 1
|
|
||||||
},
|
|
||||||
{ "type": "previous", "width": 36, "align": "right" },
|
|
||||||
{ "type": "play", "width": 36, "align": "right" },
|
|
||||||
{ "type": "next", "width": 36, "align": "right" },
|
|
||||||
{ "type": "sleep", "width": 36 , "align": "right"},
|
|
||||||
{ "type": "displaySleep", "align": "right" },
|
|
||||||
{ "type": "weather", "refreshInterval": 1800, "width": 70, "align": "right" },
|
|
||||||
{ "type": "volumeDown", "width": 36 , "align": "right"},
|
|
||||||
{ "type": "volumeUp", "width": 36 , "align": "right"},
|
|
||||||
{ "type": "battery", "refreshInterval": 60 , "align": "right"},
|
|
||||||
{ "type": "appleScriptTitledButton", "refreshInterval": 1800, "source": { "filePath": "/Users/redetection/Library/Application Support/MTMR/Weather.scpt"} , "align": "right"},
|
|
||||||
{ "type": "timeButton", "formatTemplate": "HH:mm", "width": 64, "align": "right" }
|
|
||||||
]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- `image` specify button icon
|
||||||
|
|
||||||
### Roadmap
|
```js
|
||||||
- [x] Create the first prototype with TouchBar in Storyboard
|
"image": {
|
||||||
- [x] Put in stripe menu on startup the application
|
//Can be either of those
|
||||||
- [x] Find how to simulate real buttons like brightness, volume, night shift and etc.
|
"base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAABGdB...."
|
||||||
- [x] Time in touchbar!
|
//or
|
||||||
- [x] First the weather plugin
|
"filePath": "~/img.png"
|
||||||
- [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:
|
## Troubleshooting
|
||||||
- [ ] Interface for plugins and export like presets
|
|
||||||
- [x] Startup at login
|
|
||||||
- [ ] Show on/off in Dock
|
|
||||||
- [ ] Show on/off in StatusBar
|
|
||||||
- [ ] On/off Haptic Feedback
|
|
||||||
|
|
||||||
Maybe:
|
#### If you can't open preferences:
|
||||||
- [ ] Refactoring the application on packages (AppleScript, JavaScript? and Swift?)
|
- 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>
|
||||||
|
|
||||||
### Author's presets
|
|
||||||
[@Toxblh preset](Resources/toxblh.json)
|
|
||||||
|
|
||||||
[@ReDetection preset](Resources/ReDetection.json)
|
#### 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"
|
||||||
|
|
||||||
### User's presets
|
Re-tick or check a tick for access 🍏→ System Preferences → Security and Privacy → tab Privacy → Accessibility → MTMR
|
||||||
[@luongvo209 preset](Resources/luongvo209.json)
|
|
||||||

|
|
||||||
|
|
||||||
[aadi_vs_anand preset](Resources/aadi_vs_anand.json)
|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 259 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 257 KiB |
@@ -14,7 +14,7 @@
|
|||||||
{ "type": "appleScriptTitledButton", "source": { "inline": "if application \"iTunes\" is running then\r\ttell application \"iTunes\"\r\t\tif player state is not stopped then return \"\"\r\tend tell\rend if\rif application \"Safari\" is running then\r\ttell application \"Safari\"\r\t\trepeat with t in tabs of windows\r\t\t\ttell t\r\t\t\t\tif URL starts with \"https:\/\/music.yandex.ru\" and name does not end with \"на Яндекс.Музыке\" then\r\t\t\t\t\treturn \"\"\r\t\t\t\tend if\r\t\t\tend tell\r\t\tend repeat\r\tend tell\rend if\rreturn \"▶\"" }, "refreshInterval": 30, "width": 40, "align": "right" },
|
{ "type": "appleScriptTitledButton", "source": { "inline": "if application \"iTunes\" is running then\r\ttell application \"iTunes\"\r\t\tif player state is not stopped then return \"\"\r\tend tell\rend if\rif application \"Safari\" is running then\r\ttell application \"Safari\"\r\t\trepeat with t in tabs of windows\r\t\t\ttell t\r\t\t\t\tif URL starts with \"https:\/\/music.yandex.ru\" and name does not end with \"на Яндекс.Музыке\" then\r\t\t\t\t\treturn \"\"\r\t\t\t\tend if\r\t\t\tend tell\r\t\tend repeat\r\tend tell\rend if\rreturn \"▶\"" }, "refreshInterval": 30, "width": 40, "align": "right" },
|
||||||
{ "type": "volume", "width": 100, "align": "right" },
|
{ "type": "volume", "width": 100, "align": "right" },
|
||||||
{ "type": "displaySleep", "width": 44, "align": "right" },
|
{ "type": "displaySleep", "width": 44, "align": "right" },
|
||||||
{ "type": "appleScriptTitledButton", "refreshInterval": 1800, "source": { "filePath": "/Users/redetection/Library/Application Support/MTMR/Weather.scpt"}, "align": "right" },
|
{ "type": "appleScriptTitledButton", "refreshInterval": 1800, "source": { "filePath": "~/Library/Application Support/MTMR/Weather.scpt"}, "align": "right" },
|
||||||
|
|
||||||
|
|
||||||
{ "type": "timeButton" , "align": "right"},
|
{ "type": "timeButton" , "align": "right"},
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 583 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
+10
-10
@@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,3 +4,38 @@
|
|||||||
* try move away from enums when parse preset – enums are hard to extend
|
* try move away from enums when parse preset – enums are hard to extend
|
||||||
* find better way to hide bar items
|
* find better way to hide bar items
|
||||||
* extract bar items creating from TouchBarController to separate class, cover with tests
|
* extract bar items creating from TouchBarController to separate class, cover with tests
|
||||||
|
|
||||||
|
|
||||||
|
### Roadmap
|
||||||
|
|
||||||
|
- [x] Create the first prototype with TouchBar in Storyboard
|
||||||
|
- [x] Put in stripe menu on startup the application
|
||||||
|
- [x] Find how to simulate real buttons like brightness, volume, night shift and etc.
|
||||||
|
- [x] Time in touchbar!
|
||||||
|
- [x] First the weather plugin
|
||||||
|
- [x] Find how to open full-screen TouchBar without the cross and stripe menu
|
||||||
|
- [x] Find how to add haptic feedback
|
||||||
|
- [x] Add icon and menu in StatusBar
|
||||||
|
- [x] Hide from Dock
|
||||||
|
- [x] Status menu: "preferences", "quit"
|
||||||
|
- [x] JSON or another approch for save preset, maybe in `~/Library/Application Support/MTMR/`
|
||||||
|
- [x] Custom buttons size, actions by click
|
||||||
|
- [x] Layout: [always left, NSSliderView for center, always right]
|
||||||
|
- [x] System for autoupdate (https://sparkle-project.org/)
|
||||||
|
- [ ] Overwrite default values from item types (e.g. title for brightness)
|
||||||
|
- [ ] Custom settings for paddings and margins for buttons
|
||||||
|
- [ ] XPC Service for scripts
|
||||||
|
- [ ] UI for settings
|
||||||
|
- [ ] Import config from BTT
|
||||||
|
|
||||||
|
Settings:
|
||||||
|
|
||||||
|
- [ ] Interface for plugins and export like presets
|
||||||
|
- [x] Startup at login
|
||||||
|
- [ ] Show on/off in Dock
|
||||||
|
- [ ] Show on/off in StatusBar
|
||||||
|
- [x] On/off Haptic Feedback
|
||||||
|
|
||||||
|
Maybe:
|
||||||
|
|
||||||
|
- [ ] Refactoring the application into packages (AppleScript, JavaScript? and Swift?)
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
|
# INSTALL xcpretty: sudo gem install xcpretty
|
||||||
|
|
||||||
NAME='MTMR'
|
NAME='MTMR'
|
||||||
|
|
||||||
rm -r Release 2>/dev/null
|
rm -r Release 2>/dev/null
|
||||||
|
|
||||||
xcodebuild archive \
|
xcodebuild archive \
|
||||||
-scheme "$NAME" \
|
-scheme "$NAME" \
|
||||||
-archivePath Release/App.xcarchive
|
-archivePath Release/App.xcarchive | xcpretty -c
|
||||||
|
|
||||||
xcodebuild \
|
xcodebuild \
|
||||||
-exportArchive \
|
-exportArchive \
|
||||||
-archivePath Release/App.xcarchive \
|
-archivePath Release/App.xcarchive \
|
||||||
-exportOptionsPlist export-options.plist \
|
-exportOptionsPlist export-options.plist \
|
||||||
-exportPath Release
|
-exportPath Release | xcpretty -c
|
||||||
|
|
||||||
cd Release
|
cd Release
|
||||||
rm -r App.xcarchive
|
rm -r App.xcarchive
|
||||||
@@ -20,7 +22,7 @@ NAME_DMG="${NAME}.app"
|
|||||||
echo $NAME_DMG
|
echo $NAME_DMG
|
||||||
create-dmg $NAME_DMG
|
create-dmg $NAME_DMG
|
||||||
|
|
||||||
DATE=`date +"%a, %d %b %Y %H:%M:%S %z"`
|
DATE=`LC_ALL=en_US.utf8 date +"%a, %d %b %Y %H:%M:%S %z"`
|
||||||
BUILD=`/usr/libexec/PlistBuddy -c "Print CFBundleVersion" ${NAME}.app/Contents/Info.plist`
|
BUILD=`/usr/libexec/PlistBuddy -c "Print CFBundleVersion" ${NAME}.app/Contents/Info.plist`
|
||||||
VERSION=`/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ${NAME}.app/Contents/Info.plist`
|
VERSION=`/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ${NAME}.app/Contents/Info.plist`
|
||||||
MINIMUM=`/usr/libexec/PlistBuddy -c "Print LSMinimumSystemVersion" ${NAME}.app/Contents/Info.plist`
|
MINIMUM=`/usr/libexec/PlistBuddy -c "Print LSMinimumSystemVersion" ${NAME}.app/Contents/Info.plist`
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
# INSTALL xcpretty: sudo gem install xcpretty
|
||||||
|
|
||||||
NAME='MTMR'
|
NAME='MTMR'
|
||||||
killall $NAME
|
killall $NAME
|
||||||
@@ -11,13 +12,13 @@ rm -r Release 2>/dev/null
|
|||||||
|
|
||||||
xcodebuild archive \
|
xcodebuild archive \
|
||||||
-scheme "$NAME" \
|
-scheme "$NAME" \
|
||||||
-archivePath Release/App.xcarchive
|
-archivePath Release/App.xcarchive | xcpretty
|
||||||
|
|
||||||
xcodebuild \
|
xcodebuild \
|
||||||
-exportArchive \
|
-exportArchive \
|
||||||
-archivePath Release/App.xcarchive \
|
-archivePath Release/App.xcarchive \
|
||||||
-exportOptionsPlist export-options.plist \
|
-exportOptionsPlist export-options.plist \
|
||||||
-exportPath Release
|
-exportPath Release | xcpretty
|
||||||
|
|
||||||
cd Release
|
cd Release
|
||||||
rm -r App.xcarchive
|
rm -r App.xcarchive
|
||||||
|
|||||||
Reference in New Issue
Block a user