mirror of
https://github.com/ish-app/ish.git
synced 2026-01-25 14:06:40 +00:00
428 lines
17 KiB
Objective-C
428 lines
17 KiB
Objective-C
//
|
|
// 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 () <UIGestureRecognizerDelegate>
|
|
|
|
@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<NSString *> *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<UIKeyCommand *> *)keyCommands {
|
|
static NSMutableArray<UIKeyCommand *> *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
|