-
Notifications
You must be signed in to change notification settings - Fork 109
/
SceneDelegate.swift
2878 lines (2770 loc) · 140 KB
/
SceneDelegate.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//
// SceneDelegate.swift
// a-Shell
//
// Created by Nicolas Holzschuch on 30/06/2019.
// Copyright © 2019 AsheKube. All rights reserved.
//
import UIKit
import SwiftUI
import WebKit
import ios_system
import MobileCoreServices
import Combine
import AVKit // for media playback
import AVFoundation // for media playback
var messageHandlerAdded = false
var externalKeyboardPresent: Bool? // still needed?
var inputFileURLBackup: URL?
let factoryFontSize = Float(13)
let factoryFontName = "Menlo"
var stdinString: String = ""
// Need: dictionary connecting userContentController with output streams (?)
class SceneDelegate: UIViewController, UIWindowSceneDelegate, WKNavigationDelegate, WKScriptMessageHandler, UIDocumentPickerDelegate, UIPopoverPresentationControllerDelegate, UIFontPickerViewControllerDelegate, UIDocumentInteractionControllerDelegate {
var window: UIWindow?
var windowScene: UIWindowScene?
var webView: WKWebView?
var wasmWebView: WKWebView? // webView for executing wasm
var contentView: ContentView?
var history: [String] = []
var width = 80
var height = 80
var stdout_active = false
var persistentIdentifier: String? = nil
var stdin_file: UnsafeMutablePointer<FILE>? = nil
var stdin_file_input: FileHandle? = nil
var stdout_file: UnsafeMutablePointer<FILE>? = nil
var tty_file: UnsafeMutablePointer<FILE>? = nil
var tty_file_input: FileHandle? = nil
// copies of thread_std*, used when inside a sub-thread, for example executing webAssembly
var thread_stdin_copy: UnsafeMutablePointer<FILE>? = nil
var thread_stdout_copy: UnsafeMutablePointer<FILE>? = nil
var thread_stderr_copy: UnsafeMutablePointer<FILE>? = nil
// var keyboardTimer: Timer!
private let commandQueue = DispatchQueue(label: "executeCommand", qos: .utility) // low priority, for executing commands
private var javascriptRunning = false // We can't execute JS while we are already executing JS.
// Buttons and toolbars:
var controlOn = false;
// control codes:
let interrupt = "\u{0003}" // control-C, used to kill the process
let endOfTransmission = "\u{0004}" // control-D, used to signal end of transmission
let escape = "\u{001B}"
// Are we editing a file?
var closeAfterCommandTerminates = false
var resetDirectoryAfterCommandTerminates = ""
var currentCommand = ""
var pid: pid_t = 0
private var selectedDirectory = ""
private var selectedFont = ""
// Store these for session restore:
var currentDirectory = ""
var previousDirectory = ""
// Store cancelalble instances
var cancellables = Set<AnyCancellable>()
// Customizable user interface:
var terminalFontSize: Float?
var terminalFontName: String?
var terminalBackgroundColor: UIColor?
var terminalForegroundColor: UIColor?
var terminalCursorColor: UIColor?
// for audio / video playback:
var avplayer: AVPlayer? = nil
var avcontroller: AVPlayerViewController? = nil
var avControllerPiPEnabled = false
// Create a document picker for directories.
private let documentPicker =
UIDocumentPickerViewController(documentTypes: [kUTTypeFolder as String],
in: .open)
var screenWidth: CGFloat {
if windowScene!.interfaceOrientation.isPortrait {
return UIScreen.main.bounds.size.width
} else {
return UIScreen.main.bounds.size.height
}
}
var screenHeight: CGFloat {
if windowScene!.interfaceOrientation.isPortrait {
return UIScreen.main.bounds.size.height
} else {
return UIScreen.main.bounds.size.width
}
}
var fontSize: CGFloat {
let deviceModel = UIDevice.current.model
if (deviceModel.hasPrefix("iPad")) {
let minFontSize: CGFloat = screenWidth / 55
// print("Screen width = \(screenWidth), fontSize = \(minFontSize)")
if (minFontSize > 16) { return 16.0 }
else { return minFontSize }
} else {
let minFontSize: CGFloat = screenWidth / 23
// print("Screen width = \(screenWidth), fontSize = \(minFontSize)")
if (minFontSize > 15) { return 15.0 }
else { return minFontSize }
}
}
var toolbarHeight: CGFloat {
let deviceModel = UIDevice.current.model
if (deviceModel.hasPrefix("iPad")) {
return 40
} else {
return 35
}
}
var isVimRunning: Bool {
return (currentCommand.hasPrefix("vim ")) || (currentCommand == "vim") || (currentCommand.hasPrefix("jump "))
}
@objc private func tabAction(_ sender: UIBarButtonItem) {
webView?.evaluateJavaScript("window.term_.io.onVTKeystroke(\"" + "\u{0009}" + "\");") { (result, error) in
if error != nil {
// print(error)
}
if (result != nil) {
// print(result)
}
}
}
@objc private func controlAction(_ sender: UIBarButtonItem) {
controlOn = !controlOn;
let configuration = UIImage.SymbolConfiguration(pointSize: fontSize, weight: .regular, scale: .large)
if (controlOn) {
editorToolbar.items?[1].image = UIImage(systemName: "chevron.up.square.fill")!.withConfiguration(configuration)
webView?.evaluateJavaScript("window.controlOn = true;") { (result, error) in
if error != nil {
// print(error)
}
if (result != nil) {
// print(result)
}
}
} else {
editorToolbar.items?[1].image = UIImage(systemName: "chevron.up.square")!.withConfiguration(configuration)
webView?.evaluateJavaScript("window.controlOn = false;") { (result, error) in
if error != nil {
// print(error)
}
if (result != nil) {
// print(result)
}
}
}
}
@objc private func escapeAction(_ sender: UIBarButtonItem) {
webView?.evaluateJavaScript("window.term_.io.onVTKeystroke(\"" + escape + "\");") { (result, error) in
if error != nil {
// print(error)
}
if (result != nil) {
// print(result)
}
}
}
@objc private func upAction(_ sender: UIBarButtonItem) {
webView?.evaluateJavaScript("window.term_.io.onVTKeystroke(\"" + escape + "[A\");") { (result, error) in
if error != nil {
// print(error)
}
if (result != nil) {
// print(result)
}
}
}
@objc private func downAction(_ sender: UIBarButtonItem) {
webView?.evaluateJavaScript("window.term_.io.onVTKeystroke(\"" + escape + "[B\");") { (result, error) in
if error != nil {
// print(error)
}
if (result != nil) {
// print(result)
}
}
}
@objc private func leftAction(_ sender: UIBarButtonItem) {
webView?.evaluateJavaScript("window.term_.io.onVTKeystroke(\"" + escape + "[D\");") { (result, error) in
if error != nil {
// print(error)
}
if (result != nil) {
// print(result)
}
}
}
@objc private func rightAction(_ sender: UIBarButtonItem) {
webView?.evaluateJavaScript("window.term_.io.onVTKeystroke(\"" + escape + "[C\");") { (result, error) in
if error != nil {
// print(error)
}
if (result != nil) {
// print(result)
}
}
}
var tabButton: UIBarButtonItem {
let configuration = UIImage.SymbolConfiguration(pointSize: fontSize, weight: .regular)
let tabButton = UIBarButtonItem(image: UIImage(systemName: "arrow.right.to.line.alt")!.withConfiguration(configuration), style: .plain, target: self, action: #selector(tabAction(_:)))
tabButton.isAccessibilityElement = true
tabButton.accessibilityLabel = "Tab"
return tabButton
}
var controlButton: UIBarButtonItem {
let configuration = UIImage.SymbolConfiguration(pointSize: fontSize, weight: .regular, scale: .large)
// Image used to be control
let imageControl = (controlOn == true) ? UIImage(systemName: "chevron.up.square.fill")! : UIImage(systemName: "chevron.up.square")!
let controlButton = UIBarButtonItem(image: imageControl.withConfiguration(configuration), style: .plain, target: self, action: #selector(controlAction(_:)))
controlButton.isAccessibilityElement = true
controlButton.accessibilityLabel = "Control"
return controlButton
}
var escapeButton: UIBarButtonItem {
let configuration = UIImage.SymbolConfiguration(pointSize: fontSize, weight: .regular)
let escapeButton = UIBarButtonItem(image: UIImage(systemName: "escape")!.withConfiguration(configuration), style: .plain, target: self, action: #selector(escapeAction(_:)))
escapeButton.isAccessibilityElement = true
escapeButton.accessibilityLabel = "Escape"
return escapeButton
}
var upButton: UIBarButtonItem {
let configuration = UIImage.SymbolConfiguration(pointSize: fontSize, weight: .regular)
let upButton = UIBarButtonItem(image: UIImage(systemName: "arrow.up")!.withConfiguration(configuration), style: .plain, target: self, action: #selector(upAction(_:)))
upButton.isAccessibilityElement = true
upButton.accessibilityLabel = "Up arrow"
return upButton
}
var downButton: UIBarButtonItem {
let configuration = UIImage.SymbolConfiguration(pointSize: fontSize, weight: .regular)
let downButton = UIBarButtonItem(image: UIImage(systemName: "arrow.down")!.withConfiguration(configuration), style: .plain, target: self, action: #selector(downAction(_:)))
downButton.isAccessibilityElement = true
downButton.accessibilityLabel = "Down arrow"
return downButton
}
var leftButton: UIBarButtonItem {
let configuration = UIImage.SymbolConfiguration(pointSize: fontSize, weight: .regular)
let leftButton = UIBarButtonItem(image: UIImage(systemName: "arrow.left")!.withConfiguration(configuration), style: .plain, target: self, action: #selector(leftAction(_:)))
leftButton.isAccessibilityElement = true
leftButton.accessibilityLabel = "Left arrow"
return leftButton
}
var rightButton: UIBarButtonItem {
let configuration = UIImage.SymbolConfiguration(pointSize: fontSize, weight: .regular)
let rightButton = UIBarButtonItem(image: UIImage(systemName: "arrow.right")!.withConfiguration(configuration), style: .plain, target: self, action: #selector(rightAction(_:)))
rightButton.isAccessibilityElement = true
rightButton.accessibilityLabel = "Right arrow"
return rightButton
}
public lazy var editorToolbar: UIToolbar = {
var toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: (self.webView?.bounds.width)!, height: toolbarHeight))
toolbar.tintColor = .label
toolbar.items = [tabButton, controlButton, escapeButton, UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: self, action: nil), upButton, downButton, leftButton, rightButton]
return toolbar
}()
func printPrompt() {
DispatchQueue.main.async {
self.webView?.evaluateJavaScript("window.commandRunning = ''; window.printPrompt(); window.updatePromptPosition();") { (result, error) in
if error != nil {
// print(error)
}
if (result != nil) {
// print(result)
}
}
}
}
func printHistory() {
for command in history {
fputs(command + "\n", thread_stdout)
}
}
func printText(string: String) {
fputs(string, thread_stdout)
}
func printError(string: String) {
fputs(string, thread_stderr)
}
func closeWindow() {
// Only close if all running functions are terminated:
NSLog("Closing window: \(currentCommand)")
if (currentCommand != "") {
// There is a command running. Wait.
// TODO: trigger cleanup depending on command (exit for nslookup, for ex.)
closeAfterCommandTerminates = true
return
}
let deviceModel = UIDevice.current.model
if (deviceModel.hasPrefix("iPad")) {
UIApplication.shared.requestSceneSessionDestruction(self.windowScene!.session, options: nil)
}
}
func clearScreen() {
DispatchQueue.main.async {
// clear entire display: ^[[2J
// position cursor on top line: ^[[1;1H
self.webView?.evaluateJavaScript("window.term_.io.print('" + self.escape + "[2J'); window.term_.io.print('" + self.escape + "[1;1H'); ") { (result, error) in
if error != nil {
// print(error)
}
if (result != nil) {
// print(result)
}
}
// self.webView?.accessibilityLabel = ""
}
}
func executeWebAssembly(arguments: [String]?) -> Int32 {
guard (arguments != nil) else { return -1 }
guard (arguments!.count >= 2) else { return -1 } // There must be at least one command
// copy arguments:
let command = arguments![1]
var argumentString = "["
for c in 1...arguments!.count-1 {
if let argument = arguments?[c] {
// replace quotes and backslashes in arguments:
let sanitizedArgument = argument.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"")
argumentString = argumentString + " \"" + sanitizedArgument + "\","
}
}
argumentString = argumentString + "]"
// async functions don't work in WKWebView (so, no fetch, no WebAssembly.instantiateStreaming)
// Instead, we load the file in swift and send the base64 version to JS
let currentDirectory = FileManager().currentDirectoryPath
let fileName = command.hasPrefix("/") ? command : currentDirectory + "/" + command
guard let buffer = NSData(contentsOf: URL(fileURLWithPath: fileName)) else {
fputs("wasm: file \(command) not found\n", thread_stderr)
return -1
}
let localEnvironment = environmentAsArray()
var environmentAsJSDictionary = "{"
if localEnvironment != nil {
for variable in localEnvironment! {
if let envVar = variable as? String {
// Let's not carry environment variables with quotes:
if (envVar.contains("\"")) {
continue
}
let components = envVar.components(separatedBy:"=")
if (components.count == 0) {
continue
}
let name = components[0]
var value = envVar
value.removeFirst(name.count + 1)
environmentAsJSDictionary += "\"" + name + "\"" + ":" + "\"" + value + "\",\n"
}
}
}
environmentAsJSDictionary += "}"
let base64string = buffer.base64EncodedString()
let javascript = "executeWebAssembly(\"\(base64string)\", " + argumentString + ", \"" + currentDirectory + "\", \(ios_isatty(STDIN_FILENO)), " + environmentAsJSDictionary + ")"
if (javascriptRunning) {
fputs("We can't execute webAssembly while we are already executing webAssembly.", thread_stderr)
return -1
}
javascriptRunning = true
var errorCode:Int32 = 0
thread_stdin_copy = thread_stdin
thread_stdout_copy = thread_stdout
thread_stderr_copy = thread_stderr
DispatchQueue.main.async {
self.wasmWebView?.evaluateJavaScript(javascript) { (result, error) in
if error != nil {
let userInfo = (error! as NSError).userInfo
fputs("wasm: Error ", self.thread_stderr_copy)
// WKJavaScriptExceptionSourceURL is hterm.html, of course.
if let file = userInfo["WKJavaScriptExceptionSourceURL"] as? String {
fputs("in file " + file + " ", self.thread_stderr_copy)
}
if let line = userInfo["WKJavaScriptExceptionLineNumber"] as? Int32 {
fputs("at line \(line)", self.thread_stderr_copy)
}
if let column = userInfo["WKJavaScriptExceptionColumnNumber"] as? Int32 {
fputs(", column \(column): ", self.thread_stderr_copy)
} else {
fputs(": ", self.thread_stderr_copy)
}
if let message = userInfo["WKJavaScriptExceptionMessage"] as? String {
fputs(message + "\n", self.thread_stderr_copy)
}
fflush(self.thread_stderr_copy)
// print(error)
}
if (result != nil) {
// executeWebAssembly sends back stdout and stderr as two Strings:
if let array = result! as? NSMutableArray {
if let code = array[0] as? Int32 {
// return value from program
errorCode = code
}
if let errorMessage = array[1] as? String {
// webAssembly compile error:
fputs(errorMessage, self.thread_stderr_copy);
}
} else if let string = result! as? String {
fputs(string, self.thread_stdout_copy);
}
}
self.javascriptRunning = false
}
}
// force synchronization:
while (javascriptRunning) {
if (thread_stdout != nil && stdout_active) { fflush(thread_stdout) }
if (thread_stderr != nil && stdout_active) { fflush(thread_stderr) }
}
return errorCode
}
func printJscUsage() {
fputs("Usage: jsc [--in-window] file.js\nExecutes file.js.\n--in-window: runs inside the main window (can change terminal appearance or behaviour; use with caution).\n", thread_stdout)
}
func executeJavascript(arguments: [String]?) {
guard (arguments != nil) else {
printJscUsage()
return
}
guard ((arguments!.count <= 3) && (arguments!.count > 1)) else {
printJscUsage()
return
}
var command = arguments![1]
var jscWebView = wasmWebView
if (arguments!.count == 3) {
if (arguments![1] == "--in-window") {
command = arguments![2]
jscWebView = webView
} else {
printJscUsage()
return
}
}
let fileName = FileManager().currentDirectoryPath + "/" + command
thread_stdin_copy = thread_stdin
thread_stdout_copy = thread_stdout
thread_stderr_copy = thread_stderr
if (javascriptRunning) {
fputs("We can't execute JavaScript from a script already running JavaScript.", thread_stderr)
return
}
javascriptRunning = true
do {
let javascript = try String(contentsOf: URL(fileURLWithPath: fileName), encoding: String.Encoding.utf8)
DispatchQueue.main.async {
jscWebView?.evaluateJavaScript(javascript) { (result, error) in
if error != nil {
// Extract information about *where* the error is, etc.
let userInfo = (error! as NSError).userInfo
fputs("jsc: Error ", self.thread_stderr_copy)
// WKJavaScriptExceptionSourceURL is hterm.html, of course.
fputs("in file " + command + " ", self.thread_stderr_copy)
if let line = userInfo["WKJavaScriptExceptionLineNumber"] as? Int32 {
fputs("at line \(line)", self.thread_stderr_copy)
}
if let column = userInfo["WKJavaScriptExceptionColumnNumber"] as? Int32 {
fputs(", column \(column): ", self.thread_stderr_copy)
} else {
fputs(": ", self.thread_stderr_copy)
}
if let message = userInfo["WKJavaScriptExceptionMessage"] as? String {
fputs(message + "\n", self.thread_stderr_copy)
}
fflush(self.thread_stderr_copy)
}
if (result != nil) {
if let string = result! as? String {
fputs(string, self.thread_stdout_copy)
fputs("\n", self.thread_stdout_copy)
} else if let number = result! as? Int32 {
fputs("\(number)", self.thread_stdout_copy)
fputs("\n", self.thread_stdout_copy)
} else if let number = result! as? Float {
fputs("\(number)", self.thread_stdout_copy)
fputs("\n", self.thread_stdout_copy)
} else {
fputs("\(result!)", self.thread_stdout_copy)
fputs("\n", self.thread_stdout_copy)
}
fflush(self.thread_stdout_copy)
fflush(self.thread_stderr_copy)
}
self.javascriptRunning = false
}
}
}
catch {
fputs("Error executing JavaScript file: " + command + ": \(error) \n", thread_stderr)
javascriptRunning = false
}
while (javascriptRunning) {
if (thread_stdout != nil && stdout_active) { fflush(thread_stdout) }
if (thread_stderr != nil && stdout_active) { fflush(thread_stderr) }
}
thread_stdout_copy = nil
thread_stderr_copy = nil
}
// display the current configuration of the window.
func showConfigWindow() {
if (terminalFontName != nil) {
fputs(terminalFontName! + " ", thread_stdout)
} else {
fputs(factoryFontName + " ", thread_stdout)
}
if (terminalFontSize != nil) {
fputs("\(terminalFontSize!) pt, ", thread_stdout)
} else {
fputs("\(factoryFontSize) pt, ", thread_stdout)
}
if (terminalBackgroundColor == nil) {
fputs(" background: system ", thread_stdout)
} else if (terminalBackgroundColor == .systemBackground) {
fputs(" background: system ", thread_stdout)
} else {
var r:CGFloat = 0
var g:CGFloat = 0
var b:CGFloat = 0
var a:CGFloat = 0
terminalBackgroundColor!.getRed(&r, green: &g, blue: &b, alpha: &a)
fputs(String(format: "background: %.2f %.2f %.2f ", r, g, b), thread_stdout)
}
if (terminalForegroundColor == nil) {
fputs(" foreground: system ", thread_stdout)
} else if (terminalForegroundColor == .placeholderText) {
fputs(" foreground: system ", thread_stdout)
} else {
var r:CGFloat = 0
var g:CGFloat = 0
var b:CGFloat = 0
var a:CGFloat = 0
terminalForegroundColor!.getRed(&r, green: &g, blue: &b, alpha: &a)
fputs(String(format: "foreground: %.2f %.2f %.2f ", r, g, b), thread_stdout)
}
if (terminalCursorColor == nil) {
fputs(" cursor: system ", thread_stdout)
} else if (terminalCursorColor == .link) {
fputs(" cursor: system ", thread_stdout)
} else {
var r:CGFloat = 0
var g:CGFloat = 0
var b:CGFloat = 0
var a:CGFloat = 0
terminalCursorColor!.getRed(&r, green: &g, blue: &b, alpha: &a)
fputs(String(format: "cursor: %.2f %.2f %.2f ", r, g, b), thread_stdout)
}
fputs("\n", thread_stdout)
}
func writeConfigWindow() {
// Force rewrite of all color parameters. Used for reset.
let traitCollection = webView!.traitCollection
// Set scene parameters (unless they were set before)
let backgroundColor = terminalBackgroundColor ?? UIColor.systemBackground.resolvedColor(with: traitCollection)
let foregroundColor = terminalForegroundColor ?? UIColor.placeholderText.resolvedColor(with: traitCollection)
let cursorColor = terminalCursorColor ?? UIColor.link.resolvedColor(with: traitCollection)
// TODO: add font size and font name
let fontSize = terminalFontSize ?? factoryFontSize
let fontName = terminalFontName ?? factoryFontName
// Force writing all config to term. Used when we changed many parameters.
var command = "window.term_.setForegroundColor('" + foregroundColor.toHexString() + "'); window.term_.setBackgroundColor('" + backgroundColor.toHexString() + "'); window.term_.setCursorColor('" + cursorColor.toHexString() + "'); window.term_.setFontSize(\(fontSize)); window.term_.setFontFamily('\(fontName)');"
DispatchQueue.main.async {
self.webView?.evaluateJavaScript(command) { (result, error) in
if error != nil {
// print(error)
}
if (result != nil) {
// print(result)
}
}
command = "window.term_.prefs_.set('foreground-color', '" + foregroundColor.toHexString() + "'); window.term_.prefs_.set('background-color', '" + backgroundColor.toHexString() + "'); window.term_.prefs_.set('cursor-color', '" + cursorColor.toHexString() + "'); window.term_.prefs_.set('font-size', '\(fontSize)'); window.term_.prefs_.set('font-family', '\(fontName)');"
self.webView?.evaluateJavaScript(command) { (result, error) in
if error != nil {
// print(error)
}
if (result != nil) {
// print(result)
}
}
}
}
func configWindow(fontSize: Float?, fontName: String?, backgroundColor: UIColor?, foregroundColor: UIColor?, cursorColor: UIColor?) {
if (fontSize != nil) {
terminalFontSize = fontSize
let fontSizeCommand = "window.term_.setFontSize(\(fontSize!));"
DispatchQueue.main.async {
self.webView?.evaluateJavaScript(fontSizeCommand) { (result, error) in
if error != nil {
// print(error)
}
if (result != nil) {
// print(result)
}
}
}
}
if (fontName != nil) {
terminalFontName = fontName
if (!terminalFontName!.hasSuffix(".ttf") && !terminalFontName!.hasSuffix(".otf")) {
// System fonts, defined by their names:
let fontNameCommand = "window.term_.setFontFamily(\"\(fontName!)\");"
DispatchQueue.main.async {
self.webView?.evaluateJavaScript(fontNameCommand) { (result, error) in
if error != nil {
print(error)
}
if (result != nil) {
print(result)
}
}
}
} else {
// local fonts, defined by a file:
// Currently does not work.
let localFontURL = URL(fileURLWithPath: terminalFontName!)
var localFontName = localFontURL.lastPathComponent
localFontName.removeLast(".ttf".count)
// NSLog("Local Font Name: \(localFontName)")
DispatchQueue.main.async {
let fontNameCommand = "var newStyle = document.createElement('style'); newStyle.appendChild(document.createTextNode(\"@font-face { font-family: '\(localFontName)' ; src: url('\(localFontURL.path)') format('truetype'); }\")); document.head.appendChild(newStyle); window.term_.setFontFamily(\"\(localFontName)\");"
// NSLog(fontNameCommand)
self.webView?.evaluateJavaScript(fontNameCommand) { (result, error) in
if error != nil {
print(error)
}
if (result != nil) {
print(result)
}
}
}
}
}
if (backgroundColor != nil) {
terminalBackgroundColor = backgroundColor
webView!.backgroundColor = backgroundColor
let terminalColorCommand = "window.term_.setBackgroundColor(\"\(backgroundColor!.toHexString())\");"
DispatchQueue.main.async {
self.webView?.evaluateJavaScript(terminalColorCommand) { (result, error) in
if error != nil {
print(error)
}
if (result != nil) {
print(result)
}
}
}
}
if (foregroundColor != nil) {
terminalForegroundColor = foregroundColor
webView!.tintColor = foregroundColor
let terminalColorCommand = "window.term_.setForegroundColor(\"\(foregroundColor!.toHexString())\");"
DispatchQueue.main.async {
self.webView?.evaluateJavaScript(terminalColorCommand) { (result, error) in
if error != nil {
print(error)
}
if (result != nil) {
print(result)
}
}
}
}
if (cursorColor != nil) {
terminalCursorColor = cursorColor
let terminalColorCommand = "window.term_.setCursorColor(\"\(cursorColor!.toHexString())\");"
DispatchQueue.main.async {
self.webView?.evaluateJavaScript(terminalColorCommand) { (result, error) in
if error != nil {
print(error)
}
if (result != nil) {
print(result)
}
}
}
}
// Update COLORFGBG depending on new color:
if (foregroundColor != nil ) || (backgroundColor != nil) {
let fg = foregroundColor ?? terminalForegroundColor ?? UIColor.placeholderText.resolvedColor(with: traitCollection)
let bg = backgroundColor ?? terminalBackgroundColor ?? UIColor.systemBackground.resolvedColor(with: traitCollection)
setEnvironmentFGBG(foregroundColor: fg, backgroundColor: bg)
}
}
func keepDirectoryAfterShortcut() {
resetDirectoryAfterCommandTerminates = ""
}
// Creates the iOS 13 Font picker, returns the name of the font selected.
func pickFont() -> String? {
let rootVC = self.window?.rootViewController
let fontPickerConfig = UIFontPickerViewController.Configuration()
fontPickerConfig.includeFaces = true
fontPickerConfig.filteredTraits = .traitMonoSpace
// Create the font picker
let fontPicker = UIFontPickerViewController(configuration: fontPickerConfig)
fontPicker.delegate = self
// Present the font picker
selectedFont = ""
// Main issue: the user can dismiss the fontPicker by sliding upwards.
// So we need to check if it was, indeed dismissed:
DispatchQueue.main.sync {
rootVC?.present(fontPicker, animated: true, completion: nil)
}
while (!fontPicker.isBeingDismissed) { } // Wait until fontPicker is dismissed.
// Once the fontPicker is dismissed, wait to decide whether a font has been selected:
// NSLog("Dismissed. selectedFont= \(selectedFont)")
var timerDone = false
let seconds = 0.7 // roughly 2x slower observed time
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
timerDone = true
}
while (selectedFont == "") && !timerDone { }
// NSLog("Done. selectedFont= \(selectedFont)")
if (selectedFont != "cancel") && (selectedFont != "") {
return selectedFont
}
return nil
}
func fontPickerViewControllerDidCancel(_ viewController: UIFontPickerViewController) {
// User cancelled the font picker delegate
selectedFont = "cancel"
}
func fontPickerViewControllerDidPickFont(_ viewController: UIFontPickerViewController) {
// We got a font!
if let descriptor = viewController.selectedFontDescriptor {
if let name = descriptor.fontAttributes[.family] as? String {
// "Regular" variants of the font:
selectedFont = name
return
} else if let name = descriptor.fontAttributes[.name] as? String {
// This is for Light, Medium, ExtraLight variants of the font:
selectedFont = name
return
}
}
selectedFont = "cancel"
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
selectedDirectory = "cancelled"
}
func pickFolder() {
// https://developer.apple.com/documentation/uikit/view_controllers/providing_access_to_directories
documentPicker.allowsMultipleSelection = true
documentPicker.delegate = self
let rootVC = self.window?.rootViewController
// Set the initial directory.
// documentPicker.directoryURL = URL(fileURLWithPath: FileManager().default.currentDirectoryPath)
// Present the document picker.
selectedDirectory = ""
DispatchQueue.main.async {
rootVC?.present(self.documentPicker, animated: true, completion: nil)
}
while (selectedDirectory == "") { } // wait until a directory is selected, for Shortcuts.
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
// Present the Document View Controller for the first document that was picked.
// If you support picking multiple items, make sure you handle them all.
let newDirectory = urls[0]
// NSLog("changing directory to: \(newDirectory.path.replacingOccurrences(of: " ", with: "\\ "))")
let isReadableWithoutSecurity = FileManager().isReadableFile(atPath: newDirectory.path)
let isSecuredURL = newDirectory.startAccessingSecurityScopedResource()
let isReadable = FileManager().isReadableFile(atPath: newDirectory.path)
guard isSecuredURL && isReadable else {
showAlert("Error", message: "Could not access folder.")
selectedDirectory = newDirectory.path
return
}
// If it's on iCloud, download the directory content
if (!downloadRemoteFile(fileURL: newDirectory)) {
if (isSecuredURL) {
newDirectory.stopAccessingSecurityScopedResource()
}
NSLog("Couldn't download \(newDirectory), stopAccessingSecurityScopedResource")
selectedDirectory = newDirectory.path
return
}
// Store two things at the App level:
// - the bookmark for the URL
// - a nickname for the bookmark (last component of the URL)
// The user can edit the nickname later.
// the bookmark is only stored once, the nickname is stored each time:
if (!isReadableWithoutSecurity) {
storeBookmark(fileURL: newDirectory)
}
storeName(fileURL: newDirectory, name: newDirectory.lastPathComponent)
// Call cd_main instead of ios_system("cd dir") to avoid closing streams.
changeDirectory(path: newDirectory.path) // call cd_main and checks secured bookmarked URLs
if (newDirectory.path != currentDirectory) {
previousDirectory = currentDirectory
currentDirectory = newDirectory.path
}
selectedDirectory = newDirectory.path
}
func play_media(arguments: [String]?) -> Int32 {
guard (arguments != nil) else { return -1 }
guard (arguments!.count >= 2) else { return -1 } // There must be at least one file
// copy arguments:
let path = arguments![1]
if (FileManager().fileExists(atPath: path)) {
let url = URL(fileURLWithPath: path)
// Create an AVPlayer, passing it the HTTP Live Streaming URL.
avplayer = AVPlayer(url: url)
// Create a new AVPlayerViewController and pass it a reference to the player.
avcontroller = AVPlayerViewController()
guard (avcontroller != nil) else {
return -1
}
guard (avplayer != nil) else {
return -1
}
avcontroller!.delegate = self
avControllerPiPEnabled = false
avplayer?.allowsExternalPlayback = true
// Do we have a title?
let asset = AVAsset(url: url)
let metadata = asset.commonMetadata
let titleID = AVMetadataIdentifier.commonIdentifierTitle
let titleItems = AVMetadataItem.metadataItems(from: metadata,
filteredByIdentifier: titleID)
if (titleItems.count == 0) {
// No title present, let's use the file name:
let titleMetadata = AVMutableMetadataItem()
titleMetadata.identifier = AVMetadataIdentifier.commonIdentifierTitle
titleMetadata.locale = NSLocale.current
titleMetadata.value = arguments![1] as (NSCopying & NSObjectProtocol)?
avplayer?.currentItem?.externalMetadata = [titleMetadata]
}
avcontroller!.player = avplayer
// Modally present the player and call the player's play() method when complete.
let rootVC = self.window?.rootViewController
DispatchQueue.main.async {
rootVC?.present(self.avcontroller!, animated: true) {
self.avplayer!.play()
}
}
return 0
} else {
// File not found.
if !path.hasPrefix("-") {
fputs("play: file " + path + "not found\n", thread_stderr)
}
fputs("usage: play file\n", thread_stderr)
return -1
}
}
func preview(arguments: [String]?) -> Int32 {
guard (arguments != nil) else { return -1 }
guard (arguments!.count >= 2) else { return -1 } // There must be at least one command
// copy arguments:
let path = arguments![1]
if (FileManager().fileExists(atPath: path)) {
let url = URL(fileURLWithPath: path)
let preview = UIDocumentInteractionController(url: url)
preview.delegate = self
DispatchQueue.main.async {
preview.presentPreview(animated: true)
}
return 0
} else {
// File not found.
if !path.hasPrefix("-") {
fputs("view: file " + path + "not found\n", thread_stderr)
}
fputs("usage: view file\n", thread_stderr)
return -1
}
}
func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController {
let rootVC = self.window?.rootViewController
if (rootVC == nil) {
return self
} else {
return rootVC!
}
}
// Even if Caps-Lock is activated, send lower case letters.
@objc func insertKey(_ sender: UIKeyCommand) {
guard (sender.input != nil) else { return }
// This function only gets called if we are in a notebook, in edit_mode:
// Only remap the keys if we are in a notebook, editing cell:
webView?.evaluateJavaScript("window.term_.io.onVTKeystroke(\"" + sender.input! + "\");") { (result, error) in
if error != nil {
// print(error)
}
if (result != nil) {
// print(result)
}
}
}
func executeCommand(command: String) {
NSLog("executeCommand: \(command)")
// There are 2 commands that are called directly, before going to ios_system(), because they need to.
// We still allow them to be aliased.
// We can't call exit through ios_system because it creates a new session
// Also, we want to call it as soon as possible in case something went wrong
let arguments = command.components(separatedBy: " ")
let actualCommand = aliasedCommand(arguments[0])
if (actualCommand == "exit") {
closeWindow()
// If we're here, closeWindow did not work. Clear window:
// Calling "exit(0)" here results in a major crash (I tried).
let infoCommand = "window.term_.wipeContents() ; window.printedContent = ''; window.term_.io.print('" + self.escape + "[2J'); window.term_.io.print('" + self.escape + "[1;1H'); "
self.webView?.evaluateJavaScript(infoCommand) { (result, error) in
if error != nil {
print(error)
}
if (result != nil) {
print(result)
}
}
// Also reset directory:
if (resetDirectoryAfterCommandTerminates != "") {
// NSLog("Calling resetDirectoryAfterCommandTerminates in exit to \(resetDirectoryAfterCommandTerminates)")
changeDirectory(path: self.resetDirectoryAfterCommandTerminates)
changeDirectory(path: self.resetDirectoryAfterCommandTerminates)
self.resetDirectoryAfterCommandTerminates = ""
} else {
let documentsUrl = try! FileManager().url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true)
changeDirectory(path: documentsUrl.path)
changeDirectory(path: documentsUrl.path)
}
printPrompt()
return
} // exit()
if (!command.contains("\n")) {
// save command in history. This duplicates the history array in hterm.html.
// We don't store multi-line commands in history, as they create issues.
if (history.last != command) {
// only store command if different from last command
history.append(command)
}
while (history.count > 100) {
// only keep the last 100 commands
history.removeFirst()
}
}
// Can't create/close windows through ios_system, because it creates/closes a new session.