mirror of
https://github.com/ish-app/ish.git
synced 2026-01-25 14:06:40 +00:00
309 lines
12 KiB
Objective-C
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
|