mirror of
https://github.com/Toxblh/MTMR.git
synced 2026-01-10 00:58:37 +00:00
Merge remote-tracking branch 'origin/master' into notification-widget
This commit is contained in:
commit
e5bb90249f
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@ -3,4 +3,4 @@
|
||||
issuehunt: Toxblh
|
||||
patreon: toxblh
|
||||
ko_fi: toxblh
|
||||
custom: https://www.paypal.me/toxblh
|
||||
custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=WUAAG2HH58WE4
|
||||
|
||||
22
.github/workflows/build-test.yml
vendored
Normal file
22
.github/workflows/build-test.yml
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
name: Build-and-test
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: macOS-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Run tests
|
||||
run: xcodebuild test -project MTMR.xcodeproj -scheme 'UnitTests' | xcpretty -c && exit ${PIPESTATUS[0]}
|
||||
|
||||
build:
|
||||
runs-on: macOS-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Build
|
||||
run: xcodebuild archive -project "MTMR.xcodeproj" -scheme "MTMR" -archivePath Release/App.xcarchive DEVELOPMENT_TEAM="" CODE_SIGN_IDENTITY="" | xcpretty -c && exit ${PIPESTATUS[0]}
|
||||
41
.github/workflows/publish.yml
vendored
Normal file
41
.github/workflows/publish.yml
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
name: Publish unsign version
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
Build-and-release:
|
||||
runs-on: macOS-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
|
||||
- name: Install create-dmg
|
||||
run: npm i -g create-dmg
|
||||
|
||||
- name: Build Archive
|
||||
run: xcodebuild archive -project "MTMR.xcodeproj" -scheme "MTMR" -archivePath Release/App.xcarchive DEVELOPMENT_TEAM="" CODE_SIGN_IDENTITY="" | xcpretty -c && exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: Build App
|
||||
run: xcodebuild -project "MTMR.xcodeproj" -exportArchive -archivePath Release/App.xcarchive -exportOptionsPlist export-options.plist -exportPath Release | xcpretty -c && exit ${PIPESTATUS[0]}
|
||||
|
||||
- name: Create DMG
|
||||
run: |
|
||||
cd Release
|
||||
create-dmg MTMR.app || true
|
||||
|
||||
- name: GitHub Release
|
||||
uses: "marvinpinto/action-automatic-releases@latest"
|
||||
with:
|
||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
prerelease: false
|
||||
files: Release/MTMR*.dmg
|
||||
@ -1,6 +0,0 @@
|
||||
language: swift
|
||||
xcode_project: MTMR.xcodeproj
|
||||
xcode_scheme: UnitTests
|
||||
osx_image: xcode11.1
|
||||
install: gem install xcpretty
|
||||
script: "xcodebuild test -project MTMR.xcodeproj -scheme 'UnitTests' | xcpretty -c && exit ${PIPESTATUS[0]}"
|
||||
@ -22,6 +22,8 @@
|
||||
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 */; };
|
||||
6027D1B92080E52A004FFDC7 /* BrightnessViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6027D1B72080E52A004FFDC7 /* BrightnessViewController.swift */; };
|
||||
6027D1BA2080E52A004FFDC7 /* VolumeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6027D1B82080E52A004FFDC7 /* VolumeViewController.swift */; };
|
||||
@ -71,6 +73,9 @@
|
||||
B0F3112520C9E35F0076BB88 /* SupportNSTouchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0F3112420C9E35F0076BB88 /* SupportNSTouchBar.swift */; };
|
||||
B0F54A7A2295AC7D00B4C509 /* DarkModeBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0F54A792295AC7D00B4C509 /* DarkModeBarItem.swift */; };
|
||||
B0F8771A207AC1EA00D6E430 /* TouchBarSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = B0F87719207AC1EA00D6E430 /* TouchBarSupport.m */; };
|
||||
BAF5AB5724317B4300B04904 /* BasicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF5AB5624317B4300B04904 /* BasicView.swift */; };
|
||||
BAF5AB5924317CAF00B04904 /* SwipeItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAF5AB5824317CAF00B04904 /* SwipeItem.swift */; };
|
||||
F29F6A2524BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = F29F6A2424BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
@ -106,6 +111,8 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -165,6 +172,9 @@
|
||||
B0F54A792295AC7D00B4C509 /* DarkModeBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DarkModeBarItem.swift; sourceTree = "<group>"; };
|
||||
B0F87719207AC1EA00D6E430 /* TouchBarSupport.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TouchBarSupport.m; sourceTree = "<group>"; };
|
||||
B0F8771B207AC92700D6E430 /* TouchBarSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TouchBarSupport.h; sourceTree = "<group>"; };
|
||||
BAF5AB5624317B4300B04904 /* BasicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicView.swift; sourceTree = "<group>"; };
|
||||
BAF5AB5824317CAF00B04904 /* SwipeItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeItem.swift; sourceTree = "<group>"; };
|
||||
F29F6A2424BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpNextScrubberTouchBarItem.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -242,8 +252,10 @@
|
||||
36FEF871235A1CFC00A0ABCE /* AppSettings.swift */,
|
||||
B059D623205E04F3006E6B86 /* CustomButtonTouchBarItem.swift */,
|
||||
36C2ECD8207B74B4003CDA33 /* AppleScriptTouchBarItem.swift */,
|
||||
BAF5AB5824317CAF00B04904 /* SwipeItem.swift */,
|
||||
4CDC6E4F22FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift */,
|
||||
B059D621205E03F5006E6B86 /* TouchBarController.swift */,
|
||||
BAF5AB5624317B4300B04904 /* BasicView.swift */,
|
||||
36C2ECDA207C3FE7003CDA33 /* ItemsParsing.swift */,
|
||||
B0008E542080286C003AD4DD /* SupportHelpers.swift */,
|
||||
B09EB1E3207C082000D5C1E0 /* HapticFeedback.swift */,
|
||||
@ -270,6 +282,8 @@
|
||||
B0B17426207D6AFE0004B740 /* AppleScripts */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5DC6CA01241F92CB005CD5E8 /* Music.next.scpt */,
|
||||
5DC6CA00241F92CB005CD5E8 /* Music.nowPlaying.scpt */,
|
||||
B0B1742A207D6B580004B740 /* Battery.scpt */,
|
||||
B0B17429207D6B580004B740 /* Finder.scpt */,
|
||||
B0B1742C207D6B590004B740 /* iTunes.next.scpt */,
|
||||
@ -325,6 +339,7 @@
|
||||
B08126F0217BE19000A98970 /* WidgetProtocol.swift */,
|
||||
B0F54A792295AC7D00B4C509 /* DarkModeBarItem.swift */,
|
||||
839ECD8423600BF500BE2DA5 /* NotificationTouchBarItem.swift */,
|
||||
F29F6A2424BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift */,
|
||||
);
|
||||
path = Widgets;
|
||||
sourceTree = "<group>";
|
||||
@ -422,6 +437,8 @@
|
||||
B0B1743A207D6B590004B740 /* iTunes.nowPlaying.scpt in Resources */,
|
||||
B082B257205C7D8000BC04DC /* Assets.xcassets in Resources */,
|
||||
B0B17437207D6B590004B740 /* Spotify.nowPlaying.scpt in Resources */,
|
||||
5DC6CA02241F92CB005CD5E8 /* Music.nowPlaying.scpt in Resources */,
|
||||
5DC6CA03241F92CB005CD5E8 /* Music.next.scpt in Resources */,
|
||||
B0B17436207D6B590004B740 /* iTunes.next.scpt in Resources */,
|
||||
B082B25A205C7D8000BC04DC /* Main.storyboard in Resources */,
|
||||
B0B17435207D6B590004B740 /* Spotify.next.scpt in Resources */,
|
||||
@ -472,11 +489,13 @@
|
||||
B059D622205E03F5006E6B86 /* TouchBarController.swift in Sources */,
|
||||
B05600D32083E9BB00EB218D /* CustomSlider.swift in Sources */,
|
||||
36C2ECD9207B74B4003CDA33 /* AppleScriptTouchBarItem.swift in Sources */,
|
||||
BAF5AB5724317B4300B04904 /* BasicView.swift in Sources */,
|
||||
B0846A752220C968000288A7 /* NetworkBarItem.swift in Sources */,
|
||||
B0F8771A207AC1EA00D6E430 /* TouchBarSupport.m in Sources */,
|
||||
B08126F1217BE19000A98970 /* WidgetProtocol.swift in Sources */,
|
||||
B0008E552080286C003AD4DD /* SupportHelpers.swift in Sources */,
|
||||
6042B6A72083E03A00C525C8 /* AppScrubberTouchBarItem.swift in Sources */,
|
||||
BAF5AB5924317CAF00B04904 /* SwipeItem.swift in Sources */,
|
||||
B082B253205C7D8000BC04DC /* AppDelegate.swift in Sources */,
|
||||
B0F54A7A2295AC7D00B4C509 /* DarkModeBarItem.swift in Sources */,
|
||||
B08126EF217BD0B900A98970 /* PomodoroBarItem.swift in Sources */,
|
||||
@ -487,6 +506,7 @@
|
||||
6027D1BA2080E52A004FFDC7 /* VolumeViewController.swift in Sources */,
|
||||
4CDC6E5022FCA93F0069ADD4 /* ShellScriptTouchBarItem.swift in Sources */,
|
||||
607EEA4B2087835F009DA5F0 /* WeatherBarItem.swift in Sources */,
|
||||
F29F6A2524BC7148004FF8E4 /* UpNextScrubberTouchBarItem.swift in Sources */,
|
||||
B0F3112520C9E35F0076BB88 /* SupportNSTouchBar.swift in Sources */,
|
||||
4CFF5E5C22E623DD00BFB1EE /* YandexWeatherBarItem.swift in Sources */,
|
||||
6042B6AA2083E27000C525C8 /* DeprecatedCarbonAPI.c in Sources */,
|
||||
|
||||
@ -92,7 +92,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
@objc func toggleMultitouch(_ item: NSMenuItem) {
|
||||
item.state = item.state == .on ? .off : .on
|
||||
AppSettings.multitouchGestures = item.state == .on
|
||||
TouchBarController.shared.scrollArea?.gesturesEnabled = item.state == .on
|
||||
TouchBarController.shared.basicView?.legacyGesturesEnabled = item.state == .on
|
||||
}
|
||||
|
||||
@objc func openPreset(_: Any?) {
|
||||
|
||||
@ -4,9 +4,11 @@ class AppleScriptTouchBarItem: CustomButtonTouchBarItem {
|
||||
private var script: NSAppleScript!
|
||||
private let interval: TimeInterval
|
||||
private var forceHideConstraint: NSLayoutConstraint!
|
||||
private let alternativeImages: [String: SourceProtocol]
|
||||
|
||||
init?(identifier: NSTouchBarItem.Identifier, source: SourceProtocol, interval: TimeInterval) {
|
||||
init?(identifier: NSTouchBarItem.Identifier, source: SourceProtocol, interval: TimeInterval, alternativeImages: [String: SourceProtocol]) {
|
||||
self.interval = interval
|
||||
self.alternativeImages = alternativeImages
|
||||
super.init(identifier: identifier, title: "⏳")
|
||||
forceHideConstraint = view.widthAnchor.constraint(equalToConstant: 0)
|
||||
title = "scheduled"
|
||||
@ -57,6 +59,16 @@ class AppleScriptTouchBarItem: CustomButtonTouchBarItem {
|
||||
}
|
||||
}
|
||||
|
||||
func updateIcon(iconLabel: String) {
|
||||
if alternativeImages[iconLabel] != nil {
|
||||
DispatchQueue.main.async {
|
||||
self.image = self.alternativeImages[iconLabel]!.image
|
||||
}
|
||||
} else {
|
||||
print("Cannot find icon with label \"\(iconLabel)\"")
|
||||
}
|
||||
}
|
||||
|
||||
func execute() -> String {
|
||||
var error: NSDictionary?
|
||||
let output = script.executeAndReturnError(&error)
|
||||
@ -64,6 +76,18 @@ class AppleScriptTouchBarItem: CustomButtonTouchBarItem {
|
||||
print(error)
|
||||
return "error"
|
||||
}
|
||||
if output.descriptorType == typeAEList {
|
||||
let arr = Array(1...output.numberOfItems).compactMap({ output.atIndex($0)!.stringValue ?? "" })
|
||||
|
||||
if arr.count <= 0 {
|
||||
return ""
|
||||
} else if arr.count == 1 {
|
||||
return arr[0]
|
||||
} else {
|
||||
updateIcon(iconLabel: arr[1])
|
||||
return arr[0]
|
||||
}
|
||||
}
|
||||
return output.stringValue ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
7
MTMR/AppleScripts/Music.next.scpt
Normal file
7
MTMR/AppleScripts/Music.next.scpt
Normal file
@ -0,0 +1,7 @@
|
||||
if application "Music" is running then
|
||||
tell application "Music"
|
||||
if player state is playing then
|
||||
next track
|
||||
end if
|
||||
end tell
|
||||
end if
|
||||
10
MTMR/AppleScripts/Music.nowPlaying.scpt
Normal file
10
MTMR/AppleScripts/Music.nowPlaying.scpt
Normal file
@ -0,0 +1,10 @@
|
||||
if application "Music" is running then
|
||||
tell application "Music"
|
||||
if player state is playing then
|
||||
return (get artist of current track) & " – " & (get name of current track)
|
||||
else
|
||||
return ""
|
||||
end if
|
||||
end tell
|
||||
end if
|
||||
return ""
|
||||
@ -2,6 +2,10 @@ if application "iTunes" is running then
|
||||
tell application "iTunes" to playpause
|
||||
end if
|
||||
|
||||
if application "Music" is running then
|
||||
tell application "Music" to playpause
|
||||
end if
|
||||
|
||||
if application "Spotify" is running then
|
||||
tell application "Spotify" to playpause
|
||||
end if
|
||||
|
||||
107
MTMR/BasicView.swift
Normal file
107
MTMR/BasicView.swift
Normal file
@ -0,0 +1,107 @@
|
||||
//
|
||||
// BasicView.swift
|
||||
// MTMR
|
||||
//
|
||||
// Created by Fedor Zaitsev on 3/29/20.
|
||||
// Copyright © 2020 Anton Palgunov. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
class BasicView: NSCustomTouchBarItem, NSGestureRecognizerDelegate {
|
||||
var twofingers: NSPanGestureRecognizer!
|
||||
var threefingers: NSPanGestureRecognizer!
|
||||
var fourfingers: NSPanGestureRecognizer!
|
||||
var swipeItems: [SwipeItem] = []
|
||||
var prevPositions: [Int: CGFloat] = [2:0, 3:0, 4:0]
|
||||
|
||||
// legacy gesture positions
|
||||
// by legacy I mean gestures to increse/decrease volume/brigtness which can be checked from app menu
|
||||
var legacyPrevPositions: [Int: CGFloat] = [2:0, 3:0, 4:0]
|
||||
var legacyGesturesEnabled = false
|
||||
|
||||
init(identifier: NSTouchBarItem.Identifier, items: [NSTouchBarItem], swipeItems: [SwipeItem]) {
|
||||
super.init(identifier: identifier)
|
||||
self.swipeItems = swipeItems
|
||||
let views = items.compactMap { $0.view }
|
||||
let stackView = NSStackView(views: views)
|
||||
stackView.spacing = 8
|
||||
stackView.orientation = .horizontal
|
||||
view = stackView
|
||||
|
||||
twofingers = NSPanGestureRecognizer(target: self, action: #selector(twofingersHandler(_:)))
|
||||
twofingers.numberOfTouchesRequired = 2
|
||||
twofingers.allowedTouchTypes = .direct
|
||||
view.addGestureRecognizer(twofingers)
|
||||
|
||||
threefingers = NSPanGestureRecognizer(target: self, action: #selector(threefingersHandler(_:)))
|
||||
threefingers.numberOfTouchesRequired = 3
|
||||
threefingers.allowedTouchTypes = .direct
|
||||
view.addGestureRecognizer(threefingers)
|
||||
|
||||
fourfingers = NSPanGestureRecognizer(target: self, action: #selector(fourfingersHandler(_:)))
|
||||
fourfingers.numberOfTouchesRequired = 4
|
||||
fourfingers.allowedTouchTypes = .direct
|
||||
view.addGestureRecognizer(fourfingers)
|
||||
}
|
||||
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func gestureHandler(position: CGFloat, fingers: Int, state: NSGestureRecognizer.State) {
|
||||
switch state {
|
||||
case .began:
|
||||
prevPositions[fingers] = position
|
||||
legacyPrevPositions[fingers] = position
|
||||
case .changed:
|
||||
if self.legacyGesturesEnabled {
|
||||
if fingers == 2 {
|
||||
let prevPos = legacyPrevPositions[fingers]!
|
||||
if ((position - prevPos) > 10) || ((prevPos - position) > 10) {
|
||||
if position > prevPos {
|
||||
HIDPostAuxKey(NX_KEYTYPE_SOUND_UP)
|
||||
} else if position < prevPos {
|
||||
HIDPostAuxKey(NX_KEYTYPE_SOUND_DOWN)
|
||||
}
|
||||
legacyPrevPositions[fingers] = position
|
||||
}
|
||||
}
|
||||
if fingers == 3 {
|
||||
let prevPos = legacyPrevPositions[fingers]!
|
||||
if ((position - prevPos) > 15) || ((prevPos - position) > 15) {
|
||||
if position > prevPos {
|
||||
HIDPostAuxKey(NX_KEYTYPE_BRIGHTNESS_UP)
|
||||
} else if position < prevPos {
|
||||
HIDPostAuxKey(NX_KEYTYPE_BRIGHTNESS_DOWN)
|
||||
}
|
||||
legacyPrevPositions[fingers] = position
|
||||
}
|
||||
}
|
||||
}
|
||||
case .ended:
|
||||
print("gesture ended \(position - prevPositions[fingers]!) \(fingers)")
|
||||
for item in swipeItems {
|
||||
item.processEvent(offset: position - prevPositions[fingers]!, fingers: fingers)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@objc func twofingersHandler(_ sender: NSGestureRecognizer?) {
|
||||
let position = (sender?.location(in: sender?.view).x)!
|
||||
self.gestureHandler(position: position, fingers: 2, state: sender!.state)
|
||||
}
|
||||
|
||||
@objc func threefingersHandler(_ sender: NSGestureRecognizer?) {
|
||||
let position = (sender?.location(in: sender?.view).x)!
|
||||
self.gestureHandler(position: position, fingers: 3, state: sender!.state)
|
||||
}
|
||||
|
||||
@objc func fourfingersHandler(_ sender: NSGestureRecognizer?) {
|
||||
let position = (sender?.location(in: sender?.view).x)!
|
||||
self.gestureHandler(position: position, fingers: 4, state: sender!.state)
|
||||
}
|
||||
}
|
||||
@ -8,18 +8,32 @@
|
||||
|
||||
import Cocoa
|
||||
|
||||
struct ItemAction {
|
||||
typealias TriggerClosure = (() -> Void)?
|
||||
|
||||
let trigger: Action.Trigger
|
||||
let closure: TriggerClosure
|
||||
|
||||
init(trigger: Action.Trigger, _ closure: TriggerClosure) {
|
||||
self.trigger = trigger
|
||||
self.closure = closure
|
||||
}
|
||||
}
|
||||
|
||||
class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegate {
|
||||
var tapClosure: (() -> Void)?
|
||||
var longTapClosure: (() -> Void)? {
|
||||
|
||||
var actions: [ItemAction] = [] {
|
||||
didSet {
|
||||
longClick.isEnabled = longTapClosure != nil
|
||||
multiClick.isDoubleClickEnabled = actions.filter({ $0.trigger == .doubleTap }).count > 0
|
||||
multiClick.isTripleClickEnabled = actions.filter({ $0.trigger == .tripleTap }).count > 0
|
||||
longClick.isEnabled = actions.filter({ $0.trigger == .longTap }).count > 0
|
||||
}
|
||||
}
|
||||
var finishViewConfiguration: ()->() = {}
|
||||
|
||||
private var button: NSButton!
|
||||
private var singleClick: HapticClickGestureRecognizer!
|
||||
private var longClick: LongPressGestureRecognizer!
|
||||
private var multiClick: MultiClickGestureRecognizer!
|
||||
|
||||
init(identifier: NSTouchBarItem.Identifier, title: String) {
|
||||
attributedTitle = title.defaultTouchbarAttributedString
|
||||
@ -31,10 +45,17 @@ class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegat
|
||||
longClick.isEnabled = false
|
||||
longClick.allowedTouchTypes = .direct
|
||||
longClick.delegate = self
|
||||
|
||||
singleClick = HapticClickGestureRecognizer(target: self, action: #selector(handleGestureSingle))
|
||||
singleClick.allowedTouchTypes = .direct
|
||||
singleClick.delegate = self
|
||||
|
||||
multiClick = MultiClickGestureRecognizer(
|
||||
target: self,
|
||||
action: #selector(handleGestureSingleTap),
|
||||
doubleAction: #selector(handleGestureDoubleTap),
|
||||
tripleAction: #selector(handleGestureTripleTap)
|
||||
)
|
||||
multiClick.allowedTouchTypes = .direct
|
||||
multiClick.delegate = self
|
||||
multiClick.isDoubleClickEnabled = false
|
||||
multiClick.isTripleClickEnabled = false
|
||||
|
||||
reinstallButton()
|
||||
button.attributedTitle = attributedTitle
|
||||
@ -100,33 +121,43 @@ class CustomButtonTouchBarItem: NSCustomTouchBarItem, NSGestureRecognizerDelegat
|
||||
view = button
|
||||
|
||||
view.addGestureRecognizer(longClick)
|
||||
view.addGestureRecognizer(singleClick)
|
||||
// view.addGestureRecognizer(singleClick)
|
||||
view.addGestureRecognizer(multiClick)
|
||||
finishViewConfiguration()
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: NSGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: NSGestureRecognizer) -> Bool {
|
||||
if gestureRecognizer == singleClick && otherGestureRecognizer == longClick
|
||||
|| gestureRecognizer == longClick && otherGestureRecognizer == singleClick // need it
|
||||
if gestureRecognizer == multiClick && otherGestureRecognizer == longClick
|
||||
|| gestureRecognizer == longClick && otherGestureRecognizer == multiClick // need it
|
||||
{
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@objc func handleGestureSingle(gr: NSClickGestureRecognizer) {
|
||||
switch gr.state {
|
||||
case .ended:
|
||||
tapClosure?()
|
||||
break
|
||||
default:
|
||||
break
|
||||
|
||||
func callActions(for trigger: Action.Trigger) {
|
||||
let itemActions = self.actions.filter { $0.trigger == trigger }
|
||||
for itemAction in itemActions {
|
||||
itemAction.closure?()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handleGestureSingleTap() {
|
||||
callActions(for: .singleTap)
|
||||
}
|
||||
|
||||
@objc func handleGestureDoubleTap() {
|
||||
callActions(for: .doubleTap)
|
||||
}
|
||||
|
||||
@objc func handleGestureTripleTap() {
|
||||
callActions(for: .tripleTap)
|
||||
}
|
||||
|
||||
@objc func handleGestureLong(gr: NSPressGestureRecognizer) {
|
||||
switch gr.state {
|
||||
case .possible: // tiny hack because we're calling action manually
|
||||
(self.longTapClosure ?? self.tapClosure)?()
|
||||
callActions(for: .longTap)
|
||||
break
|
||||
default:
|
||||
break
|
||||
@ -176,15 +207,78 @@ class CustomButtonCell: NSButtonCell {
|
||||
}
|
||||
}
|
||||
|
||||
class HapticClickGestureRecognizer: NSClickGestureRecognizer {
|
||||
// Thanks to https://stackoverflow.com/a/49843893
|
||||
final class MultiClickGestureRecognizer: NSClickGestureRecognizer {
|
||||
|
||||
private let _action: Selector
|
||||
private let _doubleAction: Selector
|
||||
private let _tripleAction: Selector
|
||||
private var _clickCount: Int = 0
|
||||
|
||||
public var isDoubleClickEnabled = true
|
||||
public var isTripleClickEnabled = true
|
||||
|
||||
override var action: Selector? {
|
||||
get {
|
||||
return nil /// prevent base class from performing any actions
|
||||
} set {
|
||||
if newValue != nil { // if they are trying to assign an actual action
|
||||
fatalError("Only use init(target:action:doubleAction) for assigning actions")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init(target: AnyObject, action: Selector, doubleAction: Selector, tripleAction: Selector) {
|
||||
_action = action
|
||||
_doubleAction = doubleAction
|
||||
_tripleAction = tripleAction
|
||||
super.init(target: target, action: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(target:action:doubleAction:tripleAction) is only support atm")
|
||||
}
|
||||
|
||||
override func touchesBegan(with event: NSEvent) {
|
||||
HapticFeedback.shared?.tap(strong: 2)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -17,9 +17,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.25</string>
|
||||
<string>0.27</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>297</string>
|
||||
<string>434</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.utilities</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
@ -39,7 +39,7 @@
|
||||
<key>NSHomeKitUsageDescription</key>
|
||||
<string>MTMR needs access to HomeKit for work</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2018 Anton Palgunov. All rights reserved.</string>
|
||||
<string>Copyright © 2018 - 2020 Anton Palgunov. All rights reserved.</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>Weather widget need your location for correct work</string>
|
||||
<key>NSLocationAlwaysUsageDescription</key>
|
||||
|
||||
@ -3,47 +3,52 @@ import Foundation
|
||||
|
||||
extension Data {
|
||||
func barItemDefinitions() -> [BarItemDefinition]? {
|
||||
return try? JSONDecoder().decode([BarItemDefinition].self, from: utf8string!.stripComments().data(using: .utf8)!)
|
||||
return try! JSONDecoder().decode([BarItemDefinition].self, from: utf8string!.stripComments().data(using: .utf8)!)
|
||||
}
|
||||
}
|
||||
|
||||
struct BarItemDefinition: Decodable {
|
||||
let type: ItemType
|
||||
let action: ActionType
|
||||
let longAction: LongActionType
|
||||
let actions: [Action]
|
||||
let legacyAction: LegacyActionType
|
||||
let legacyLongAction: LegacyLongActionType
|
||||
let additionalParameters: [GeneralParameters.CodingKeys: GeneralParameter]
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case actions
|
||||
}
|
||||
|
||||
init(type: ItemType, action: ActionType, longAction: LongActionType, additionalParameters: [GeneralParameters.CodingKeys: GeneralParameter]) {
|
||||
init(type: ItemType, actions: [Action], action: LegacyActionType, legacyLongAction: LegacyLongActionType, additionalParameters: [GeneralParameters.CodingKeys: GeneralParameter]) {
|
||||
self.type = type
|
||||
self.action = action
|
||||
self.longAction = longAction
|
||||
self.actions = actions
|
||||
self.legacyAction = action
|
||||
self.legacyLongAction = legacyLongAction
|
||||
self.additionalParameters = additionalParameters
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let type = try container.decode(String.self, forKey: .type)
|
||||
let parametersDecoder = SupportedTypesHolder.sharedInstance.lookup(by: type)
|
||||
let actions = try container.decodeIfPresent([Action].self, forKey: .actions)
|
||||
let parametersDecoder = SupportedTypesHolder.sharedInstance.lookup(by: type, actions: actions ?? [])
|
||||
var additionalParameters = try GeneralParameters(from: decoder).parameters
|
||||
|
||||
if let result = try? parametersDecoder(decoder),
|
||||
case let (itemType, action, longAction, parameters) = result {
|
||||
case let (itemType, actions, action, longAction, parameters) = result {
|
||||
parameters.forEach { additionalParameters[$0] = $1 }
|
||||
self.init(type: itemType, action: action, longAction: longAction, additionalParameters: additionalParameters)
|
||||
self.init(type: itemType, actions: actions, action: action, legacyLongAction: longAction, additionalParameters: additionalParameters)
|
||||
} else {
|
||||
self.init(type: .staticButton(title: "unknown"), action: .none, longAction: .none, additionalParameters: additionalParameters)
|
||||
self.init(type: .staticButton(title: "unknown"), actions: [], action: .none, legacyLongAction: .none, additionalParameters: additionalParameters)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typealias ParametersDecoder = (Decoder) throws -> (
|
||||
item: ItemType,
|
||||
action: ActionType,
|
||||
longAction: LongActionType,
|
||||
actions: [Action],
|
||||
legacyAction: LegacyActionType,
|
||||
legacyLongAction: LegacyLongActionType,
|
||||
parameters: [GeneralParameters.CodingKeys: GeneralParameter]
|
||||
)
|
||||
|
||||
@ -51,8 +56,11 @@ class SupportedTypesHolder {
|
||||
private var supportedTypes: [String: ParametersDecoder] = [
|
||||
"escape": { _ in (
|
||||
item: .staticButton(title: "esc"),
|
||||
action: .keyPress(keycode: 53),
|
||||
longAction: .none,
|
||||
actions: [
|
||||
Action(trigger: .singleTap, value: .keyPress(keycode: 53))
|
||||
],
|
||||
legacyAction: .none,
|
||||
legacyLongAction: .none,
|
||||
parameters: [.align: .align(.left)]
|
||||
) },
|
||||
|
||||
@ -65,8 +73,11 @@ class SupportedTypesHolder {
|
||||
|
||||
"delete": { _ in (
|
||||
item: .staticButton(title: "del"),
|
||||
action: .keyPress(keycode: 117),
|
||||
longAction: .none,
|
||||
actions: [
|
||||
Action(trigger: .singleTap, value: .keyPress(keycode: 117))
|
||||
],
|
||||
legacyAction: .none,
|
||||
legacyLongAction: .none,
|
||||
parameters: [:]
|
||||
) },
|
||||
|
||||
@ -74,8 +85,11 @@ class SupportedTypesHolder {
|
||||
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "brightnessUp"))
|
||||
return (
|
||||
item: .staticButton(title: ""),
|
||||
action: .keyPress(keycode: 144),
|
||||
longAction: .none,
|
||||
actions: [
|
||||
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_BRIGHTNESS_UP))
|
||||
],
|
||||
legacyAction: .none,
|
||||
legacyLongAction: .none,
|
||||
parameters: [.image: imageParameter]
|
||||
)
|
||||
},
|
||||
@ -84,8 +98,11 @@ class SupportedTypesHolder {
|
||||
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "brightnessDown"))
|
||||
return (
|
||||
item: .staticButton(title: ""),
|
||||
action: .keyPress(keycode: 145),
|
||||
longAction: .none,
|
||||
actions: [
|
||||
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_BRIGHTNESS_DOWN))
|
||||
],
|
||||
legacyAction: .none,
|
||||
legacyLongAction: .none,
|
||||
parameters: [.image: imageParameter]
|
||||
)
|
||||
},
|
||||
@ -94,8 +111,11 @@ class SupportedTypesHolder {
|
||||
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "ill_up"))
|
||||
return (
|
||||
item: .staticButton(title: ""),
|
||||
action: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_UP),
|
||||
longAction: .none,
|
||||
actions: [
|
||||
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_UP))
|
||||
],
|
||||
legacyAction: .none,
|
||||
legacyLongAction: .none,
|
||||
parameters: [.image: imageParameter]
|
||||
)
|
||||
},
|
||||
@ -104,8 +124,11 @@ class SupportedTypesHolder {
|
||||
let imageParameter = GeneralParameter.image(source: #imageLiteral(resourceName: "ill_down"))
|
||||
return (
|
||||
item: .staticButton(title: ""),
|
||||
action: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_DOWN),
|
||||
longAction: .none,
|
||||
actions: [
|
||||
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_ILLUMINATION_DOWN))
|
||||
],
|
||||
legacyAction: .none,
|
||||
legacyLongAction: .none,
|
||||
parameters: [.image: imageParameter]
|
||||
)
|
||||
},
|
||||
@ -114,8 +137,11 @@ class SupportedTypesHolder {
|
||||
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarVolumeDownTemplateName)!)
|
||||
return (
|
||||
item: .staticButton(title: ""),
|
||||
action: .hidKey(keycode: NX_KEYTYPE_SOUND_DOWN),
|
||||
longAction: .none,
|
||||
actions: [
|
||||
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_SOUND_DOWN))
|
||||
],
|
||||
legacyAction: .none,
|
||||
legacyLongAction: .none,
|
||||
parameters: [.image: imageParameter]
|
||||
)
|
||||
},
|
||||
@ -124,8 +150,11 @@ class SupportedTypesHolder {
|
||||
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarVolumeUpTemplateName)!)
|
||||
return (
|
||||
item: .staticButton(title: ""),
|
||||
action: .hidKey(keycode: NX_KEYTYPE_SOUND_UP),
|
||||
longAction: .none,
|
||||
actions: [
|
||||
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_SOUND_UP))
|
||||
],
|
||||
legacyAction: .none,
|
||||
legacyLongAction: .none,
|
||||
parameters: [.image: imageParameter]
|
||||
)
|
||||
},
|
||||
@ -134,8 +163,11 @@ class SupportedTypesHolder {
|
||||
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarAudioOutputMuteTemplateName)!)
|
||||
return (
|
||||
item: .staticButton(title: ""),
|
||||
action: .hidKey(keycode: NX_KEYTYPE_MUTE),
|
||||
longAction: .none,
|
||||
actions: [
|
||||
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_MUTE))
|
||||
],
|
||||
legacyAction: .none,
|
||||
legacyLongAction: .none,
|
||||
parameters: [.image: imageParameter]
|
||||
)
|
||||
},
|
||||
@ -144,8 +176,11 @@ class SupportedTypesHolder {
|
||||
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarRewindTemplateName)!)
|
||||
return (
|
||||
item: .staticButton(title: ""),
|
||||
action: .hidKey(keycode: NX_KEYTYPE_PREVIOUS),
|
||||
longAction: .none,
|
||||
actions: [
|
||||
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_PREVIOUS))
|
||||
],
|
||||
legacyAction: .none,
|
||||
legacyLongAction: .none,
|
||||
parameters: [.image: imageParameter]
|
||||
)
|
||||
},
|
||||
@ -154,8 +189,11 @@ class SupportedTypesHolder {
|
||||
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarPlayPauseTemplateName)!)
|
||||
return (
|
||||
item: .staticButton(title: ""),
|
||||
action: .hidKey(keycode: NX_KEYTYPE_PLAY),
|
||||
longAction: .none,
|
||||
actions: [
|
||||
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_PLAY))
|
||||
],
|
||||
legacyAction: .none,
|
||||
legacyLongAction: .none,
|
||||
parameters: [.image: imageParameter]
|
||||
)
|
||||
},
|
||||
@ -164,23 +202,32 @@ class SupportedTypesHolder {
|
||||
let imageParameter = GeneralParameter.image(source: NSImage(named: NSImage.touchBarFastForwardTemplateName)!)
|
||||
return (
|
||||
item: .staticButton(title: ""),
|
||||
action: .hidKey(keycode: NX_KEYTYPE_NEXT),
|
||||
longAction: .none,
|
||||
actions: [
|
||||
Action(trigger: .singleTap, value: .hidKey(keycode: NX_KEYTYPE_NEXT))
|
||||
],
|
||||
legacyAction: .none,
|
||||
legacyLongAction: .none,
|
||||
parameters: [.image: imageParameter]
|
||||
)
|
||||
},
|
||||
|
||||
"sleep": { _ in (
|
||||
item: .staticButton(title: "☕️"),
|
||||
action: .shellScript(executable: "/usr/bin/pmset", parameters: ["sleepnow"]),
|
||||
longAction: .none,
|
||||
actions: [
|
||||
Action(trigger: .singleTap, value: .shellScript(executable: "/usr/bin/pmset", parameters: ["sleepnow"]))
|
||||
],
|
||||
legacyAction: .none,
|
||||
legacyLongAction: .none,
|
||||
parameters: [:]
|
||||
) },
|
||||
|
||||
"displaySleep": { _ in (
|
||||
item: .staticButton(title: "☕️"),
|
||||
action: .shellScript(executable: "/usr/bin/pmset", parameters: ["displaysleepnow"]),
|
||||
longAction: .none,
|
||||
actions: [
|
||||
Action(trigger: .singleTap, value: .shellScript(executable: "/usr/bin/pmset", parameters: ["displaysleepnow"]))
|
||||
],
|
||||
legacyAction: .none,
|
||||
legacyLongAction: .none,
|
||||
parameters: [:]
|
||||
) },
|
||||
|
||||
@ -209,11 +256,12 @@ class SupportedTypesHolder {
|
||||
|
||||
static let sharedInstance = SupportedTypesHolder()
|
||||
|
||||
func lookup(by type: String) -> ParametersDecoder {
|
||||
func lookup(by type: String, actions: [Action]) -> ParametersDecoder {
|
||||
return supportedTypes[type] ?? { decoder in (
|
||||
item: try ItemType(from: decoder),
|
||||
action: try ActionType(from: decoder),
|
||||
longAction: try LongActionType(from: decoder),
|
||||
actions: actions,
|
||||
legacyAction: try LegacyActionType(from: decoder),
|
||||
legacyLongAction: try LegacyLongActionType(from: decoder),
|
||||
parameters: [:]
|
||||
) }
|
||||
}
|
||||
@ -222,12 +270,13 @@ class SupportedTypesHolder {
|
||||
supportedTypes[typename] = decoder
|
||||
}
|
||||
|
||||
func register(typename: String, item: ItemType, action: ActionType, longAction: LongActionType) {
|
||||
func register(typename: String, item: ItemType, actions: [Action], legacyAction: LegacyActionType, legacyLongAction: LegacyLongActionType) {
|
||||
register(typename: typename) { _ in
|
||||
(
|
||||
item: item,
|
||||
action,
|
||||
longAction,
|
||||
actions,
|
||||
legacyAction,
|
||||
legacyLongAction,
|
||||
parameters: [:]
|
||||
)
|
||||
}
|
||||
@ -236,7 +285,7 @@ class SupportedTypesHolder {
|
||||
|
||||
enum ItemType: Decodable {
|
||||
case staticButton(title: String)
|
||||
case appleScriptTitledButton(source: SourceProtocol, refreshInterval: Double)
|
||||
case appleScriptTitledButton(source: SourceProtocol, refreshInterval: Double, alternativeImages: [String: SourceProtocol])
|
||||
case shellScriptTitledButton(source: SourceProtocol, refreshInterval: Double)
|
||||
case timeButton(formatTemplate: String, timeZone: String?, locale: String?)
|
||||
case battery
|
||||
@ -255,6 +304,8 @@ enum ItemType: Decodable {
|
||||
case pomodoro(workTime: Double, restTime: Double)
|
||||
case network(flip: Bool)
|
||||
case darkMode
|
||||
case swipe(direction: String, fingers: Int, minOffset: Float, sourceApple: SourceProtocol?, sourceBash: SourceProtocol?)
|
||||
case upnext(from: Double, to: Double, maxToShow: Int, autoResize: Bool)
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
@ -280,6 +331,13 @@ enum ItemType: Decodable {
|
||||
case autoResize
|
||||
case filter
|
||||
case disableMarquee
|
||||
case alternativeImages
|
||||
case sourceApple
|
||||
case sourceBash
|
||||
case direction
|
||||
case fingers
|
||||
case minOffset
|
||||
case maxToShow
|
||||
}
|
||||
|
||||
enum ItemTypeRaw: String, Decodable {
|
||||
@ -303,6 +361,8 @@ enum ItemType: Decodable {
|
||||
case pomodoro
|
||||
case network
|
||||
case darkMode
|
||||
case swipe
|
||||
case upnext
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
@ -312,7 +372,8 @@ enum ItemType: Decodable {
|
||||
case .appleScriptTitledButton:
|
||||
let source = try container.decode(Source.self, forKey: .source)
|
||||
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 1800.0
|
||||
self = .appleScriptTitledButton(source: source, refreshInterval: interval)
|
||||
let alternativeImages = try container.decodeIfPresent([String: Source].self, forKey: .alternativeImages) ?? [:]
|
||||
self = .appleScriptTitledButton(source: source, refreshInterval: interval, alternativeImages: alternativeImages)
|
||||
|
||||
case .shellScriptTitledButton:
|
||||
let source = try container.decode(Source.self, forKey: .source)
|
||||
@ -396,11 +457,114 @@ enum ItemType: Decodable {
|
||||
|
||||
case .darkMode:
|
||||
self = .darkMode
|
||||
|
||||
case .swipe:
|
||||
let sourceApple = try container.decodeIfPresent(Source.self, forKey: .sourceApple)
|
||||
let sourceBash = try container.decodeIfPresent(Source.self, forKey: .sourceBash)
|
||||
let direction = try container.decode(String.self, forKey: .direction)
|
||||
let fingers = try container.decode(Int.self, forKey: .fingers)
|
||||
let minOffset = try container.decodeIfPresent(Float.self, forKey: .minOffset) ?? 0.0
|
||||
self = .swipe(direction: direction, fingers: fingers, minOffset: minOffset, sourceApple: sourceApple, sourceBash: sourceBash)
|
||||
|
||||
case .upnext:
|
||||
let from = try container.decodeIfPresent(Double.self, forKey: .from) ?? 0 // Lower bounds of period of time in hours to search for events
|
||||
let to = try container.decodeIfPresent(Double.self, forKey: .to) ?? 12 // Upper bounds of period of time in hours to search for events
|
||||
let maxToShow = try container.decodeIfPresent(Int.self, forKey: .maxToShow) ?? 3 // 1 indexed array. Get the 1st, 2nd, 3rd event to display multiple notifications
|
||||
let autoResize = try container.decodeIfPresent(Bool.self, forKey: .autoResize) ?? false
|
||||
let interval = try container.decodeIfPresent(Double.self, forKey: .refreshInterval) ?? 60.0
|
||||
self = .upnext(from: from, to: to, maxToShow: maxToShow, autoResize: autoResize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ActionType: Decodable {
|
||||
struct FailableDecodable<Base : Decodable> : Decodable {
|
||||
|
||||
let base: Base?
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
self.base = try? container.decode(Base.self)
|
||||
}
|
||||
}
|
||||
|
||||
struct Action: Decodable {
|
||||
enum Trigger: String, Decodable {
|
||||
case singleTap
|
||||
case doubleTap
|
||||
case tripleTap
|
||||
case longTap
|
||||
}
|
||||
|
||||
enum Value {
|
||||
case none
|
||||
case hidKey(keycode: Int32)
|
||||
case keyPress(keycode: Int)
|
||||
case appleScript(source: SourceProtocol)
|
||||
case shellScript(executable: String, parameters: [String])
|
||||
case custom(closure: () -> Void)
|
||||
case openUrl(url: String)
|
||||
}
|
||||
|
||||
private enum ActionTypeRaw: String, Decodable {
|
||||
case hidKey
|
||||
case keyPress
|
||||
case appleScript
|
||||
case shellScript
|
||||
case openUrl
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case trigger
|
||||
case action
|
||||
case keycode
|
||||
case actionAppleScript
|
||||
case executablePath
|
||||
case shellArguments
|
||||
case url
|
||||
}
|
||||
|
||||
let trigger: Trigger
|
||||
let value: Value
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
trigger = try container.decode(Trigger.self, forKey: .trigger)
|
||||
let type = try container.decodeIfPresent(ActionTypeRaw.self, forKey: .action)
|
||||
|
||||
switch type {
|
||||
case .some(.hidKey):
|
||||
let keycode = try container.decode(Int32.self, forKey: .keycode)
|
||||
value = .hidKey(keycode: keycode)
|
||||
|
||||
case .some(.keyPress):
|
||||
let keycode = try container.decode(Int.self, forKey: .keycode)
|
||||
value = .keyPress(keycode: keycode)
|
||||
|
||||
case .some(.appleScript):
|
||||
let source = try container.decode(Source.self, forKey: .actionAppleScript)
|
||||
value = .appleScript(source: source)
|
||||
|
||||
case .some(.shellScript):
|
||||
let executable = try container.decode(String.self, forKey: .executablePath)
|
||||
let parameters = try container.decodeIfPresent([String].self, forKey: .shellArguments) ?? []
|
||||
value = .shellScript(executable: executable, parameters: parameters)
|
||||
|
||||
case .some(.openUrl):
|
||||
let url = try container.decode(String.self, forKey: .url)
|
||||
value = .openUrl(url: url)
|
||||
case .none:
|
||||
value = .none
|
||||
}
|
||||
}
|
||||
|
||||
init(trigger: Trigger, value: Value) {
|
||||
self.trigger = trigger
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
enum LegacyActionType: Decodable {
|
||||
case none
|
||||
case hidKey(keycode: Int32)
|
||||
case keyPress(keycode: Int)
|
||||
@ -458,7 +622,7 @@ enum ActionType: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
enum LongActionType: Decodable {
|
||||
enum LegacyLongActionType: Decodable {
|
||||
case none
|
||||
case hidKey(keycode: Int32)
|
||||
case keyPress(keycode: Int)
|
||||
|
||||
@ -1,10 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
class ScrollViewItem: NSCustomTouchBarItem, NSGestureRecognizerDelegate {
|
||||
var twofingersPrev: CGFloat = 0.0
|
||||
var threefingersPrev: CGFloat = 0.0
|
||||
var twofingers: NSPanGestureRecognizer!
|
||||
var threefingers: NSPanGestureRecognizer!
|
||||
class ScrollViewItem: NSCustomTouchBarItem/*, NSGestureRecognizerDelegate*/ {
|
||||
|
||||
init(identifier: NSTouchBarItem.Identifier, items: [NSTouchBarItem]) {
|
||||
super.init(identifier: identifier)
|
||||
@ -15,70 +11,10 @@ class ScrollViewItem: NSCustomTouchBarItem, NSGestureRecognizerDelegate {
|
||||
let scrollView = NSScrollView(frame: CGRect(origin: .zero, size: stackView.fittingSize))
|
||||
scrollView.documentView = stackView
|
||||
view = scrollView
|
||||
|
||||
twofingers = NSPanGestureRecognizer(target: self, action: #selector(twofingersHandler(_:)))
|
||||
twofingers.allowedTouchTypes = .direct
|
||||
twofingers.numberOfTouchesRequired = 2
|
||||
view.addGestureRecognizer(twofingers)
|
||||
|
||||
threefingers = NSPanGestureRecognizer(target: self, action: #selector(threefingersHandler(_:)))
|
||||
threefingers.allowedTouchTypes = .direct
|
||||
threefingers.numberOfTouchesRequired = 3
|
||||
view.addGestureRecognizer(threefingers)
|
||||
}
|
||||
|
||||
var gesturesEnabled = true {
|
||||
didSet {
|
||||
twofingers.isEnabled = gesturesEnabled
|
||||
threefingers.isEnabled = gesturesEnabled
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc func twofingersHandler(_ sender: NSGestureRecognizer?) { // Volume
|
||||
let position = (sender?.location(in: sender?.view).x)!
|
||||
|
||||
switch sender!.state {
|
||||
case .began:
|
||||
twofingersPrev = position
|
||||
case .changed:
|
||||
if ((position - twofingersPrev) > 10) || ((twofingersPrev - position) > 10) {
|
||||
if position > twofingersPrev {
|
||||
HIDPostAuxKey(NX_KEYTYPE_SOUND_UP)
|
||||
} else if position < twofingersPrev {
|
||||
HIDPostAuxKey(NX_KEYTYPE_SOUND_DOWN)
|
||||
}
|
||||
twofingersPrev = position
|
||||
}
|
||||
case .ended:
|
||||
twofingersPrev = 0.0
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@objc func threefingersHandler(_ sender: NSGestureRecognizer?) { // Brightness
|
||||
let position = (sender?.location(in: sender?.view).x)!
|
||||
|
||||
switch sender!.state {
|
||||
case .began:
|
||||
threefingersPrev = position
|
||||
case .changed:
|
||||
if ((position - threefingersPrev) > 15) || ((threefingersPrev - position) > 15) {
|
||||
if position > threefingersPrev {
|
||||
GenericKeyPress(keyCode: CGKeyCode(144)).send()
|
||||
} else if position < threefingersPrev {
|
||||
GenericKeyPress(keyCode: CGKeyCode(145)).send()
|
||||
}
|
||||
threefingersPrev = position
|
||||
}
|
||||
case .ended:
|
||||
threefingersPrev = 0.0
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,11 +62,19 @@ class ShellScriptTouchBarItem: CustomButtonTouchBarItem {
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
66
MTMR/SwipeItem.swift
Normal file
66
MTMR/SwipeItem.swift
Normal file
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -59,6 +59,10 @@ extension ItemType {
|
||||
return NetworkBarItem.identifier
|
||||
case .darkMode:
|
||||
return DarkModeBarItem.identifier
|
||||
case .swipe(direction: _, fingers: _, minOffset: _, sourceApple: _, sourceBash: _):
|
||||
return "com.toxblh.mtmr.swipe."
|
||||
case .upnext(from: _, to: _, maxToShow: _, autoResize: _):
|
||||
return "com.connorgmeehan.mtmrup.next."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -78,10 +82,10 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
||||
var items: [NSTouchBarItem.Identifier: NSTouchBarItem] = [:]
|
||||
var leftIdentifiers: [NSTouchBarItem.Identifier] = []
|
||||
var centerIdentifiers: [NSTouchBarItem.Identifier] = []
|
||||
var centerItems: [NSTouchBarItem] = []
|
||||
var rightIdentifiers: [NSTouchBarItem.Identifier] = []
|
||||
var scrollArea: ScrollViewItem?
|
||||
var centerScrollArea = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollArea.".appending(UUID().uuidString))
|
||||
var basicViewIdentifier = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollView.".appending(UUID().uuidString))
|
||||
var basicView: BasicView?
|
||||
var swipeItems: [SwipeItem] = []
|
||||
|
||||
var blacklistAppIdentifiers: [String] = []
|
||||
var frontmostApplicationIdentifier: String? {
|
||||
@ -90,13 +94,28 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
||||
|
||||
private override init() {
|
||||
super.init()
|
||||
SupportedTypesHolder.sharedInstance.register(typename: "exitTouchbar", item: .staticButton(title: "exit"), action: .custom(closure: { [weak self] in self?.dismissTouchBar() }), longAction: .none)
|
||||
SupportedTypesHolder.sharedInstance.register(
|
||||
typename: "exitTouchbar",
|
||||
item: .staticButton(title: "exit"),
|
||||
actions: [
|
||||
Action(trigger: .singleTap, value: .custom(closure: { [weak self] in self?.dismissTouchBar() }))
|
||||
],
|
||||
legacyAction: .none,
|
||||
legacyLongAction: .none
|
||||
)
|
||||
|
||||
SupportedTypesHolder.sharedInstance.register(typename: "close") { _ in
|
||||
(item: .staticButton(title: ""), action: .custom(closure: { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
self.reloadPreset(path: self.lastPresetPath)
|
||||
}), longAction: .none, parameters: [.width: .width(30), .image: .image(source: (NSImage(named: NSImage.stopProgressFreestandingTemplateName))!)])
|
||||
(
|
||||
item: .staticButton(title: ""),
|
||||
actions: [
|
||||
Action(trigger: .singleTap, value: .custom(closure: { [weak self] in
|
||||
guard let `self` = self else { return }
|
||||
self.reloadPreset(path: self.lastPresetPath)
|
||||
}))
|
||||
],
|
||||
legacyAction: .none,
|
||||
legacyLongAction: .none,
|
||||
parameters: [.width: .width(30), .image: .image(source: (NSImage(named: NSImage.stopProgressFreestandingTemplateName))!)])
|
||||
}
|
||||
|
||||
blacklistAppIdentifiers = AppSettings.blacklistedAppIds
|
||||
@ -116,24 +135,29 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
||||
jsonItems = newJsonItems
|
||||
itemDefinitions = [:]
|
||||
items = [:]
|
||||
leftIdentifiers = []
|
||||
centerItems = []
|
||||
rightIdentifiers = []
|
||||
|
||||
loadItemDefinitions(jsonItems: jsonItems)
|
||||
createItems()
|
||||
|
||||
centerItems = centerIdentifiers.compactMap({ (identifier) -> NSTouchBarItem? in
|
||||
let centerItems = centerIdentifiers.compactMap({ (identifier) -> NSTouchBarItem? in
|
||||
items[identifier]
|
||||
})
|
||||
|
||||
centerScrollArea = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollArea.".appending(UUID().uuidString))
|
||||
scrollArea = ScrollViewItem(identifier: centerScrollArea, items: centerItems)
|
||||
scrollArea?.gesturesEnabled = AppSettings.multitouchGestures
|
||||
let centerScrollArea = NSTouchBarItem.Identifier("com.toxblh.mtmr.scrollArea.".appending(UUID().uuidString))
|
||||
let scrollArea = ScrollViewItem(identifier: centerScrollArea, items: centerItems)
|
||||
|
||||
touchBar.delegate = self
|
||||
touchBar.defaultItemIdentifiers = []
|
||||
touchBar.defaultItemIdentifiers = leftIdentifiers + [centerScrollArea] + rightIdentifiers
|
||||
touchBar.defaultItemIdentifiers = [basicViewIdentifier]
|
||||
|
||||
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()
|
||||
}
|
||||
@ -163,7 +187,7 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
||||
|
||||
func reloadPreset(path: String) {
|
||||
lastPresetPath = path
|
||||
let items = path.fileData?.barItemDefinitions() ?? [BarItemDefinition(type: .staticButton(title: "bad preset"), action: .none, longAction: .none, additionalParameters: [:])]
|
||||
let items = path.fileData?.barItemDefinitions() ?? [BarItemDefinition(type: .staticButton(title: "bad preset"), actions: [], action: .none, legacyLongAction: .none, additionalParameters: [:])]
|
||||
createAndUpdatePreset(newJsonItems: items)
|
||||
}
|
||||
|
||||
@ -189,7 +213,12 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
||||
|
||||
func createItems() {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -225,16 +254,11 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
||||
}
|
||||
|
||||
func touchBar(_: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? {
|
||||
if identifier == centerScrollArea {
|
||||
return scrollArea
|
||||
if identifier == basicViewIdentifier {
|
||||
return basicView
|
||||
}
|
||||
|
||||
guard let item = self.items[identifier],
|
||||
let definition = self.itemDefinitions[identifier],
|
||||
definition.align != .center else {
|
||||
return nil
|
||||
}
|
||||
return item
|
||||
return nil
|
||||
}
|
||||
|
||||
func createItem(forIdentifier identifier: NSTouchBarItem.Identifier, definition item: BarItemDefinition) -> NSTouchBarItem? {
|
||||
@ -242,8 +266,8 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
||||
switch item.type {
|
||||
case let .staticButton(title: title):
|
||||
barItem = CustomButtonTouchBarItem(identifier: identifier, title: title)
|
||||
case let .appleScriptTitledButton(source: source, refreshInterval: interval):
|
||||
barItem = AppleScriptTouchBarItem(identifier: identifier, source: source, interval: interval)
|
||||
case let .appleScriptTitledButton(source: source, refreshInterval: interval, alternativeImages: alternativeImages):
|
||||
barItem = AppleScriptTouchBarItem(identifier: identifier, source: source, interval: interval, alternativeImages: alternativeImages)
|
||||
case let .shellScriptTitledButton(source: source, refreshInterval: interval):
|
||||
barItem = ShellScriptTouchBarItem(identifier: identifier, source: source, interval: interval)
|
||||
case let .timeButton(formatTemplate: template, timeZone: timeZone, locale: locale):
|
||||
@ -300,13 +324,23 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
||||
barItem = NetworkBarItem(identifier: identifier, flip: flip)
|
||||
case .darkMode:
|
||||
barItem = DarkModeBarItem(identifier: identifier)
|
||||
case let .swipe(direction: direction, fingers: fingers, minOffset: minOffset, sourceApple: sourceApple, sourceBash: sourceBash):
|
||||
barItem = SwipeItem(identifier: identifier, direction: direction, fingers: fingers, minOffset: minOffset, sourceApple: sourceApple, sourceBash: sourceBash)
|
||||
case let .upnext(from: from, to: to, maxToShow: maxToShow, autoResize: autoResize):
|
||||
barItem = UpNextScrubberTouchBarItem(identifier: identifier, interval: 60, from: from, to: to, maxToShow: maxToShow, autoResize: autoResize)
|
||||
}
|
||||
|
||||
if let action = self.action(forItem: item), let item = barItem as? CustomButtonTouchBarItem {
|
||||
item.tapClosure = action
|
||||
item.actions.append(ItemAction(trigger: .singleTap, action))
|
||||
}
|
||||
if let longAction = self.longAction(forItem: item), let item = barItem as? CustomButtonTouchBarItem {
|
||||
item.longTapClosure = longAction
|
||||
item.actions.append(ItemAction(trigger: .longTap, longAction))
|
||||
}
|
||||
|
||||
if let touchBarItem = barItem as? CustomButtonTouchBarItem {
|
||||
for action in item.actions {
|
||||
touchBarItem.actions.append(ItemAction(trigger: action.trigger, self.closure(for: action)))
|
||||
}
|
||||
}
|
||||
if case let .bordered(bordered)? = item.additionalParameters[.bordered], let item = barItem as? CustomButtonTouchBarItem {
|
||||
item.isBordered = bordered
|
||||
@ -329,9 +363,53 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
||||
}
|
||||
return barItem
|
||||
}
|
||||
|
||||
func closure(for action: Action) -> (() -> Void)? {
|
||||
switch action.value {
|
||||
case let .hidKey(keycode: keycode):
|
||||
return { HIDPostAuxKey(keycode) }
|
||||
case let .keyPress(keycode: keycode):
|
||||
return { GenericKeyPress(keyCode: CGKeyCode(keycode)).send() }
|
||||
case let .appleScript(source: source):
|
||||
guard let appleScript = source.appleScript else {
|
||||
print("cannot create apple script for item \(action)")
|
||||
return {}
|
||||
}
|
||||
return {
|
||||
DispatchQueue.appleScriptQueue.async {
|
||||
var error: NSDictionary?
|
||||
appleScript.executeAndReturnError(&error)
|
||||
if let error = error {
|
||||
print("error \(error) when handling \(action) ")
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .shellScript(executable: executable, parameters: parameters):
|
||||
return {
|
||||
let task = Process()
|
||||
task.launchPath = executable
|
||||
task.arguments = parameters
|
||||
task.launch()
|
||||
}
|
||||
case let .openUrl(url: url):
|
||||
return {
|
||||
if let url = URL(string: url), NSWorkspace.shared.open(url) {
|
||||
#if DEBUG
|
||||
print("URL was successfully opened")
|
||||
#endif
|
||||
} else {
|
||||
print("error", url)
|
||||
}
|
||||
}
|
||||
case let .custom(closure: closure):
|
||||
return closure
|
||||
case .none:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func action(forItem item: BarItemDefinition) -> (() -> Void)? {
|
||||
switch item.action {
|
||||
switch item.legacyAction {
|
||||
case let .hidKey(keycode: keycode):
|
||||
return { HIDPostAuxKey(keycode) }
|
||||
case let .keyPress(keycode: keycode):
|
||||
@ -375,7 +453,7 @@ class TouchBarController: NSObject, NSTouchBarDelegate {
|
||||
}
|
||||
|
||||
func longAction(forItem item: BarItemDefinition) -> (() -> Void)? {
|
||||
switch item.longAction {
|
||||
switch item.legacyLongAction {
|
||||
case let .hidKey(keycode: keycode):
|
||||
return { HIDPostAuxKey(keycode) }
|
||||
case let .keyPress(keycode: keycode):
|
||||
@ -427,6 +505,12 @@ extension NSCustomTouchBarItem: CanSetWidth {
|
||||
}
|
||||
}
|
||||
|
||||
extension NSPopoverTouchBarItem: CanSetWidth {
|
||||
func setWidth(value: CGFloat) {
|
||||
view?.widthAnchor.constraint(equalToConstant: value).isActive = true
|
||||
}
|
||||
}
|
||||
|
||||
extension BarItemDefinition {
|
||||
var align: Align {
|
||||
if case let .align(result)? = additionalParameters[.align] {
|
||||
|
||||
@ -82,12 +82,14 @@ class AppScrubberTouchBarItem: NSCustomTouchBarItem {
|
||||
public func createAppButton(for app: DockItem) -> DockBarItem {
|
||||
let item = DockBarItem(app)
|
||||
item.isBordered = false
|
||||
item.tapClosure = { [weak self] in
|
||||
self?.switchToApp(app: app)
|
||||
}
|
||||
item.longTapClosure = { [weak self] in
|
||||
self?.handleHalfLongPress(item: app)
|
||||
}
|
||||
item.actions.append(contentsOf: [
|
||||
ItemAction(trigger: .singleTap) { [weak self] in
|
||||
self?.switchToApp(app: app)
|
||||
},
|
||||
ItemAction(trigger: .longTap) { [weak self] in
|
||||
self?.handleHalfLongPress(item: app)
|
||||
}
|
||||
])
|
||||
item.killAppClosure = {[weak self] in
|
||||
self?.handleLongPress(item: app)
|
||||
}
|
||||
|
||||
@ -15,6 +15,9 @@ class CurrencyBarItem: CustomButtonTouchBarItem {
|
||||
private var postfix: String
|
||||
private var from: String
|
||||
private var to: String
|
||||
private var decimal: Int
|
||||
private var decimalValue: Float32!
|
||||
private var decimalString: String!
|
||||
private var oldValue: Float32!
|
||||
private var full: Bool = false
|
||||
|
||||
@ -32,11 +35,29 @@ class CurrencyBarItem: CustomButtonTouchBarItem {
|
||||
"IDR": "Rp",
|
||||
"MXN": "$",
|
||||
"SGD": "$",
|
||||
"CHF": "Fr.",
|
||||
"BTC": "฿",
|
||||
"LTC": "Ł",
|
||||
"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) {
|
||||
activity = NSBackgroundActivityScheduler(identifier: "\(identifier.rawValue).updatecheck")
|
||||
@ -57,6 +78,14 @@ class CurrencyBarItem: CustomButtonTouchBarItem {
|
||||
postfix = to
|
||||
}
|
||||
|
||||
|
||||
if let decimal = decimals[to] {
|
||||
self.decimal = decimal
|
||||
} else {
|
||||
decimal = 2
|
||||
}
|
||||
|
||||
|
||||
super.init(identifier: identifier, title: "⏳")
|
||||
|
||||
activity.repeats = true
|
||||
@ -114,10 +143,12 @@ class CurrencyBarItem: CustomButtonTouchBarItem {
|
||||
}
|
||||
|
||||
oldValue = value
|
||||
decimalValue = (value * pow(10,Float(decimal))).rounded() / pow(10,Float(decimal))
|
||||
decimalString = String(decimalValue)
|
||||
|
||||
var title = ""
|
||||
if full {
|
||||
title = String(format: "%@‣%.2f%@", prefix, value, postfix)
|
||||
title = String(format: "%@%@‣%@", prefix, postfix, decimalString)
|
||||
} else {
|
||||
title = String(format: "%@%.2f", prefix, value)
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@ class DarkModeBarItem: CustomButtonTouchBarItem, Widget {
|
||||
isBordered = false
|
||||
setWidth(value: 24)
|
||||
|
||||
tapClosure = { [weak self] in self?.DarkModeToggle() }
|
||||
actions.append(ItemAction(trigger: .singleTap) { [weak self] in self?.DarkModeToggle() })
|
||||
|
||||
timer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(refresh), userInfo: nil, repeats: true)
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ class DnDBarItem: CustomButtonTouchBarItem {
|
||||
isBordered = false
|
||||
setWidth(value: 32)
|
||||
|
||||
tapClosure = { [weak self] in self?.DnDToggle() }
|
||||
actions.append(ItemAction(trigger: .singleTap) { [weak self] in self?.DnDToggle() })
|
||||
|
||||
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(refresh), userInfo: nil, repeats: true)
|
||||
|
||||
|
||||
@ -18,9 +18,10 @@ class InputSourceBarItem: CustomButtonTouchBarItem {
|
||||
|
||||
observeIputSourceChangedNotification()
|
||||
textInputSourceDidChange()
|
||||
tapClosure = { [weak self] in
|
||||
|
||||
actions.append(ItemAction(trigger: .singleTap) { [weak self] in
|
||||
self?.switchInputSource()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
required init?(coder _: NSCoder) {
|
||||
|
||||
@ -11,6 +11,7 @@ import ScriptingBridge
|
||||
|
||||
class MusicBarItem: CustomButtonTouchBarItem {
|
||||
private enum Player: String {
|
||||
case Music = "com.apple.Music"
|
||||
case iTunes = "com.apple.iTunes"
|
||||
case Spotify = "com.spotify.client"
|
||||
case VOX = "com.coppertino.Vox"
|
||||
@ -19,6 +20,7 @@ class MusicBarItem: CustomButtonTouchBarItem {
|
||||
}
|
||||
|
||||
private let playerBundleIdentifiers = [
|
||||
Player.Music,
|
||||
Player.iTunes,
|
||||
Player.Spotify,
|
||||
Player.VOX,
|
||||
@ -39,9 +41,12 @@ class MusicBarItem: CustomButtonTouchBarItem {
|
||||
|
||||
super.init(identifier: identifier, title: "⏳")
|
||||
isBordered = false
|
||||
|
||||
tapClosure = { [weak self] in self?.playPause() }
|
||||
longTapClosure = { [weak self] in self?.nextTrack() }
|
||||
|
||||
actions = [
|
||||
ItemAction(trigger: .singleTap) { [weak self] in self?.playPause() },
|
||||
ItemAction(trigger: .doubleTap) { [weak self] in self?.previousTrack() },
|
||||
ItemAction(trigger: .longTap) { [weak self] in self?.nextTrack() }
|
||||
]
|
||||
|
||||
refreshAndSchedule()
|
||||
}
|
||||
@ -71,6 +76,10 @@ class MusicBarItem: CustomButtonTouchBarItem {
|
||||
let mp = (musicPlayer as iTunesApplication)
|
||||
mp.playpause!()
|
||||
return
|
||||
} else if ident == .Music {
|
||||
let mp = (musicPlayer as MusicApplication)
|
||||
mp.playpause!()
|
||||
return
|
||||
} else if ident == .VOX {
|
||||
let mp = (musicPlayer as VoxApplication)
|
||||
mp.playpause!()
|
||||
@ -134,6 +143,11 @@ class MusicBarItem: CustomButtonTouchBarItem {
|
||||
mp.nextTrack!()
|
||||
updatePlayer()
|
||||
return
|
||||
} else if ident == .Music {
|
||||
let mp = (musicPlayer as MusicApplication)
|
||||
mp.nextTrack!()
|
||||
updatePlayer()
|
||||
return
|
||||
} else if ident == .VOX {
|
||||
let mp = (musicPlayer as VoxApplication)
|
||||
mp.next!()
|
||||
@ -166,6 +180,31 @@ class MusicBarItem: CustomButtonTouchBarItem {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc func previousTrack() {
|
||||
for ident in playerBundleIdentifiers {
|
||||
if let musicPlayer = SBApplication(bundleIdentifier: ident.rawValue) {
|
||||
if musicPlayer.isRunning {
|
||||
if ident == .Spotify {
|
||||
let mp = (musicPlayer as SpotifyApplication)
|
||||
mp.previousTrack!()
|
||||
updatePlayer()
|
||||
return
|
||||
} else if ident == .iTunes {
|
||||
let mp = (musicPlayer as iTunesApplication)
|
||||
mp.previousTrack!()
|
||||
updatePlayer()
|
||||
return
|
||||
} else if ident == .Music {
|
||||
let mp = (musicPlayer as MusicApplication)
|
||||
mp.previousTrack!()
|
||||
updatePlayer()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refreshAndSchedule() {
|
||||
DispatchQueue.main.async {
|
||||
@ -188,6 +227,8 @@ class MusicBarItem: CustomButtonTouchBarItem {
|
||||
tempTitle = (musicPlayer as SpotifyApplication).title
|
||||
} else if ident == .iTunes {
|
||||
tempTitle = (musicPlayer as iTunesApplication).title
|
||||
} else if ident == .Music {
|
||||
tempTitle = (musicPlayer as MusicApplication).title
|
||||
} else if ident == .VOX {
|
||||
tempTitle = (musicPlayer as VoxApplication).title
|
||||
} else if ident == .Safari {
|
||||
@ -321,6 +362,29 @@ extension iTunesApplication {
|
||||
}
|
||||
}
|
||||
|
||||
@objc protocol MusicApplication {
|
||||
@objc optional var currentTrack: MusicTrack { get }
|
||||
@objc optional func playpause()
|
||||
@objc optional func nextTrack()
|
||||
@objc optional func previousTrack()
|
||||
}
|
||||
|
||||
extension SBApplication: MusicApplication {}
|
||||
|
||||
@objc protocol MusicTrack {
|
||||
@objc optional var artist: String { get }
|
||||
@objc optional var name: String { get }
|
||||
}
|
||||
|
||||
extension SBObject: MusicTrack {}
|
||||
|
||||
extension MusicApplication {
|
||||
var title: String {
|
||||
guard let t = currentTrack else { return "" }
|
||||
return (t.artist ?? "") + " — " + (t.name ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
@objc protocol VoxApplication {
|
||||
@objc optional func playpause()
|
||||
@objc optional func next()
|
||||
|
||||
@ -30,8 +30,8 @@ class NightShiftBarItem: CustomButtonTouchBarItem {
|
||||
super.init(identifier: identifier, title: "")
|
||||
isBordered = false
|
||||
setWidth(value: 28)
|
||||
|
||||
tapClosure = { [weak self] in self?.nightShiftAction() }
|
||||
|
||||
actions.append(ItemAction(trigger: .singleTap) { [weak self] in self?.nightShiftAction() })
|
||||
|
||||
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(refresh), userInfo: nil, repeats: true)
|
||||
|
||||
|
||||
@ -23,8 +23,9 @@ class PomodoroBarItem: CustomButtonTouchBarItem, Widget {
|
||||
|
||||
return (
|
||||
item: .pomodoro(workTime: workTime ?? 1500.00, restTime: restTime ?? 300),
|
||||
action: .none,
|
||||
longAction: .none,
|
||||
actions: [],
|
||||
legacyAction: .none,
|
||||
legacyLongAction: .none,
|
||||
parameters: [:]
|
||||
)
|
||||
}
|
||||
@ -35,7 +36,7 @@ class PomodoroBarItem: CustomButtonTouchBarItem, Widget {
|
||||
case none
|
||||
}
|
||||
|
||||
private let defaultTitle = "🍅"
|
||||
private let defaultTitle = "🍅 "
|
||||
private let workTime: TimeInterval
|
||||
private let restTime: TimeInterval
|
||||
private var typeTime: TimeTypes = .none
|
||||
@ -50,8 +51,10 @@ class PomodoroBarItem: CustomButtonTouchBarItem, Widget {
|
||||
self.workTime = workTime
|
||||
self.restTime = restTime
|
||||
super.init(identifier: identifier, title: defaultTitle)
|
||||
tapClosure = { [weak self] in self?.startStopWork() }
|
||||
longTapClosure = { [weak self] in self?.startStopRest() }
|
||||
actions.append(contentsOf: [
|
||||
ItemAction(trigger: .singleTap) { [weak self] in self?.startStopWork() },
|
||||
ItemAction(trigger: .longTap) { [weak self] in self?.startStopRest() }
|
||||
])
|
||||
}
|
||||
|
||||
required init?(coder _: NSCoder) {
|
||||
|
||||
256
MTMR/Widgets/UpNextScrubberTouchBarItem.swift
Normal file
256
MTMR/Widgets/UpNextScrubberTouchBarItem.swift
Normal file
@ -0,0 +1,256 @@
|
||||
//
|
||||
// UpNextScrubberTouchBarItems.swift
|
||||
// MTMR
|
||||
//
|
||||
// Created by Connor Meehan on 13/7/20.
|
||||
// Copyright © 2020 Anton Palgunov. All rights reserved.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import EventKit
|
||||
|
||||
class UpNextScrubberTouchBarItem: NSCustomTouchBarItem {
|
||||
// Dependencies
|
||||
private let scrollView = NSScrollView()
|
||||
private let activity: NSBackgroundActivityScheduler // Update scheduler
|
||||
private var eventSources : [IUpNextSource] = []
|
||||
private var items: [UpNextItem] = []
|
||||
|
||||
// Settings
|
||||
private var futureSearchCutoff: Double
|
||||
private var pastSearchCutoff: Double
|
||||
private var maxToShow: Int
|
||||
private var widthConstraint: NSLayoutConstraint?
|
||||
private var autoResize: Bool = false
|
||||
|
||||
/// <#Description#>
|
||||
/// - Parameters:
|
||||
/// - identifier: Unique identifier of widget
|
||||
/// - interval: Update view interval in seconds
|
||||
/// - from: Relative to current time, how far back we search for events in hours
|
||||
/// - to: Relative to current time, how far forward we search for events in hours
|
||||
/// - maxToShow: Which event to show (1 is first, 2 is second, and so on)
|
||||
init(identifier: NSTouchBarItem.Identifier, interval: TimeInterval, from: Double, to: Double, maxToShow: Int, autoResize: Bool) {
|
||||
// Initialise member properties
|
||||
activity = NSBackgroundActivityScheduler(identifier: "\(identifier.rawValue).updateCheck")
|
||||
pastSearchCutoff = from * 3600
|
||||
futureSearchCutoff = to * 3600
|
||||
self.maxToShow = maxToShow
|
||||
self.autoResize = autoResize
|
||||
UpNextItem.df.dateFormat = "HH:mm"
|
||||
// Error handling
|
||||
if (maxToShow <= 0) {
|
||||
fatalError("Error on UpNext bar item. maxToShow property must be greater than 0.")
|
||||
}
|
||||
// Init super
|
||||
super.init(identifier: identifier)
|
||||
view = scrollView
|
||||
// Add event sources
|
||||
// Can optionally pass an update view callback to an event source to redraw element
|
||||
self.eventSources.append(UpNextCalenderSource(updateCallback: self.updateView))
|
||||
// Fallback interactivity via interval
|
||||
activity.interval = interval
|
||||
activity.repeats = true
|
||||
activity.qualityOfService = .utility
|
||||
activity.schedule { (completion: NSBackgroundActivityScheduler.CompletionHandler) in
|
||||
self.updateView()
|
||||
completion(NSBackgroundActivityScheduler.Result.finished)
|
||||
}
|
||||
updateView()
|
||||
}
|
||||
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private func updateView() -> Void {
|
||||
items = []
|
||||
var upcomingEvents = self.getUpcomingEvents()
|
||||
upcomingEvents.sort(by: {$0.startDate.compare($1.startDate) == .orderedAscending})
|
||||
var index = 1
|
||||
DispatchQueue.main.async {
|
||||
for event in upcomingEvents {
|
||||
// Create UpNextItem
|
||||
let item = UpNextItem(event: event)
|
||||
item.backgroundColor = self.getBackgroundColor(startDate: event.startDate)
|
||||
// Bind tap event
|
||||
item.actions.append(ItemAction(trigger: .singleTap) { [weak self] in
|
||||
self?.switchToApp(event: event)
|
||||
})
|
||||
// Add to view
|
||||
self.items.append(item)
|
||||
// Check if should display any more
|
||||
if (index == self.maxToShow) {
|
||||
break;
|
||||
}
|
||||
index += 1
|
||||
}
|
||||
self.reloadData()
|
||||
self.updateSize()
|
||||
}
|
||||
}
|
||||
|
||||
private func reloadData() {
|
||||
let stackView = NSStackView(views: items.compactMap { $0.view })
|
||||
stackView.spacing = 5
|
||||
stackView.orientation = .horizontal
|
||||
let visibleRect = self.scrollView.documentVisibleRect
|
||||
self.scrollView.documentView = stackView
|
||||
stackView.scroll(visibleRect.origin)
|
||||
}
|
||||
|
||||
func updateSize() {
|
||||
if self.autoResize {
|
||||
self.widthConstraint?.isActive = false
|
||||
|
||||
let width = self.scrollView.documentView?.fittingSize.width ?? 0
|
||||
self.widthConstraint = self.scrollView.widthAnchor.constraint(equalToConstant: width)
|
||||
self.widthConstraint!.isActive = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func getUpcomingEvents() -> [UpNextEventModel] {
|
||||
var upcomingEvents: [UpNextEventModel] = []
|
||||
|
||||
// Calculate the range we're going to search for events in
|
||||
let dateLowerBounds = Date(timeIntervalSinceNow: self.pastSearchCutoff)
|
||||
let dateUpperBounds = Date(timeIntervalSinceNow: self.futureSearchCutoff)
|
||||
|
||||
// Get all events from all sources
|
||||
for eventSource in self.eventSources {
|
||||
if (eventSource.hasPermission) {
|
||||
let events = eventSource.getUpcomingEvents(dateLowerBounds: dateLowerBounds, dateUpperBounds: dateUpperBounds)
|
||||
upcomingEvents.append(contentsOf: events)
|
||||
}
|
||||
}
|
||||
|
||||
return upcomingEvents
|
||||
}
|
||||
|
||||
public func switchToApp(event: UpNextEventModel) {
|
||||
var bundleIdentifier: String
|
||||
switch(event.sourceType) {
|
||||
case .iCalendar:
|
||||
bundleIdentifier = UpNextCalenderSource.bundleIdentifier
|
||||
}
|
||||
|
||||
NSWorkspace.shared.launchApplication(withBundleIdentifier: bundleIdentifier, options: [.default], additionalEventParamDescriptor: nil, launchIdentifier: nil)
|
||||
|
||||
// NB: if you can't open app which on another space, try to check mark
|
||||
// "When switching to an application, switch to a Space with open windows for the application"
|
||||
// in Mission control settings
|
||||
}
|
||||
|
||||
|
||||
func getBackgroundColor(startDate: Date) -> NSColor {
|
||||
let distance = startDate.timeIntervalSinceReferenceDate/60 - Date().timeIntervalSinceReferenceDate/60 // Get time difference in minutes
|
||||
if (distance < 0 as TimeInterval) { // If it's in the past, draw as blue
|
||||
return NSColor.systemBlue
|
||||
} else if (distance < 30 as TimeInterval) { // Less than 30 minutes, backround is red
|
||||
return NSColor.systemRed
|
||||
}
|
||||
return NSColor.clear
|
||||
}
|
||||
}
|
||||
|
||||
private class UpNextItem : CustomButtonTouchBarItem {
|
||||
static public let df = DateFormatter()
|
||||
|
||||
init(event: UpNextEventModel) {
|
||||
let identifier = UpNextItem.getIdentifier(event: event)
|
||||
let title = UpNextItem.getTitle(event: event)
|
||||
super.init(identifier: NSTouchBarItem.Identifier(rawValue: identifier), title: title)
|
||||
}
|
||||
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
private static func getTitle(event: UpNextEventModel) -> String {
|
||||
var title = ""
|
||||
let startDateString = UpNextItem.df.string(for: event.startDate)
|
||||
switch event.sourceType {
|
||||
case .iCalendar:
|
||||
title = String.init(format: "🗓 %@ - %@ ", event.title, startDateString!)
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
private static func getIdentifier(event: UpNextEventModel) -> String {
|
||||
var identifier : String
|
||||
switch event.sourceType {
|
||||
case .iCalendar:
|
||||
identifier = "com.mtmr.iCalendarEvent"
|
||||
}
|
||||
return identifier + "." + event.title
|
||||
}
|
||||
}
|
||||
|
||||
enum UpNextSourceType {
|
||||
case iCalendar
|
||||
}
|
||||
|
||||
// Model for events to be displayed in dock
|
||||
struct UpNextEventModel {
|
||||
let title: String
|
||||
let startDate: Date
|
||||
let sourceType: UpNextSourceType
|
||||
}
|
||||
|
||||
|
||||
// Interface for any event source
|
||||
protocol IUpNextSource {
|
||||
static var bundleIdentifier: String { get }
|
||||
var hasPermission : Bool { get }
|
||||
var updateCallback : () -> Void { get set }
|
||||
|
||||
init(updateCallback: @escaping () -> Void)
|
||||
func getUpcomingEvents(dateLowerBounds: Date, dateUpperBounds: Date) -> [UpNextEventModel]
|
||||
}
|
||||
|
||||
class UpNextCalenderSource : IUpNextSource {
|
||||
static public let bundleIdentifier: String = "com.apple.iCal"
|
||||
|
||||
public var hasPermission: Bool = false
|
||||
private var eventStore : EKEventStore
|
||||
internal var updateCallback: () -> Void
|
||||
|
||||
required init(updateCallback: @escaping () -> Void = {}) {
|
||||
self.updateCallback = updateCallback
|
||||
eventStore = EKEventStore()
|
||||
NotificationCenter.default.addObserver(forName: .EKEventStoreChanged, object: eventStore, queue: nil, using: handleUpdate)
|
||||
let authStatus = EKEventStore.authorizationStatus(for: .event)
|
||||
if (authStatus != .authorized) {
|
||||
eventStore.requestAccess(to: .event){ granted, error in
|
||||
self.hasPermission = granted;
|
||||
self.handleUpdate()
|
||||
if(!granted) {
|
||||
NSLog("Error: MTMR UpNextBarWidget not given calendar access.")
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.handleUpdate()
|
||||
}
|
||||
|
||||
}
|
||||
public func handleUpdate() {
|
||||
self.handleUpdate(note: Notification(name: Notification.Name("refresh view")))
|
||||
}
|
||||
public func handleUpdate(note: Notification) {
|
||||
self.updateCallback()
|
||||
}
|
||||
|
||||
public func getUpcomingEvents(dateLowerBounds: Date, dateUpperBounds: Date) -> [UpNextEventModel] {
|
||||
var upcomingEvents: [UpNextEventModel] = []
|
||||
let calendars = self.eventStore.calendars(for: .event)
|
||||
let predicate = self.eventStore.predicateForEvents(withStart: dateLowerBounds, end: dateUpperBounds, calendars: calendars)
|
||||
let events = self.eventStore.events(matching: predicate)
|
||||
for event in events {
|
||||
upcomingEvents.append(UpNextEventModel(title: event.title, startDate: event.startDate, sourceType: UpNextSourceType.iCalendar))
|
||||
}
|
||||
return upcomingEvents
|
||||
}
|
||||
}
|
||||
@ -12,10 +12,14 @@ import CoreLocation
|
||||
class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate {
|
||||
private let activity: NSBackgroundActivityScheduler
|
||||
private let unitsStr = "°C"
|
||||
private let iconsSource = ["Ясно": "☀️", "Малооблачно": "🌤", "Облачно с прояснениями": "⛅️", "Пасмурно": "☁️", "Небольшой дождь": "🌦", "Дождь": "🌧", "Ливень": "⛈", "Гроза": "🌩", "Дождь со снегом": "☔️", "Небольшой снег": "❄️", "Снег": "🌨", "Туман": "🌫"]
|
||||
private let iconsSource = [
|
||||
"Ясно": "☀️", "Малооблачно": "🌤", "Облачно с прояснениями": "⛅️", "Пасмурно": "☁️", "Небольшой дождь": "🌦", "Дождь": "🌧", "Ливень": "⛈", "Гроза": "🌩", "Дождь с грозой": "⛈", "Дождь со снегом": "☔️", "Небольшой снег": "❄️", "Снег": "🌨", "Туман": "🌫",
|
||||
"Clear": "☀️", "Mostly clear": "🌤", "Partly cloudy": "⛅️", "Overcast": "☁️", "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")
|
||||
@ -46,8 +50,13 @@ class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate
|
||||
manager.delegate = self
|
||||
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
|
||||
manager.startUpdatingLocation()
|
||||
|
||||
tapClosure = tapClosure ?? defaultTapAction
|
||||
|
||||
if actions.filter({ $0.trigger == .singleTap }).isEmpty {
|
||||
actions.append(ItemAction(
|
||||
trigger: .singleTap,
|
||||
defaultTapAction
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder _: NSCoder) {
|
||||
@ -58,7 +67,8 @@ class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate
|
||||
var urlRequest = URLRequest(url: URL(string: getWeatherUrl())!)
|
||||
urlRequest.addValue("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36", forHTTPHeaderField: "user-agent") // important for the right format
|
||||
|
||||
let task = URLSession.shared.dataTask(with: urlRequest) { data, _, error in
|
||||
updateWeatherTask?.cancel()
|
||||
updateWeatherTask = URLSession.shared.dataTask(with: urlRequest) { data, _, error in
|
||||
guard error == nil, let response = data?.utf8string else {
|
||||
return
|
||||
}
|
||||
@ -83,12 +93,12 @@ class YandexWeatherBarItem: CustomButtonTouchBarItem, CLLocationManagerDelegate
|
||||
}
|
||||
}
|
||||
|
||||
task.resume()
|
||||
updateWeatherTask?.resume()
|
||||
}
|
||||
|
||||
func getWeatherUrl() -> String {
|
||||
if location != nil {
|
||||
return "https://yandex.ru/pogoda/?lat=\(location.coordinate.latitude)&lon=\(location.coordinate.longitude)?lang=ru"
|
||||
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
|
||||
}
|
||||
|
||||
@ -28,6 +28,26 @@
|
||||
}
|
||||
},
|
||||
|
||||
// Music
|
||||
{
|
||||
"type": "appleScriptTitledButton",
|
||||
"source": {
|
||||
"inline": "if application \"Music\" is running then\rtell application \"Music\"\rif player state is playing then\rreturn (get artist of current track) & \" – \" & (get name of current track)\relse\rreturn \"\"\rend if\rend tell\rend if\rreturn \"\"\r"
|
||||
},
|
||||
"action": "appleScript",
|
||||
"actionAppleScript": {
|
||||
"inline": "if application \"Music\" is running then\rtell application \"Music\"\rif player state is playing then\rnext track\rend if\rend tell\rend if\r"
|
||||
},
|
||||
"longAction": "appleScript",
|
||||
"longActionAppleScript": {
|
||||
"inline": "if application \"Music\" is running then\rtell application \"Music\"\rif player state is playing then\rprev track\rend if\rend tell\rend if\r"
|
||||
},
|
||||
"refreshInterval": 2,
|
||||
"image": {
|
||||
"base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAADAUExURUdwTOVVZCzB+3qc0gkDBgEBAgcKEwAAAA4EBP5aVU2V95iJv7V3rtBOvH5W6jaOyclScKZGX3wuQCMuUqZN7+NQYXtDvFd9/sxYni2z6UhBhyhvnIp7sf38/PXz9ePm69/k6fHv8/j29+3q7/v6+ufq7uvu8fTw8+1egOFki/dbboVj/HNy/T+j/dNtnEul81vC8Vmf3OeRqOBVofK4xZfF7sDb7PLe6LxU1KKK79PL6vrW3fh4g5Zi4bi16daa0A3Qc90AAAAddFJOUwD3/v0uOlYNG/z+/v7998OYYztt/Le2/eDqi5jEo2rNTwAABMtJREFUWMPdl1tjqjoQhauC0Hqvta26JVxUQBQFRPBS/f//as8kISBqd/twXs5KjBDyrZkEFfP09D+WoigqFxz+mlbVXncweEZVBoNuD1x+E1vtMVZoURn01J/mob52KRNuj4k5mZjJcRueFotFpfv6EwdVRfy0tSdXSranheN0/zkRSB7x42Q6udExdBy3972Fog4o/kBg4X6bhPJaeX7eTqeT6b0MJtPpdOu6n49XQgX+dOQ8vpWEvYn/2AHiL052PpoROcsOtND17ztQ3rwTuCQz9O/moKiMf6BkG/puKBzurKQ64PmbU2bDzUxk3Uql4lZcl3Vpvt9VbxLoLZwjY7E1WcNZoB0XpbELie/3Sg6KVHG2jGPs1LTCE2UXFfgIgtBgyq8d/E/pehJq1zmZGc7kAPsMX4Ec932T25uX5vUklFcHJlDU1OT4wllkvOtn9lrSbF7dCUggNEtaMOXhQZq4WkpBcksJQBCOnyjvM4P8KqQgFW9BFJka2NMKB1gw+VMxvN9smnwI1EuzpxS+g9FWYySjsTpOtnq+H162iW01m/wyXLUPzT/5HKQoSjQmU8vE6TAElvWggbhuNRpScQa002bVtJmBCz9qNusWBkJmoyHmoHajC4yybVujhR26mJVha7lDo2FrhnA4N0aq+BpE24zjgsMoEsfU0AADaKCwemiIRZA+o6N9oygyMi/EAWk0DMNgFvCmN/5IwqCV3PCGzzIwbINrzgwykVq2iorUalm2UTZotXKWqVYz5uBjzDUoxrxWyzKQWy061LZsNIJ3PAMDIcbVauwdGmxrNblgYNnCgStN54ylBSsYoAxerwwgJsCWTS0sepym0Mdp1gYBw5lmwgDXIDEoaeHLYE36BafzuQWFQ9RASM/XQPpMD5YQ2gA/AwPArQJyZWDsgo64C+/pBRkDG4s31hdmwFNAGz1mBjPukBs8qdSgLDBA1LJm1lw/14IgWAdAQ5nhax4HY/FR7qfpHQMPUChzS0c6eFmv17MZo7HZBP3MQJHTNCnzxPMgOFUteAGtMwMmfRPIijBgc+AmNK6+9zw+9Iw05YsG8aaT/7Kro7eUcoSWw3n/1W57SxgGOZCYxl+VDDabcf6LpNTf3g6IQ4XY7TbiyyUBmhqsViswWK02cE7ITIdmtxnWlcJz6f1tPyMsha+2R4UGyJPdywotwGSHOKHdm+IMYA40BRhtkXOb42DAh8crppjw8CyB4nMBlvFtD/0WSfZebkDHguLNar2JdyTXptqRrx6OmMKZkISQc4Yv9yyDXHiiEx1qXL1OAFdhBJPAAQeRQDEiRZEm+kwnu2p1XHo64yQ8j47bL1kCZ87pDKWxuW4mQJ9O9ba31xE5Y/rnA4VoTCJQwvnyBNgk+pkDi8sSJjlKRPxhX7r3Lytz0LPMi1H1Qv7VuzwuAzh4h1ukKFi/YV9+9E8THZbne2Ezxd/xsNGQ6u+wgoeH4SH9Tl367t+2Ko/acA8Oj/DhWP7X/30Zkvj4WMYlj10MOISXf7DlkPvvH6g43u0oCzDS1U5f/sHWC3d7cn3UAQf4HeHfwxXQY4yu/HTDKNXro3Gngw4vw2FnPKrXJfUXu0fqIdeFZOnXm08FTRSxcf391pW7oNGT8vRf6i9jqljwYzAm6AAAAABJRU5ErkJggg=="
|
||||
}
|
||||
},
|
||||
|
||||
// iTunes
|
||||
{
|
||||
"type": "appleScriptTitledButton",
|
||||
|
||||
@ -6,7 +6,7 @@ class AppleScriptDefinitionTests: XCTestCase {
|
||||
[ { "type": "appleScriptTitledButton", "source": { "inline": "tell everything fine" } } ]
|
||||
""".data(using: .utf8)!
|
||||
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonNoActionFixture)
|
||||
guard case .appleScriptTitledButton(let source, _)? = result?.first?.type else {
|
||||
guard case .appleScriptTitledButton(let source, _, _)? = result?.first?.type else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
@ -18,7 +18,7 @@ class AppleScriptDefinitionTests: XCTestCase {
|
||||
[ { "type": "appleScriptTitledButton", "source": { "filePath": "/ololo/pew" } } ]
|
||||
""".data(using: .utf8)!
|
||||
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonNoActionFixture)
|
||||
guard case .appleScriptTitledButton(let source, _)? = result?.first?.type else {
|
||||
guard case .appleScriptTitledButton(let source, _, _)? = result?.first?.type else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
@ -32,7 +32,7 @@ class AppleScriptDefinitionTests: XCTestCase {
|
||||
[ { "type": "appleScriptTitledButton", "source": { "filePath": "~/pew" } } ]
|
||||
""".data(using: .utf8)!
|
||||
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonNoActionFixture)
|
||||
guard case .appleScriptTitledButton(let source, _)? = result?.first?.type else {
|
||||
guard case .appleScriptTitledButton(let source, _, _)? = result?.first?.type else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
@ -58,7 +58,7 @@ class AppleScriptDefinitionTests: XCTestCase {
|
||||
[ { "type": "appleScriptTitledButton", "source": { "inline": "tell everything fine" }, "refreshInterval": 305} ]
|
||||
""".data(using: .utf8)!
|
||||
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonNoActionFixture)
|
||||
guard case .appleScriptTitledButton(_, 305)? = result?.first?.type else {
|
||||
guard case .appleScriptTitledButton(_, 305, _)? = result?.first?.type else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ class ParseConfig: XCTestCase {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
guard case .none? = result?.first?.action else {
|
||||
guard result?.first?.actions.count == 0 else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
@ -18,14 +18,29 @@ class ParseConfig: XCTestCase {
|
||||
|
||||
func testButtonKeyCodeAction() {
|
||||
let buttonKeycodeFixture = """
|
||||
[ { "type": "staticButton", "title": "Pew", "action": "hidKey", "keycode": 123} ]
|
||||
[ { "type": "staticButton", "title": "Pew", "actions": [ { "trigger": "singleTap", "action": "hidKey", "keycode": 123 } ] } ]
|
||||
""".data(using: .utf8)!
|
||||
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonKeycodeFixture)
|
||||
guard case .staticButton("Pew")? = result?.first?.type else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
guard case .hidKey(keycode: 123)? = result?.first?.action else {
|
||||
guard case .hidKey(keycode: 123)? = result?.first?.actions.filter({ $0.trigger == .singleTap }).first?.value else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func testButtonKeyCodeLegacyAction() {
|
||||
let buttonKeycodeFixture = """
|
||||
[ { "type": "staticButton", "title": "Pew", "action": "hidKey", "keycode": 123 } ]
|
||||
""".data(using: .utf8)!
|
||||
let result = try? JSONDecoder().decode([BarItemDefinition].self, from: buttonKeycodeFixture)
|
||||
guard case .staticButton("Pew")? = result?.first?.type else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
guard case .hidKey(keycode: 123)? = result?.first?.legacyAction else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
@ -40,7 +55,7 @@ class ParseConfig: XCTestCase {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
guard case .keyPress(keycode: 53)? = result?.first?.action else {
|
||||
guard case .keyPress(keycode: 53)? = result?.first?.actions.filter({ $0.trigger == .singleTap }).first?.value else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
@ -55,7 +70,7 @@ class ParseConfig: XCTestCase {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
guard case .keyPress(keycode: 53)? = result?.first?.action else {
|
||||
guard case .keyPress(keycode: 53)? = result?.first?.actions.filter({ $0.trigger == .singleTap }).first?.value else {
|
||||
XCTFail()
|
||||
return
|
||||
}
|
||||
|
||||
183
README.md
183
README.md
@ -18,7 +18,7 @@ My idea is to create a platform for creating plugins to customize the TouchBar.
|
||||
<a href="https://t.me/joinchat/AmVYGg8vW38c13_3MxdE_g"><img height="20px" src="https://telegram.org/img/t_logo.png" /> Telegram</a>
|
||||
</p>
|
||||
|
||||
<p align="center"><a href="https://www.paypal.me/toxblh/10" title="Donate via Paypal"><img height="36px" src="Resources/support_paypal.svg" alt="PayPal donate button" /></a>
|
||||
<p align="center"><a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=WUAAG2HH58WE4" title="Donate via Paypal"><img height="36px" src="Resources/support_paypal.svg" alt="PayPal donate button" /></a>
|
||||
<a href="https://www.buymeacoffee.com/toxblh" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" height="36px" ></a>
|
||||
<a href="https://www.patreon.com/bePatron?u=9900748"><img height="36px" src="https://c5.patreon.com/external/logo/become_a_patron_button.png" srcset="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png 2x"></a>
|
||||
<a href="https://www.producthunt.com/posts/my-touchbar-my-rules-mtmr">
|
||||
@ -84,6 +84,7 @@ The pre-installed configuration contains less or more than you'll probably want,
|
||||
- darkMode
|
||||
- pomodoro
|
||||
- network
|
||||
- upnext (Calendar events)
|
||||
|
||||
> Media Keys
|
||||
|
||||
@ -102,11 +103,31 @@ The pre-installed configuration contains less or more than you'll probably want,
|
||||
- appleScriptTitledButton
|
||||
- shellScriptTitledButton
|
||||
|
||||
## Gestures on central part:
|
||||
## Gestures
|
||||
|
||||
By default you can enable basic gestures from application menu (status bar -> MTMR icon -> Volume/Brightness gestures):
|
||||
- two finger slide: change you Volume
|
||||
- three finger slide: change you Brightness
|
||||
|
||||
### Custom gestures
|
||||
|
||||
You can add custom actions for two/three/four finger swipes. To do it, you need to use `swipe` type:
|
||||
|
||||
```json
|
||||
"type": "swipe",
|
||||
"fingers": 2, // number of fingers required (2,3 or 4)
|
||||
"direction": "right", // direction of swipe (right/left)
|
||||
"minOffset": 10, // optional: minimal required offset for gesture to emit event
|
||||
"sourceApple": { // optional: apple script to run
|
||||
"inline": "beep"
|
||||
},
|
||||
"sourceBash": { // optional: bash script to run
|
||||
"inline": "touch /Users/lobster/test"
|
||||
}
|
||||
```
|
||||
|
||||
You may create as many `swipe` objects in the preset as you want.
|
||||
|
||||
## Built-in slider types:
|
||||
|
||||
- brightness
|
||||
@ -133,10 +154,37 @@ The pre-installed configuration contains less or more than you'll probably want,
|
||||
"inline": "tell application \"Finder\"\rif not (exists window 1) then\rmake new Finder window\rset target of front window to path to home folder as string\rend if\ractivate\rend tell",
|
||||
// or
|
||||
"base64": "StringInbase64"
|
||||
}
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
> 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
|
||||
{
|
||||
"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.
|
||||
@ -150,10 +198,15 @@ Example of "CPU load" button which also changes color based on load value.
|
||||
"source": {
|
||||
"inline": "top -l 2 -n 0 -F | egrep -o ' \\d*\\.\\d+% idle' | tail -1 | awk -F% '{p = 100 - $1; if (p > 30) c = \"\\033[33m\"; if (p > 70) c = \"\\033[30;43m\"; printf \"%s%4.1f%%\\n\", c, p}'"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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.
|
||||
@ -296,8 +349,46 @@ To close a group, use the button:
|
||||
},
|
||||
```
|
||||
|
||||
#### `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:
|
||||
|
||||
### Example:
|
||||
|
||||
```js
|
||||
"actions": [
|
||||
{
|
||||
"trigger": "singleTap",
|
||||
"action": "hidKey",
|
||||
"keycode": 53
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Triggers:
|
||||
|
||||
- `singleTap`
|
||||
- `doubleTap`
|
||||
- `tripleTap`
|
||||
- `longTap`
|
||||
|
||||
### Types
|
||||
|
||||
- `hidKey`
|
||||
> https://github.com/aosm/IOHIDFamily/blob/master/IOHIDSystem/IOKit/hidsystem/ev_keymap.h use only numbers
|
||||
|
||||
@ -339,22 +430,6 @@ To close a group, use the button:
|
||||
"url": "https://google.com",
|
||||
```
|
||||
|
||||
## LongActions
|
||||
|
||||
If you want to longPress for some operations, it is similar to the configuration for Actions but with additional parameters, for example:
|
||||
|
||||
```js
|
||||
"longAction": "hidKey",
|
||||
"longKeycode": 53,
|
||||
```
|
||||
|
||||
- longAction
|
||||
- longKeycode
|
||||
- longActionAppleScript
|
||||
- longExecutablePath
|
||||
- longShellArguments
|
||||
- longUrl
|
||||
|
||||
## Additional parameters:
|
||||
|
||||
- `width` restrict how much room a particular button will take
|
||||
@ -375,39 +450,45 @@ If you want to longPress for some operations, it is similar to the configuration
|
||||
"bordered": "false" // "true" or "false"
|
||||
```
|
||||
|
||||
### Roadmap
|
||||
- `background` allow to specify you button background color
|
||||
|
||||
- [x] Create the first prototype with TouchBar in Storyboard
|
||||
- [x] Put in stripe menu on startup the application
|
||||
- [x] Find how to simulate real buttons like brightness, volume, night shift and etc.
|
||||
- [x] Time in touchbar!
|
||||
- [x] First the weather plugin
|
||||
- [x] Find how to open full-screen TouchBar without the cross and stripe menu
|
||||
- [x] Find how to add haptic feedback
|
||||
- [x] Add icon and menu in StatusBar
|
||||
- [x] Hide from Dock
|
||||
- [x] Status menu: "preferences", "quit"
|
||||
- [x] JSON or another approch for save preset, maybe in `~/Library/Application Support/MTMR/`
|
||||
- [x] Custom buttons size, actions by click
|
||||
- [x] Layout: [always left, NSSliderView for center, always right]
|
||||
- [x] System for autoupdate (https://sparkle-project.org/)
|
||||
- [ ] Overwrite default values from item types (e.g. title for brightness)
|
||||
- [ ] Custom settings for paddings and margins for buttons
|
||||
- [ ] XPC Service for scripts
|
||||
- [ ] UI for settings
|
||||
- [ ] Import config from BTT
|
||||
```js
|
||||
"background": "#FF0000",
|
||||
```
|
||||
by using background with color "#000000" and bordered == false you can create button without gray background but with background when the button is pressed
|
||||
|
||||
Settings:
|
||||
- `title` specify button title
|
||||
|
||||
- [ ] Interface for plugins and export like presets
|
||||
- [x] Startup at login
|
||||
- [ ] Show on/off in Dock
|
||||
- [ ] Show on/off in StatusBar
|
||||
- [ ] On/off Haptic Feedback
|
||||
```js
|
||||
"title": "hello"
|
||||
```
|
||||
|
||||
Maybe:
|
||||
- `image` specify button icon
|
||||
|
||||
- [ ] Refactoring the application into packages (AppleScript, JavaScript? and Swift?)
|
||||
```js
|
||||
"image": {
|
||||
//Can be either of those
|
||||
"base64": "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAABGdB...."
|
||||
//or
|
||||
"filePath": "~/img.png"
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
#### If you can't open preferences:
|
||||
- Opening another program which can't edit text
|
||||
1. Open Terminal.app
|
||||
2. Put `open -a TextEdit ~/Library/Application\ Support/MTMR/items.json` command and press <kbd>Enter</kbd>
|
||||
|
||||
|
||||
#### Buttons or gestures doesn't work:
|
||||
- "After the last update my mtmr is not working anymore!"
|
||||
- "Buttons sometimes do not trigger action"
|
||||
- "ESC don't work"
|
||||
- "Gestures don't work"
|
||||
|
||||
Re-tick or check a tick for access 🍏→ System Preferences → Security and Privacy → tab Privacy → Accessibility → MTMR
|
||||
|
||||
## Credits
|
||||
|
||||
|
||||
@ -4,3 +4,38 @@
|
||||
* try move away from enums when parse preset – enums are hard to extend
|
||||
* find better way to hide bar items
|
||||
* 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?)
|
||||
|
||||
12
build.sh
12
build.sh
@ -1,16 +1,18 @@
|
||||
# INSTALL xcpretty: sudo gem install xcpretty
|
||||
|
||||
NAME='MTMR'
|
||||
|
||||
rm -r Release 2>/dev/null
|
||||
|
||||
xcodebuild archive \
|
||||
-scheme "$NAME" \
|
||||
-archivePath Release/App.xcarchive
|
||||
-archivePath Release/App.xcarchive | xcpretty -c
|
||||
|
||||
xcodebuild \
|
||||
-exportArchive \
|
||||
-archivePath Release/App.xcarchive \
|
||||
-exportOptionsPlist export-options.plist \
|
||||
-exportPath Release
|
||||
-exportPath Release | xcpretty -c
|
||||
|
||||
cd Release
|
||||
rm -r App.xcarchive
|
||||
@ -20,7 +22,7 @@ NAME_DMG="${NAME}.app"
|
||||
echo $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`
|
||||
VERSION=`/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ${NAME}.app/Contents/Info.plist`
|
||||
MINIMUM=`/usr/libexec/PlistBuddy -c "Print LSMinimumSystemVersion" ${NAME}.app/Contents/Info.plist`
|
||||
@ -61,8 +63,8 @@ echo "<?xml version=\"1.0\" standalone=\"yes\"?>
|
||||
echo ""
|
||||
echo "Homebrew https://github.com/Homebrew/homebrew-cask/edit/master/Casks/mtmr.rb"
|
||||
echo ""
|
||||
echo " version '${VERSION}'"
|
||||
echo " sha256 '${SHA256}'"
|
||||
echo " version \"${VERSION}\""
|
||||
echo " sha256 \"${SHA256}\""
|
||||
echo ""
|
||||
echo "Update MTMR v${VERSION}"
|
||||
|
||||
|
||||
5
test.sh
5
test.sh
@ -1,3 +1,4 @@
|
||||
# INSTALL xcpretty: sudo gem install xcpretty
|
||||
|
||||
NAME='MTMR'
|
||||
killall $NAME
|
||||
@ -11,13 +12,13 @@ rm -r Release 2>/dev/null
|
||||
|
||||
xcodebuild archive \
|
||||
-scheme "$NAME" \
|
||||
-archivePath Release/App.xcarchive
|
||||
-archivePath Release/App.xcarchive | xcpretty
|
||||
|
||||
xcodebuild \
|
||||
-exportArchive \
|
||||
-archivePath Release/App.xcarchive \
|
||||
-exportOptionsPlist export-options.plist \
|
||||
-exportPath Release
|
||||
-exportPath Release | xcpretty
|
||||
|
||||
cd Release
|
||||
rm -r App.xcarchive
|
||||
|
||||
Loading…
Reference in New Issue
Block a user