Skip to content

Commit

Permalink
Bell notifications (blinksh#1175)
Browse files Browse the repository at this point in the history
· Play an audible bell sound & haptic feedback when a BEL character is received
· If a BEL character is received and the terminal that originated it is not in focus or you've changed apps it results in a system notification
· Dedicated Setting for notifications
· Support for standard OSC sequence & iTerm2 notifications
  • Loading branch information
Javier de Martín authored Dec 14, 2020
1 parent 36076ab commit 0b60c9b
Show file tree
Hide file tree
Showing 15 changed files with 464 additions and 72 deletions.
43 changes: 31 additions & 12 deletions Blink.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
22A9494C2067FD5E003A0666 /* tar.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 22A9494A2067FD5E003A0666 /* tar.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
22E1F3352036EE87001FCC5C /* openssl.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 22E1F3332036EE86001FCC5C /* openssl.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
22E1F3362036EE87001FCC5C /* libssh2.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 22E1F3342036EE86001FCC5C /* libssh2.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
803B99D72582869200DC99C8 /* BKNotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 803B99D62582869200DC99C8 /* BKNotificationsView.swift */; };
803B99E3258381B200DC99C8 /* SettingsHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 803B99E2258381B200DC99C8 /* SettingsHostingController.swift */; };
85A34307200A837A009324F1 /* webfontloader.js in Resources */ = {isa = PBXBuildFile; fileRef = 85A34303200A837A009324F1 /* webfontloader.js */; };
85A34309200A8FAF009324F1 /* term.css in Resources */ = {isa = PBXBuildFile; fileRef = 85A34308200A8FAF009324F1 /* term.css */; };
B700AED81DD0F2C200100EBF /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B700AED71DD0F2C200100EBF /* CloudKit.framework */; };
Expand Down Expand Up @@ -226,7 +228,6 @@
D2C24423238E44AB0082C69C /* KBWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C2440E238E44AB0082C69C /* KBWebView.swift */; };
D2C24424238E44AB0082C69C /* KBWebViewBase.m in Sources */ = {isa = PBXBuildFile; fileRef = D2C2440F238E44AB0082C69C /* KBWebViewBase.m */; };
D2C24425238E44AB0082C69C /* KeyModifierPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C24410238E44AB0082C69C /* KeyModifierPicker.swift */; };
D2C2442F238E62A70082C69C /* KBSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C2442E238E62A70082C69C /* KBSettingsViewController.swift */; };
D2C244332390FB830082C69C /* KeyBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C244322390FB830082C69C /* KeyBinding.swift */; };
D2C244352390FEEF0082C69C /* KeyBindingAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C244342390FEEF0082C69C /* KeyBindingAction.swift */; };
D2C24437239104250082C69C /* ShortcutsConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2C24436239104250082C69C /* ShortcutsConfigView.swift */; };
Expand Down Expand Up @@ -300,6 +301,13 @@
remoteGlobalIDString = 148993CF1D8F2515000132F7;
remoteInfo = TestHost;
};
803B99D32582865500DC99C8 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 0732F1101D062C9F00AB5438 /* UICKeyChainStore.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 80FC965B252C6209006CF96B;
remoteInfo = BlinkShellIntents;
};
D24AFD5A222410E700CFD3C1 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 0732F1051D062BF700AB5438 /* MBProgressHUD.xcodeproj */;
Expand Down Expand Up @@ -401,6 +409,8 @@
22A9494A2067FD5E003A0666 /* tar.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = tar.framework; sourceTree = "<group>"; };
22E1F3332036EE86001FCC5C /* openssl.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = openssl.framework; sourceTree = "<group>"; };
22E1F3342036EE86001FCC5C /* libssh2.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = libssh2.framework; sourceTree = "<group>"; };
803B99D62582869200DC99C8 /* BKNotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BKNotificationsView.swift; sourceTree = "<group>"; };
803B99E2258381B200DC99C8 /* SettingsHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsHostingController.swift; sourceTree = "<group>"; };
85A34303200A837A009324F1 /* webfontloader.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = webfontloader.js; sourceTree = "<group>"; };
85A34308200A8FAF009324F1 /* term.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = term.css; sourceTree = "<group>"; };
B700AED71DD0F2C200100EBF /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
Expand Down Expand Up @@ -652,7 +662,6 @@
D2C2440E238E44AB0082C69C /* KBWebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KBWebView.swift; sourceTree = "<group>"; };
D2C2440F238E44AB0082C69C /* KBWebViewBase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = KBWebViewBase.m; sourceTree = "<group>"; };
D2C24410238E44AB0082C69C /* KeyModifierPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyModifierPicker.swift; sourceTree = "<group>"; };
D2C2442E238E62A70082C69C /* KBSettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KBSettingsViewController.swift; sourceTree = "<group>"; };
D2C244322390FB830082C69C /* KeyBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBinding.swift; sourceTree = "<group>"; };
D2C244342390FEEF0082C69C /* KeyBindingAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyBindingAction.swift; sourceTree = "<group>"; };
D2C24436239104250082C69C /* ShortcutsConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsConfigView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -862,6 +871,7 @@
0732F1191D062CA000AB5438 /* libUICKeyChainStore.a */,
0732F11B1D062CA000AB5438 /* UICKeyChainStoreTests.xctest */,
0747D63E2018182D00CF8F0C /* TestHost.app */,
803B99D42582865500DC99C8 /* BlinkShellIntents.appex */,
);
name = Products;
sourceTree = "<group>";
Expand Down Expand Up @@ -909,6 +919,14 @@
path = Sessions;
sourceTree = SOURCE_ROOT;
};
803B99CB2582865500DC99C8 /* Notifications */ = {
isa = PBXGroup;
children = (
803B99D62582869200DC99C8 /* BKNotificationsView.swift */,
);
path = Notifications;
sourceTree = "<group>";
};
B75112C71DE4A7680040C693 /* UserConfiguration */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1004,6 +1022,7 @@
C9B2E0061D6B612300B89F69 /* Model */,
C9B2E0111D6B612300B89F69 /* Network */,
C9B2E0141D6B612300B89F69 /* ViewControllers */,
803B99E2258381B200DC99C8 /* SettingsHostingController.swift */,
);
path = Settings;
sourceTree = "<group>";
Expand Down Expand Up @@ -1043,6 +1062,7 @@
C9B2E0141D6B612300B89F69 /* ViewControllers */ = {
isa = PBXGroup;
children = (
803B99CB2582865500DC99C8 /* Notifications */,
B7A4876A1DC7C6F4007BA809 /* Settings */,
B7A4875F1DC7BD29007BA809 /* BKDefaultUser */,
07E3AEBD1D918FBA007BC086 /* About */,
Expand Down Expand Up @@ -1258,7 +1278,6 @@
D2C243F7238E44960082C69C /* KB */ = {
isa = PBXGroup;
children = (
D2C2442A238E62690082C69C /* Controllers */,
D2C24400238E44AB0082C69C /* Views */,
D2C243F9238E44AB0082C69C /* Model */,
);
Expand Down Expand Up @@ -1315,14 +1334,6 @@
path = General;
sourceTree = "<group>";
};
D2C2442A238E62690082C69C /* Controllers */ = {
isa = PBXGroup;
children = (
D2C2442E238E62A70082C69C /* KBSettingsViewController.swift */,
);
path = Controllers;
sourceTree = "<group>";
};
D2E4F92A20B2BB4500B30F7B /* Products */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1551,6 +1562,13 @@
remoteRef = 0747D63D2018182D00CF8F0C /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
803B99D42582865500DC99C8 /* BlinkShellIntents.appex */ = {
isa = PBXReferenceProxy;
fileType = "wrapper.app-extension";
path = BlinkShellIntents.appex;
remoteRef = 803B99D32582865500DC99C8 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
D2A3F4DD206909A8006BB305 /* MBProgressHUD.framework */ = {
isa = PBXReferenceProxy;
fileType = wrapper.framework;
Expand Down Expand Up @@ -1646,7 +1664,9 @@
D2D75EE021AFDA10007336B6 /* LayoutManager.m in Sources */,
B7A4876D1DC7C709007BA809 /* BKSettingsViewController.m in Sources */,
D241CBD923040734003D64A5 /* KBKeyView.swift in Sources */,
803B99D72582869200DC99C8 /* BKNotificationsView.swift in Sources */,
C94E9B631D6BA21C00DA4DD6 /* DismissSegue.m in Sources */,
803B99E3258381B200DC99C8 /* SettingsHostingController.swift in Sources */,
D2C2441C238E44AB0082C69C /* NavButton.swift in Sources */,
0716B5721CFFAB9300268B5B /* AppDelegate.m in Sources */,
07F670731D05EEE200C0A53C /* MoshSession.m in Sources */,
Expand Down Expand Up @@ -1803,7 +1823,6 @@
079635871D0E6602000473B1 /* TermView.m in Sources */,
D241CBE6230562E9003D64A5 /* KBSizes.swift in Sources */,
D241CBD023040734003D64A5 /* KBKeyViewFlexible.swift in Sources */,
D2C2442F238E62A70082C69C /* KBSettingsViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
1 change: 1 addition & 0 deletions Blink/SpaceController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,7 @@ extension SpaceController: UIPageViewControllerDataSource {
}

extension SpaceController: TermControlDelegate {

func terminalHangup(control: TermController) {
if currentTerm() == control {
_closeCurrentSpace()
Expand Down
45 changes: 44 additions & 1 deletion Blink/TermController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

import Combine
import UserNotifications
import AVFoundation

@objc protocol TermControlDelegate: NSObjectProtocol {
// May be do it optional
Expand Down Expand Up @@ -280,9 +281,52 @@ let _apiRoutes:[String: (MCPSession, String) -> AnyPublisher<String, Never>] = [
"completion.for": Complete.forAPI
]

// MARK: - TermDeviceDelegate methods

extension TermController: TermDeviceDelegate {

/**
When a `ring-bell` notification has been received on `TermView` react to it by sounding a bell if the terminal that sent it
is in focus and if it's not send a notification. Tapping the notification opens the session that sent it.

Only reproduce haptic feedback on iPhones and if it's enabled.

Enable/Disable standard OSC sequences & iTerm2 notifications
*/
func viewDidReceiveBellRing() {
if !_termView.isFocused() && BKDefaults.isNotificationOnBellUnfocusedOn() {
viewNotify(["title": "🔔 \(_termView.title ?? "")"])
} else if BKDefaults.isPlaySoundOnBellOn() && _termView.isFocused() {
AudioServicesPlaySystemSound(1103);
}

// Haptic feedback is only visible from iPhones
if UIDevice.current.userInterfaceIdiom == .phone && !BKDefaults.hapticFeedbackOnBellOff() {
UINotificationFeedbackGenerator().notificationOccurred(.warning)
}
}

/**
Presents a UserNotification with the `title` & `body` values passed on `data`. Tapping on the notification opens the terminal that originated the notification. Also triggered when the terminal receives a standard `OSC` sequence & iTerm2-like notification.

- Parameters:
- data: Set the `title` and `body` String values to display those values in the notification banner.
*/
func viewNotify(_ data: [AnyHashable : Any]!) {

/// Don't show anything if OSC notifications have been deactivated
guard BKDefaults.isOscNotificationsOn() else {
return
}

/**
- Show notification if terminal is in focus
- Don't show notification if terminal is not in focus and user has deactivated background notifications
*/
guard _termView.isFocused() || BKDefaults.isNotificationOnBellUnfocusedOn() else {
return
}

let content = UNMutableNotificationContent()
content.title = (data["title"] as? String) ?? title ?? "Blink"
content.body = (data["body"] as? String) ?? ""
Expand All @@ -298,7 +342,6 @@ extension TermController: TermDeviceDelegate {
center.add(req, withCompletionHandler: nil)
}
}

}

func apiCall(_ api: String!, andRequest request: String!) {
Expand Down
1 change: 1 addition & 0 deletions Blink/TermDevice.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
- (void)deviceFocused;
- (void)apiCall:(NSString *)api andRequest:(NSString *)request;
- (void)viewNotify:(NSDictionary *)data;
- (void)viewDidReceiveBellRing;
- (UIViewController *)viewController;

@end
Expand Down
4 changes: 4 additions & 0 deletions Blink/TermDevice.m
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,10 @@ - (void)viewNotify:(NSDictionary *)data {
[_delegate viewNotify:data];
}

- (void)viewDidReceiveBellRing {
[_delegate viewDidReceiveBellRing];
}

- (void)viewAPICall:(NSString *)api andJSONRequest:(NSString *)request {
[_delegate apiCall:api andRequest:request];
}
Expand Down
1 change: 1 addition & 0 deletions Blink/TermView.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
- (void)viewAPICall:(NSString *)api andJSONRequest:(NSString *)request;
- (void)viewNotify:(NSDictionary *)data;
- (void)viewSelectionChanged;
- (void)viewDidReceiveBellRing;

@end

Expand Down
6 changes: 4 additions & 2 deletions Blink/TermView.m
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
#import "BKFont.h"
#import "BKTheme.h"
#import "TermJS.h"
#import <AVFoundation/AVFoundation.h>

#import "Blink-Swift.h"

Expand Down Expand Up @@ -352,6 +353,9 @@ - (void)userContentController:(WKUserContentController *)userContentController
[_device viewAPICall:data[@"name"] andJSONRequest:data[@"request"]];
} else if ([operation isEqualToString:@"notify"]) {
[_device viewNotify:data];
} else if ([operation isEqualToString:@"ring-bell"]) {
[_device viewDidReceiveBellRing];

}
}

Expand Down Expand Up @@ -385,8 +389,6 @@ - (void)_onTerminalReady:(NSDictionary *)data
[_coverView removeFromSuperview];
_coverView = nil;
}];


}

- (BOOL)isFocused {
Expand Down
9 changes: 9 additions & 0 deletions Resources/term.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ hterm.notify = function(params) {
_postMessage('notify', {title, body: params.body})
}

hterm.Terminal.prototype.ringBell = function() {
// Blink cursor on BEL character
this.cursorNode_.style.backgroundColor = this.scrollPort_.getForegroundColor();

setTimeout(() => this.restyleCursor_(), 200);

_postMessage('ring-bell', null);
};

hterm.Terminal.prototype.copyStringToClipboard = function(content) {
if (this.prefs_.get('enable-clipboard-notice')) {
setTimeout(this.showOverlay.bind(this, hterm.notifyCopyMessage, 500), 200);
Expand Down
Loading

0 comments on commit 0b60c9b

Please sign in to comment.