// // ViewController.m // iSH // // Created by Theodore Dubois on 10/17/17. // #import "TerminalViewController.h" #import "AppDelegate.h" #import "TerminalView.h" #import "BarButton.h" #import "ArrowBarButton.h" #import "UserPreferences.h" #import "AboutViewController.h" #include "kernel/init.h" #include "kernel/task.h" #include "kernel/calls.h" #include "fs/devices.h" @interface TerminalViewController () @property UITapGestureRecognizer *tapRecognizer; @property (weak, nonatomic) IBOutlet TerminalView *termView; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *bottomConstraint; @property (weak, nonatomic) IBOutlet UIButton *tabKey; @property (weak, nonatomic) IBOutlet UIButton *controlKey; @property (weak, nonatomic) IBOutlet UIButton *escapeKey; @property (strong, nonatomic) IBOutletCollection(id) NSArray *barButtons; @property (strong, nonatomic) IBOutletCollection(id) NSArray *barControls; @property (weak, nonatomic) IBOutlet UIInputView *barView; @property (weak, nonatomic) IBOutlet UIStackView *bar; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *barTop; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *barBottom; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *barLeading; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *barTrailing; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *barButtonWidth; @property (weak, nonatomic) IBOutlet UIButton *infoButton; @property (weak, nonatomic) IBOutlet UIButton *pasteButton; @property (weak, nonatomic) IBOutlet UIButton *hideKeyboardButton; @property int sessionPid; @property (nonatomic) Terminal *sessionTerminal; @property int sessionTerminalNumber; @end @implementation TerminalViewController - (void)viewDidLoad { [super viewDidLoad]; int bootError = [AppDelegate bootError]; if (bootError < 0) { NSString *message = [NSString stringWithFormat:@"could not boot"]; NSString *subtitle = [NSString stringWithFormat:@"error code %d", bootError]; if (bootError == _EINVAL) subtitle = [subtitle stringByAppendingString:@"\n(try reinstalling the app, see release notes for details)"]; [self showMessage:message subtitle:subtitle]; NSLog(@"boot failed with code %d", bootError); } self.termView.terminal = self.terminal; [self.termView becomeFirstResponder]; NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(keyboardDidSomething:) name:UIKeyboardWillChangeFrameNotification object:nil]; [self _updateStyleFromPreferences:NO]; [[UserPreferences shared] addObserver:self forKeyPath:@"theme" options:NSKeyValueObservingOptionNew context:nil]; if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) { [self.bar removeArrangedSubview:self.hideKeyboardButton]; [self.hideKeyboardButton removeFromSuperview]; } if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone) { self.barView.frame = CGRectMake(0, 0, 100, 48); } else { self.barView.frame = CGRectMake(0, 0, 100, 55); } // SF Symbols is cool if (@available(iOS 13, *)) { [self.infoButton setImage:[UIImage systemImageNamed:@"gear"] forState:UIControlStateNormal]; [self.pasteButton setImage:[UIImage systemImageNamed:@"doc.on.clipboard"] forState:UIControlStateNormal]; [self.hideKeyboardButton setImage:[UIImage systemImageNamed:@"keyboard.chevron.compact.down"] forState:UIControlStateNormal]; [self.tabKey setTitle:nil forState:UIControlStateNormal]; [self.tabKey setImage:[UIImage systemImageNamed:@"arrow.right.to.line.alt"] forState:UIControlStateNormal]; [self.controlKey setTitle:nil forState:UIControlStateNormal]; [self.controlKey setImage:[UIImage systemImageNamed:@"control"] forState:UIControlStateNormal]; [self.escapeKey setTitle:nil forState:UIControlStateNormal]; [self.escapeKey setImage:[UIImage systemImageNamed:@"escape"] forState:UIControlStateNormal]; } } - (void)awakeFromNib { [super awakeFromNib]; [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(processExited:) name:ProcessExitedNotification object:nil]; } - (void)startNewSession { int err = [self startSession]; if (err < 0) { [self showMessage:@"could not start session" subtitle:[NSString stringWithFormat:@"error code %d", err]]; } } - (void)reconnectSessionFromTerminalUUID:(NSUUID *)uuid { self.sessionTerminal = [Terminal terminalWithUUID:uuid]; if (self.sessionTerminal == nil) [self startNewSession]; } - (NSUUID *)sessionTerminalUUID { return self.terminal.uuid; } - (int)startSession { int err = become_new_init_child(); if (err < 0) return err; struct tty *tty; self.sessionTerminal = nil; Terminal *terminal = [Terminal createPseudoTerminal:&tty]; if (terminal == nil) { NSAssert(IS_ERR(tty), @"tty should be error"); return (int) PTR_ERR(tty); } self.sessionTerminal = terminal; self.sessionTerminalNumber = tty->num; NSString *stdioFile = [NSString stringWithFormat:@"/dev/pts/%d", tty->num]; err = create_stdio(stdioFile.fileSystemRepresentation, TTY_PSEUDO_SLAVE_MAJOR, tty->num); if (err < 0) return err; tty_release(tty); char argv[4096]; NSArray *command = UserPreferences.shared.launchCommand; [Terminal convertCommand:command toArgs:argv limitSize:sizeof(argv)]; const char *envp = "TERM=xterm-256color\0"; err = do_execve(command[0].UTF8String, command.count, argv, envp); if (err < 0) return err; self.sessionPid = current->pid; task_start(current); return 0; } - (void)processExited:(NSNotification *)notif { int pid = [notif.userInfo[@"pid"] intValue]; if (pid != self.sessionPid) return; [self.sessionTerminal destroy]; self.sessionTerminalNumber = 0; // On iOS 13, there are multiple windows, so just close this one. if (@available(iOS 13, *)) { // On iPhone, destroying scenes will fail, but the error doesn't actually go to the error handler, which is really stupid. Apple doesn't fix bugs, so I'm forced to just add a check here. if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad && self.sceneSession != nil) { [UIApplication.sharedApplication requestSceneSessionDestruction:self.sceneSession options:nil errorHandler:^(NSError *error) { NSLog(@"scene destruction error %@", error); self.sceneSession = nil; [self processExited:notif]; }]; return; } } [self startNewSession]; } - (void)showMessage:(NSString *)message subtitle:(NSString *)subtitle { dispatch_async(dispatch_get_main_queue(), ^{ UIAlertController *alert = [UIAlertController alertControllerWithTitle:message message:subtitle preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"k" style:UIAlertActionStyleDefault handler:nil]]; [self presentViewController:alert animated:YES completion:nil]; }); } - (void)dealloc { @try { [[UserPreferences shared] removeObserver:self forKeyPath:@"theme"]; } @catch (NSException * __unused exception) {} } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (object == [UserPreferences shared]) { [self _updateStyleFromPreferences:YES]; } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } - (void)_updateStyleFromPreferences:(BOOL)animated { NSTimeInterval duration = animated ? 0.1 : 0; [UIView animateWithDuration:duration animations:^{ self.view.backgroundColor = UserPreferences.shared.theme.backgroundColor; UIKeyboardAppearance keyAppearance = UserPreferences.shared.theme.keyboardAppearance; self.termView.keyboardAppearance = keyAppearance; for (BarButton *button in self.barButtons) { button.keyAppearance = keyAppearance; } UIColor *tintColor = keyAppearance == UIKeyboardAppearanceLight ? UIColor.blackColor : UIColor.whiteColor; for (UIControl *control in self.barControls) { control.tintColor = tintColor; } }]; } - (UIStatusBarStyle)preferredStatusBarStyle { return UserPreferences.shared.theme.statusBarStyle; } - (BOOL)prefersStatusBarHidden { BOOL isIPhoneX = UIApplication.sharedApplication.delegate.window.safeAreaInsets.top > 20; return !isIPhoneX; } - (void)keyboardDidSomething:(NSNotification *)notification { // NSLog(@"%@", notification); BOOL initialLayout = self.termView.needsUpdateConstraints; CGRect keyboardFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; CGFloat pad; if (keyboardFrame.origin.x == 0 && keyboardFrame.origin.y == 0 && keyboardFrame.size.height == 0 && keyboardFrame.size.width == 0) { pad = 0; } else { pad = UIScreen.mainScreen.bounds.size.height - keyboardFrame.origin.y; } if (pad == 0) { pad = self.view.safeAreaInsets.bottom; } // NSLog(@"pad %f", pad); self.bottomConstraint.constant = -pad; [self.view setNeedsUpdateConstraints]; if (!initialLayout) { // if initial layout hasn't happened yet, the terminal view is going to be at a really weird place, so animating it is going to look really bad NSNumber *interval = notification.userInfo[UIKeyboardAnimationDurationUserInfoKey]; NSNumber *curve = notification.userInfo[UIKeyboardAnimationCurveUserInfoKey]; [UIView animateWithDuration:interval.doubleValue delay:0 options:curve.integerValue << 16 animations:^{ [self.view layoutIfNeeded]; } completion:nil]; } } - (void)ishExited:(NSNotification *)notification { [self performSelectorOnMainThread:@selector(displayExitThing) withObject:nil waitUntilDone:YES]; } - (void)displayExitThing { UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"attempted to kill init" message:nil preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"exit" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { id delegate = [UIApplication sharedApplication].delegate; [delegate exitApp]; }]]; if ([UserPreferences.shared hasChangedLaunchCommand]) [alert addAction:[UIAlertAction actionWithTitle:@"i typed the init command wrong, let me fix it" style:UIAlertActionStyleDefault handler:nil]]; [self presentViewController:alert animated:YES completion:nil]; } #pragma mark Bar - (IBAction)showAbout:(id)sender { UINavigationController *navigationController = [[UIStoryboard storyboardWithName:@"About" bundle:nil] instantiateInitialViewController]; if ([sender isKindOfClass:[UIGestureRecognizer class]]) { UIGestureRecognizer *recognizer = sender; if (recognizer.state == UIGestureRecognizerStateBegan) { AboutViewController *aboutViewController = (AboutViewController *) navigationController.topViewController; aboutViewController.includeDebugPanel = YES; } else { return; } } [self presentViewController:navigationController animated:YES completion:nil]; } - (void)resizeBar { CGSize screen = UIScreen.mainScreen.bounds.size; CGSize bar = self.barView.bounds.size; // set sizing parameters on bar // numbers stolen from iVim and modified somewhat if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone) { // phone [self setBarHorizontalPadding:6 verticalPadding:6 buttonWidth:32]; } else if (bar.width == screen.width || bar.width == screen.height) { // full-screen ipad [self setBarHorizontalPadding:15 verticalPadding:8 buttonWidth:43]; } else if (bar.width <= 320) { // slide over [self setBarHorizontalPadding:8 verticalPadding:8 buttonWidth:26]; } else { // split view [self setBarHorizontalPadding:10 verticalPadding:8 buttonWidth:36]; } [UIView performWithoutAnimation:^{ [self.barView layoutIfNeeded]; }]; } - (void)setBarHorizontalPadding:(CGFloat)horizontal verticalPadding:(CGFloat)vertical buttonWidth:(CGFloat)buttonWidth { self.barLeading.constant = self.barTrailing.constant = horizontal; self.barTop.constant = self.barBottom.constant = vertical; self.barButtonWidth.constant = buttonWidth; } - (IBAction)pressEscape:(id)sender { [self pressKey:@"\x1b"]; } - (IBAction)pressTab:(id)sender { [self pressKey:@"\t"]; } - (void)pressKey:(NSString *)key { [self.termView insertText:key]; } - (IBAction)pressControl:(id)sender { self.controlKey.selected = !self.controlKey.selected; } - (IBAction)pressArrow:(ArrowBarButton *)sender { switch (sender.direction) { case ArrowUp: [self pressKey:[self.terminal arrow:'A']]; break; case ArrowDown: [self pressKey:[self.terminal arrow:'B']]; break; case ArrowLeft: [self pressKey:[self.terminal arrow:'D']]; break; case ArrowRight: [self pressKey:[self.terminal arrow:'C']]; break; case ArrowNone: break; } } - (void)switchTerminal:(UIKeyCommand *)sender { unsigned i = (unsigned) sender.input.integerValue; if (i == 7) self.terminal = self.sessionTerminal; else self.terminal = [Terminal terminalWithType:TTY_CONSOLE_MAJOR number:i]; } - (void)increaseFontSize:(UIKeyCommand *)command { self.termView.overrideFontSize = self.termView.effectiveFontSize + 1; } - (void)decreaseFontSize:(UIKeyCommand *)command { self.termView.overrideFontSize = self.termView.effectiveFontSize - 1; } - (void)resetFontSize:(UIKeyCommand *)command { self.termView.overrideFontSize = 0; } - (NSArray *)keyCommands { static NSMutableArray *commands = nil; if (commands == nil) { commands = [NSMutableArray new]; for (unsigned i = 1; i <= 7; i++) { [commands addObject: [UIKeyCommand keyCommandWithInput:[NSString stringWithFormat:@"%d", i] modifierFlags:UIKeyModifierCommand|UIKeyModifierAlternate|UIKeyModifierShift action:@selector(switchTerminal:)]]; } [commands addObject: [UIKeyCommand keyCommandWithInput:@"+" modifierFlags:UIKeyModifierCommand action:@selector(increaseFontSize:) discoverabilityTitle:@"Increase Font Size"]]; [commands addObject: [UIKeyCommand keyCommandWithInput:@"=" modifierFlags:UIKeyModifierCommand action:@selector(increaseFontSize:)]]; [commands addObject: [UIKeyCommand keyCommandWithInput:@"-" modifierFlags:UIKeyModifierCommand action:@selector(decreaseFontSize:) discoverabilityTitle:@"Decrease Font Size"]]; [commands addObject: [UIKeyCommand keyCommandWithInput:@"0" modifierFlags:UIKeyModifierCommand action:@selector(resetFontSize:) discoverabilityTitle:@"Reset Font Size"]]; [commands addObject: [UIKeyCommand keyCommandWithInput:@"," modifierFlags:UIKeyModifierCommand action:@selector(showAbout:) discoverabilityTitle:@"Settings"]]; } return commands; } - (void)setTerminal:(Terminal *)terminal { _terminal = terminal; self.termView.terminal = self.terminal; } - (void)setSessionTerminal:(Terminal *)sessionTerminal { if (_terminal == _sessionTerminal) self.terminal = sessionTerminal; _sessionTerminal = sessionTerminal; } @end @interface BarView : UIInputView @property (weak) IBOutlet TerminalViewController *terminalViewController; @end @implementation BarView - (void)layoutSubviews { [self.terminalViewController resizeBar]; } @end