ish/app/TerminalView.m
2019-09-22 18:52:24 -07:00

309 lines
12 KiB
Objective-C

//
// TerminalView.m
// iSH
//
// Created by Theodore Dubois on 11/3/17.
//
#import "ScrollbarView.h"
#import "TerminalView.h"
#import "UserPreferences.h"
#import "UIApplication+OpenURL.h"
@interface TerminalView ()
@property (nonatomic) NSMutableArray<UIKeyCommand *> *keyCommands;
@property ScrollbarView *scrollbarView;
@end
@implementation TerminalView
- (void)awakeFromNib {
[super awakeFromNib];
self.inputAssistantItem.leadingBarButtonGroups = @[];
self.inputAssistantItem.trailingBarButtonGroups = @[];
ScrollbarView *scrollbarView = self.scrollbarView = [[ScrollbarView alloc] initWithFrame:self.bounds];
self.scrollbarView = scrollbarView;
scrollbarView.delegate = self;
scrollbarView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
scrollbarView.bounces = NO;
[self addSubview:scrollbarView];
[UserPreferences.shared addObserver:self forKeyPath:@"capsLockMapping" options:0 context:nil];
[UserPreferences.shared addObserver:self forKeyPath:@"optionMapping" options:0 context:nil];
[UserPreferences.shared addObserver:self forKeyPath:@"backtickMapEscape" options:0 context:nil];
}
- (void)dealloc {
[UserPreferences.shared removeObserver:self forKeyPath:@"capsLockMapping"];
[UserPreferences.shared removeObserver:self forKeyPath:@"optionMapping"];
[UserPreferences.shared removeObserver:self forKeyPath:@"backtickMapEscape"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
_keyCommands = nil;
}
- (void)setTerminal:(Terminal *)terminal {
NSArray<NSString *>* handlers = @[@"focus", @"newScrollHeight", @"newScrollTop", @"openLink"];
if (self.terminal) {
// remove old terminal
NSAssert(self.terminal.webView.superview == self.scrollbarView, @"old terminal view was not in our view");
[self.terminal.webView removeFromSuperview];
self.scrollbarView.contentView = nil;
for (NSString *handler in handlers) {
[self.terminal.webView.configuration.userContentController removeScriptMessageHandlerForName:handler];
}
}
_terminal = terminal;
WKWebView *webView = terminal.webView;
webView.scrollView.scrollEnabled = NO;
webView.scrollView.delaysContentTouches = NO;
webView.scrollView.canCancelContentTouches = NO;
webView.scrollView.panGestureRecognizer.enabled = NO;
for (NSString *handler in handlers) {
[webView.configuration.userContentController addScriptMessageHandler:self name:handler];
}
webView.frame = self.bounds;
self.opaque = webView.opaque = NO;
webView.backgroundColor = UIColor.clearColor;
webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.scrollbarView.contentView = webView;
[self.scrollbarView addSubview:webView];
}
#pragma mark Focus and scrolling
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name isEqualToString:@"focus"]) {
if (!self.isFirstResponder) {
[self becomeFirstResponder];
}
} else if ([message.name isEqualToString:@"newScrollHeight"]) {
self.scrollbarView.contentSize = CGSizeMake(0, [message.body doubleValue]);
} else if ([message.name isEqualToString:@"newScrollTop"]) {
CGFloat newOffset = [message.body doubleValue];
if (self.scrollbarView.contentOffset.y == newOffset)
return;
[self.scrollbarView setContentOffset:CGPointMake(0, newOffset) animated:NO];
} else if ([message.name isEqualToString:@"openLink"]) {
[UIApplication openURL:message.body];
}
}
- (BOOL)resignFirstResponder {
[self.terminal.webView evaluateJavaScript:@"exports.blur()" completionHandler:nil];
return [super resignFirstResponder];
}
- (IBAction)loseFocus:(id)sender {
[self resignFirstResponder];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
[self.terminal.webView evaluateJavaScript:[NSString stringWithFormat:@"exports.newScrollTop(%f)", scrollView.contentOffset.y] completionHandler:nil];
}
- (void)setKeyboardAppearance:(UIKeyboardAppearance)keyboardAppearance {
_keyboardAppearance = keyboardAppearance;
if (keyboardAppearance == UIKeyboardAppearanceLight) {
self.scrollbarView.indicatorStyle = UIScrollViewIndicatorStyleBlack;
} else {
self.scrollbarView.indicatorStyle = UIScrollViewIndicatorStyleWhite;
}
}
#pragma mark Keyboard
// implementing these makes a keyboard pop up when this view is first responder
- (void)insertText:(NSString *)text {
if (self.controlKey.selected) {
self.controlKey.selected = NO;
if (text.length == 1) {
char ch = [text characterAtIndex:0];
if (strchr(controlKeys, ch) != NULL) {
ch = toupper(ch) ^ 0x40;
text = [NSString stringWithFormat:@"%c", ch];
}
}
}
text = [text stringByReplacingOccurrencesOfString:@"\n" withString:@"\r"];
NSData *data = [text dataUsingEncoding:NSUTF8StringEncoding];
[self.terminal sendInput:data.bytes length:data.length];
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
if ([NSStringFromSelector(action) hasPrefix:@"_accessibility"] && [self.terminal.webView canPerformAction:action withSender:sender])
return YES;
return [super canPerformAction:action withSender:sender];
}
- (void)paste:(id)sender {
NSString *string = UIPasteboard.generalPasteboard.string;
if (string) {
[self insertText:string];
}
}
- (void)copy:(id)sender {
[self.terminal.webView evaluateJavaScript:@"exports.copy()" completionHandler:nil];
}
- (id)forwardingTargetForSelector:(SEL)selector {
if ([NSStringFromSelector(selector) hasPrefix:@"_accessibility"])
return self.terminal.webView;
return nil;
}
- (void)deleteBackward {
[self insertText:@"\x7f"];
}
- (BOOL)hasText {
return YES; // it's always ok to send a "delete"
}
- (void)clearScrollback:(UIKeyCommand *)command {
[self.terminal.webView evaluateJavaScript:@"exports.clearScrollback()" completionHandler:nil];
}
- (UITextSmartDashesType)smartDashesType API_AVAILABLE(ios(11)) {
return UITextSmartDashesTypeNo;
}
- (UITextSmartQuotesType)smartQuotesType API_AVAILABLE(ios(11)) {
return UITextSmartQuotesTypeNo;
}
- (UITextSmartInsertDeleteType)smartInsertDeleteType API_AVAILABLE(ios(11)) {
return UITextSmartInsertDeleteTypeNo;
}
- (UITextAutocapitalizationType)autocapitalizationType {
return UITextAutocapitalizationTypeNone;
}
- (UITextAutocorrectionType)autocorrectionType {
return UITextAutocorrectionTypeNo;
}
#pragma mark Hardware Keyboard
- (void)handleKeyCommand:(UIKeyCommand *)command {
NSString *key = command.input;
if (command.modifierFlags == 0) {
if ([key isEqualToString:@"`"] && UserPreferences.shared.backtickMapEscape)
key = UIKeyInputEscape;
if ([key isEqualToString:UIKeyInputEscape])
key = @"\x1b";
else if ([key isEqualToString:UIKeyInputUpArrow])
key = [self.terminal arrow:'A'];
else if ([key isEqualToString:UIKeyInputDownArrow])
key = [self.terminal arrow:'B'];
else if ([key isEqualToString:UIKeyInputLeftArrow])
key = [self.terminal arrow:'D'];
else if ([key isEqualToString:UIKeyInputRightArrow])
key = [self.terminal arrow:'C'];
[self insertText:key];
} else if (command.modifierFlags & UIKeyModifierShift) {
[self insertText:[key uppercaseString]];
} else if (command.modifierFlags & UIKeyModifierAlternate) {
[self insertText:[@"\x1b" stringByAppendingString:key]];
} else if (command.modifierFlags & UIKeyModifierAlphaShift) {
[self handleCapsLockWithCommand:command];
} else if (command.modifierFlags & UIKeyModifierControl || command.modifierFlags & UIKeyModifierAlphaShift) {
if (key.length == 0)
return;
if ([key isEqualToString:@"2"])
key = @"@";
else if ([key isEqualToString:@"6"])
key = @"^";
else if ([key isEqualToString:@"-"])
key = @"_";
char ch = (char) [key.uppercaseString characterAtIndex:0] ^ 0x40;
[self insertText:[NSString stringWithFormat:@"%c", ch]];
}
}
static const char *alphabet = "abcdefghijklmnopqrstuvwxyz";
static const char *controlKeys = "abcdefghijklmnopqrstuvwxyz26-=[]\\";
static const char *metaKeys = "abcdefghijklmnopqrstuvwxyz0123456789-=[]\\;',./";
- (NSArray<UIKeyCommand *> *)keyCommands {
if (_keyCommands != nil)
return _keyCommands;
_keyCommands = [NSMutableArray new];
[self addKeys:controlKeys withModifiers:UIKeyModifierControl];
for (NSString *specialKey in @[UIKeyInputEscape, UIKeyInputUpArrow, UIKeyInputDownArrow,
UIKeyInputLeftArrow, UIKeyInputRightArrow, @"\t"]) {
[self addKey:specialKey withModifiers:0];
}
if (UserPreferences.shared.capsLockMapping != CapsLockMapNone) {
[self addKeys:controlKeys withModifiers:UIKeyModifierAlphaShift];
[self addKeys:alphabet withModifiers:0];
[self addKeys:alphabet withModifiers:UIKeyModifierShift];
[self addKey:@"" withModifiers:UIKeyModifierAlphaShift]; // otherwise tap of caps lock can switch layouts
}
if (UserPreferences.shared.capsLockMapping == OptionMapEsc) {
[self addKeys:metaKeys withModifiers:UIKeyModifierAlternate];
}
if (UserPreferences.shared.backtickMapEscape) {
[self addKey:@"`" withModifiers:0];
}
[_keyCommands addObject:[UIKeyCommand keyCommandWithInput:@"k"
modifierFlags:UIKeyModifierCommand|UIKeyModifierShift
action:@selector(clearScrollback:)
discoverabilityTitle:@"Clear Scrollback"]];
return _keyCommands;
}
- (void)addKeys:(const char *)keys withModifiers:(UIKeyModifierFlags)modifiers {
for (size_t i = 0; keys[i] != '\0'; i++) {
[self addKey:[NSString stringWithFormat:@"%c", keys[i]] withModifiers:modifiers];
}
}
- (void)addKey:(NSString *)key withModifiers:(UIKeyModifierFlags)modifiers {
UIKeyCommand *command = [UIKeyCommand keyCommandWithInput:key
modifierFlags:modifiers
action:@selector(handleKeyCommand:)];
[_keyCommands addObject:command];
}
- (void)keyCommandTriggered:(UIKeyCommand *)sender {
dispatch_async(dispatch_get_main_queue(), ^{
[self handleKeyCommand:sender];
});
}
- (void)handleCapsLockWithCommand:(UIKeyCommand *)command {
CapsLockMapping target = UserPreferences.shared.capsLockMapping;
NSString *newInput = command.input ? command.input : @"";
UIKeyModifierFlags flags = command.modifierFlags;
flags ^= UIKeyModifierAlphaShift;
if(target == CapsLockMapEscape) {
newInput = UIKeyInputEscape;
} else if(target == CapsLockMapControl) {
if([newInput length] == 0) {
return;
}
flags |= UIKeyModifierControl;
} else {
return;
}
UIKeyCommand *newCommand = [UIKeyCommand keyCommandWithInput:newInput
modifierFlags:flags
action:@selector(keyCommandTriggered:)];
[self handleKeyCommand:newCommand];
}
@end