Jelajahi Sumber

Merge branch 'master' into marcin/reorganize

Marcin Krzyzanowski 5 tahun lalu
induk
melakukan
af67bea515

+ 1 - 1
Makefile

@@ -8,4 +8,4 @@ build-fuzzer:
 	(SWIFT_EXEC=/Library/Developer/Toolchains/swift-latest.xctoolchain/usr/bin/swiftc swift build -Xswiftc "-sanitize=fuzzer" -Xswiftc "-parse-as-library")
 
 run-fuzzer:
-	swift run SwiftTermFuzz fuzzer-corpus -rss_limit_mb=40480 -jobs=12
+	swift run SwiftTermFuzz SwiftTermFuzzerCorpus -rss_limit_mb=40480 -jobs=12

+ 6 - 3
Package.swift

@@ -1,7 +1,4 @@
 // swift-tools-version:5.1
-// IMPORTANT: Remember to update ../Package.swift when modify this file!
-
-// This file is located in this directory to satisfy Xcode assumptions.
 
 import PackageDescription
 
@@ -13,6 +10,7 @@ let package = Package(
     ],
     products: [
         .executable(name: "SwiftTermFuzz", targets: ["SwiftTermFuzz"]),
+        //.executable(name: "CaptureOutput", targets: ["CaptureOutput"]),
         .library(
             name: "SwiftTerm",
             targets: ["SwiftTerm"]
@@ -30,6 +28,11 @@ let package = Package(
             dependencies: ["SwiftTerm"],
             path: "Sources/SwiftTermFuzz"
         ),
+//        .target (
+//            name: "CaptureOutput",
+//            dependencies: ["SwiftTerm"],
+//            path: "Sources/CaptureOutput"
+//        ),        
         .testTarget(
             name: "SwiftTermTests",
             dependencies: ["SwiftTerm"],

+ 96 - 0
Sources/CaptureOutput/main.swift

@@ -0,0 +1,96 @@
+//
+//  main.swift
+//
+// Capture output is used to capture the output of assorted commands that
+// are fed to the application.   Used to augment the fuzzing tests
+//
+//  Created by Miguel de Icaza on 4/24/20.
+//
+
+import Foundation
+import SwiftTerm
+
+public class CaptureTerminal : TerminalDelegate, LocalProcessDelegate {
+    var data: [UInt8] = []
+    public private(set) var terminal: Terminal!
+    var process: LocalProcess!
+    var onEnd: (_ exitCode: Int32?) -> ()
+    var dir: String?
+    
+    public init (queue: DispatchQueue? = nil, options: TerminalOptions = TerminalOptions.default, onEnd: @escaping (_ exitCode: Int32?) -> ())
+    {
+        self.onEnd = onEnd
+        terminal = Terminal(delegate: self, options: options)
+        process = LocalProcess(delegate: self, dispatchQueue: queue)
+    }
+    
+    public func processTerminated(_ source: LocalProcess, exitCode: Int32?) {
+        onEnd (exitCode)
+    }
+    
+    public func dataReceived(slice: ArraySlice<UInt8>) {
+        //print (String (bytes: slice, encoding: .utf8))
+        terminal.feed(buffer: slice)
+        data.append(contentsOf: slice)
+    }
+    
+    func send(data: ArraySlice<UInt8>) {
+        process.send (data: data)
+    }
+
+    func send(_ text: String) {
+        send (data: ([UInt8] (text.utf8))[...])
+        
+    }
+
+    public func send(source: Terminal, data: ArraySlice<UInt8>) {
+        send (data: data)
+    }
+    
+
+    public func getWindowSize() -> winsize {
+        return winsize(ws_row: UInt16(terminal.rows), ws_col: UInt16(terminal.cols), ws_xpixel: UInt16 (16), ws_ypixel: UInt16 (16))
+    }
+    
+    public func mouseModeChanged(source: Terminal) {
+    }
+
+    public func hostCurrentDirectoryUpdated(source: Terminal) {
+        dir = source.hostCurrentDirectory
+    }
+}
+
+
+func runAndCapture (strings: [String], name: String, delay: Int = 1)
+{
+    let queue = DispatchQueue(label: "Runner", qos: .userInteractive, attributes: .concurrent, autoreleaseFrequency: .inherit, target: nil)
+
+    let h = CaptureTerminal (queue: queue) { exitCode in }
+    h.process.startProcess(executable: "/bin/bash", args: [], environment: nil)
+    
+    for line in strings {
+        h.send (line)
+        // 2 seconds
+        Thread.sleep(forTimeInterval: Double (delay))
+    }
+    let data = Data (h.data)
+    FileManager.default.createFile(atPath: name, contents: data, attributes:nil)
+}
+
+runAndCapture (strings: ["mc\n", "\u{1b}0"], name: "/tmp/mc.output")
+runAndCapture (strings: ["emacs /Users/miguel/cvs/SwiftTermFuzzerCorpus/UTF-8-demo.txt\n",
+                         "\u{16}\u{16}\u{16}\u{16}\u{16}",
+                         "\u{16}\u{16}\u{16}\u{16}\u{16}",
+                         "\u{18}\u{3}"], name: "/tmp/emacs.output")
+runAndCapture(strings: ["vim  -u /tmp/scroll.vim -c ':call AutoScroll(100)'  /Users/miguel/cvs/SwiftTermFuzzerCorpus/UTF-8-demo.txt\n",
+                        "",
+                        ""],
+              
+              name: "/tmp/vim.autoscroll.output")
+runAndCapture(strings: ["vim -u /tmp/scroll.vim -c ':call AutoWindowScroll(10)'  /Users/miguel/cvs/SwiftTermFuzzerCorpus/UTF-8-demo.txt\n",
+                        "",
+                        ""],
+              
+              name: "/tmp/vim2.autoscroll.output")
+runAndCapture (strings: ["~/bin/esb\n"], name: "/tmp/esctest.output", delay: 20)
+

+ 8 - 7
Sources/SwiftTerm/Apple/AppleTerminalView.swift

@@ -151,7 +151,7 @@ extension TerminalView {
         return CellDimension(width: cellWidth, height: cellHeight)
     }
     
-    func mapColor (color: Attribute.Color, isFg: Bool) -> TTColor
+    func mapColor (color: Attribute.Color, isFg: Bool, isBold: Bool) -> TTColor
     {
         switch color {
         case .defaultColor:
@@ -171,7 +171,7 @@ extension TerminalView {
                 return c
             }
             
-            let tcolor = Color.defaultAnsiColors [Int (ansi)]
+            let tcolor = Color.defaultAnsiColors [Int (ansi) + (isBold ? 8 : 0)]
             
             let newColor = TTColor.make (red: CGFloat (tcolor.red) / 255.0,
                                          green: CGFloat (tcolor.green) / 255.0,
@@ -219,7 +219,8 @@ extension TerminalView {
         }
         
         var font: TTFont
-        if flags.contains (.bold){
+        let isBold = flags.contains(.bold)
+        if isBold {
             if flags.contains (.italic) {
                 font = options.font.boldItalic
             } else {
@@ -231,11 +232,11 @@ extension TerminalView {
             font = options.font.normal
         }
         
-        let fgColor = mapColor (color: fg, isFg: true)
+        let fgColor = mapColor (color: fg, isFg: true, isBold: isBold)
         var nsattr: [NSAttributedString.Key:Any] = [
             .font: font,
             .foregroundColor: fgColor,
-            .backgroundColor: mapColor(color: bg, isFg: false)
+            .backgroundColor: mapColor(color: bg, isFg: false, isBold: false)
         ]
         if flags.contains (.underline) {
             nsattr [.underlineColor] = fgColor
@@ -687,7 +688,7 @@ extension TerminalView {
             // do the display update
             updateDisplay (notifyAccessibility: notifyAccessibility)
             //selectionView.notifyScrolled(source: terminal)
-            delegate?.scrolled (source: self, position: scrollPosition)
+            terminalDelegate?.scrolled (source: self, position: scrollPosition)
             updateScroller()
             setNeedsDisplay(frame)
         }
@@ -752,7 +753,7 @@ extension TerminalView {
     public func send(data: ArraySlice<UInt8>)
     {
         ensureCaretIsVisible ()
-        delegate?.send (source: self, data: data)
+        terminalDelegate?.send (source: self, data: data)
     }
     
     /**

+ 4 - 0
Sources/SwiftTerm/Buffer.swift

@@ -192,6 +192,10 @@ class Buffer {
     public func getChar (at: Position) -> CharData
     {
         let bufferRow = lines [at.row]
+        let col = at.col
+        if col >= bufferRow.count || col < 0 {
+            return CharData.Null
+        }
         return bufferRow [at.col]
     }
     

+ 1 - 1
Sources/SwiftTerm/CharData.swift

@@ -243,7 +243,7 @@ public struct CharData {
     var unused: UInt8 // Purely here to align to 16 bytes
     
     /// The color and character attributes for the cell
-    var attribute: Attribute
+    public var attribute: Attribute
     
     /// Initializes a new instance of the CharData structure with the provided attribute, character and the dimension
     /// - Parameter attribute: an attribute containing the color and style attributes for the cell

+ 104 - 23
Sources/SwiftTerm/Colors.swift

@@ -10,33 +10,114 @@ import Foundation
 
 class Color {
     public var red, green, blue: UInt8
-    static var defaultAnsiColors: [Color] = setupDefaultAnsiColors ()
+    static var defaultAnsiColors: [Color] = setupDefaultAnsiColors (initialColors: miguelColors)
     static var defaultForeground = Color (red: 0xff, green: 0xff, blue: 0xff)
     static var defaultBackground = Color (red: 0, green: 0, blue: 0)
     
-    static func setupDefaultAnsiColors () -> [Color]
+    static let paleColors: [Color] = [
+        // dark colors
+        Color (red: 0x2e, green: 0x34, blue: 0x36),
+        Color (red: 0xcc, green: 0x00, blue: 0x00),
+        Color (red: 0x4e, green: 0x9a, blue: 0x06),
+        Color (red: 0xc4, green: 0xa0, blue: 0x00),
+        Color (red: 0x34, green: 0x65, blue: 0xa4),
+        Color (red: 0x75, green: 0x50, blue: 0x7b),
+        Color (red: 0x06, green: 0x98, blue: 0x9a),
+        Color (red: 0xd3, green: 0xd7, blue: 0xcf),
+        
+        // bright colors
+        Color (red: 0x55, green: 0x57, blue: 0x53),
+        Color (red: 0xef, green: 0x29, blue: 0x29),
+        Color (red: 0x8a, green: 0xe2, blue: 0x34),
+        Color (red: 0xfc, green: 0xe9, blue: 0x4f),
+        Color (red: 0x72, green: 0x9f, blue: 0xcf),
+        Color (red: 0xad, green: 0x7f, blue: 0xa8),
+        Color (red: 0x34, green: 0xe2, blue: 0xe2),
+        Color (red: 0xee, green: 0xee, blue: 0xec)
+    ]
+    
+    static let vgaColors: [Color] = [
+        // dark colors
+        Color (red: 0, green: 0, blue: 0),
+        Color (red: 170, green: 0, blue: 0),
+        Color (red: 0, green: 170, blue: 0),
+        Color (red: 170, green: 85, blue: 0),
+        Color (red: 0, green: 0, blue: 170),
+        Color (red: 170, green: 0, blue: 170),
+        Color (red: 0, green: 170, blue: 170),
+        Color (red: 170, green: 170, blue: 170),
+        Color (red: 85, green: 85, blue: 85),
+        Color (red: 255, green: 85, blue: 85),
+        Color (red: 85, green: 255, blue: 85),
+        Color (red: 255, green: 255, blue: 85),
+        Color (red: 85, green: 85, blue: 255),
+        Color (red: 255, green: 85, blue: 255),
+        Color (red: 85, green: 255, blue: 255),
+        Color (red: 255, green: 255, blue: 255),
+    ]
+    
+    static let terminalAppColors: [Color] = [
+        Color (red: 0, green: 0, blue: 0),
+        Color (red: 194, green: 54, blue: 33),
+        Color (red: 37, green: 188, blue: 36),
+        Color (red: 173, green: 173, blue: 39),
+        Color (red: 73, green: 46, blue: 225),
+        Color (red: 211, green: 56, blue: 211),
+        Color (red: 51, green: 187, blue: 200),
+        Color (red: 203, green: 204, blue: 205),
+        Color (red: 129, green: 131, blue: 131),
+        Color (red: 252, green: 57, blue: 31),
+        Color (red: 49, green: 231, blue: 34),
+        Color (red: 234, green: 236, blue: 35),
+        Color (red: 88, green: 51, blue: 255),
+        Color (red: 249, green: 53, blue: 248),
+        Color (red: 20, green: 240, blue: 240),
+        Color (red: 233, green: 235, blue: 235),
+    ]
+    
+    
+    static let xtermColors: [Color] = [
+        Color (red: 0, green: 0, blue: 0),
+        Color (red: 205, green: 0, blue: 0),
+        Color (red: 0, green: 205, blue: 0),
+        Color (red: 205, green: 205, blue: 0),
+        Color (red: 0, green: 0, blue: 238),
+        Color (red: 205, green: 0, blue: 205),
+        Color (red: 0, green: 205, blue: 205),
+        Color (red: 229, green: 229, blue: 229),
+        Color (red: 127, green: 127, blue: 127),
+        Color (red: 255, green: 0, blue: 0),
+        Color (red: 0, green: 255, blue: 0),
+        Color (red: 255, green: 255, blue: 0),
+        Color (red: 92, green: 92, blue: 255),
+        Color (red: 255, green: 0, blue: 255),
+        Color (red: 0, green: 255, blue: 255),
+        Color (red: 255, green: 255, blue: 255),
+    ]
+    
+    static let miguelColors: [Color] = [
+        Color (red: 0, green: 0, blue: 0),
+        Color (red: 153, green: 0, blue: 1),
+        Color (red: 0, green: 166, blue: 3),
+        Color (red: 153, green: 153, blue: 0),
+        Color (red: 3, green: 0, blue: 178),
+        Color (red: 178, green: 0, blue: 178),
+        Color (red: 0, green: 165, blue: 178),
+        Color (red: 191, green: 191, blue: 191),
+        Color (red: 138, green: 137, blue: 138),
+        Color (red: 229, green: 0, blue: 1),
+        Color (red: 0, green: 216, blue: 0),
+        Color (red: 229, green: 229, blue: 0),
+        Color (red: 7, green: 0, blue: 254),
+        Color (red: 229, green: 0, blue: 229),
+        Color (red: 0, green: 229, blue: 229),
+        Color (red: 229, green: 229, blue: 229),
+    ]
+    
+    static func setupDefaultAnsiColors (initialColors: [Color]) -> [Color]
     {
-        var colors: [Color] = [
-            // dark colors
-            Color (red: 0x2e, green: 0x34, blue: 0x36),
-            Color (red: 0xcc, green: 0x00, blue: 0x00),
-            Color (red: 0x4e, green: 0x9a, blue: 0x06),
-            Color (red: 0xc4, green: 0xa0, blue: 0x00),
-            Color (red: 0x34, green: 0x65, blue: 0xa4),
-            Color (red: 0x75, green: 0x50, blue: 0x7b),
-            Color (red: 0x06, green: 0x98, blue: 0x9a),
-            Color (red: 0xd3, green: 0xd7, blue: 0xcf),
-            
-            // bright colors
-            Color (red: 0x55, green: 0x57, blue: 0x53),
-            Color (red: 0xef, green: 0x29, blue: 0x29),
-            Color (red: 0x8a, green: 0xe2, blue: 0x34),
-            Color (red: 0xfc, green: 0xe9, blue: 0x4f),
-            Color (red: 0x72, green: 0x9f, blue: 0xcf),
-            Color (red: 0xad, green: 0x7f, blue: 0xa8),
-            Color (red: 0x34, green: 0xe2, blue: 0xe2),
-            Color (red: 0xee, green: 0xee, blue: 0xec)
-        ]
+        var colors = initialColors
+        
         // Fill in the remaining 240 ANSI colors.
         let v = [ 0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff ];
         

+ 7 - 1
Sources/SwiftTerm/EscapeSequenceParser.swift

@@ -596,7 +596,13 @@ class EscapeSequenceParser {
             if x < 48 || x > 57 {
                 return result
             }
-            result = result * 10 + Int ((x - 48))
+            
+            let newV = result * 10 + Int ((x - 48))
+            let willOverflow =  newV > ((Int.max/10)-10)
+            if willOverflow {
+                return 0
+            }
+            result = newV
         }
         return result
     }

+ 1 - 1
Sources/SwiftTerm/Mac/MacLocalTerminalView.swift

@@ -68,7 +68,7 @@ public class LocalProcessTerminalView: TerminalView, TerminalViewDelegate, Local
 
     func setup ()
     {
-        delegate = self
+        terminalDelegate = self
         process = LocalProcess (delegate: self)
     }
     

+ 8 - 12
Sources/SwiftTerm/Mac/MacTerminalView.swift

@@ -101,7 +101,7 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations {
     /**
      * The delegate that the TerminalView uses to interact with its hosting
      */
-    public weak var delegate: TerminalViewDelegate?
+    public weak var terminalDelegate: TerminalViewDelegate?
     
     var accessibility: AccessibilityService = AccessibilityService()
     var search: SearchService!
@@ -213,7 +213,7 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations {
     }
     
     open func send(source: Terminal, data: ArraySlice<UInt8>) {
-        delegate?.send (source: self, data: data)
+        terminalDelegate?.send (source: self, data: data)
     }
     
     /**
@@ -232,7 +232,7 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations {
     open func scrolled(source terminal: Terminal, yDisp: Int) {
         //selectionView.notifyScrolled(source: terminal)
         updateScroller()
-        delegate?.scrolled(source: self, position: scrollPosition)
+        terminalDelegate?.scrolled(source: self, position: scrollPosition)
     }
     
     open func linefeed(source: Terminal) {
@@ -306,7 +306,7 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations {
             accessibility.invalidate ()
             search.invalidate ()
             
-            delegate?.sizeChanged (source: self, newCols: newCols, newRows: newRows)
+            terminalDelegate?.sizeChanged (source: self, newCols: newCols, newRows: newRows)
             needsDisplay = true
         }
     }
@@ -810,7 +810,7 @@ open class TerminalView: NSView, NSTextInputClient, NSUserInterfaceValidations {
         if event.modifierFlags.contains(.command){
             if let payload = getPayload(for: event) {
                 if let (url, params) = urlAndParamsFrom(payload: payload) {
-                    delegate?.requestOpenLink(source: self, link: url, params: params)
+                    terminalDelegate?.requestOpenLink(source: self, link: url, params: params)
                 }
             }
         }
@@ -1008,11 +1008,11 @@ extension TerminalView: TerminalDelegate {
     }
     
     open func setTerminalTitle(source: Terminal, title: String) {
-        delegate?.setTerminalTitle(source: self, title: title)
+        terminalDelegate?.setTerminalTitle(source: self, title: title)
     }
     
     open func sizeChanged(source: Terminal) {
-        delegate?.sizeChanged(source: self, newCols: source.cols, newRows: source.rows)
+        terminalDelegate?.sizeChanged(source: self, newCols: source.cols, newRows: source.rows)
         updateScroller ()
     }
     
@@ -1040,11 +1040,7 @@ extension NSColor {
 
     static func make (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) -> NSColor
     {
-        return NSColor (
-            calibratedRed: red,
-            green: green,
-            blue: blue,
-            alpha: alpha)
+        return NSColor (deviceRed: red, green: green, blue: blue, alpha: alpha)
     }
 }
 

+ 4 - 1
Sources/SwiftTerm/SelectionService.swift

@@ -271,8 +271,11 @@ class SelectionService {
      * Implements the behavior to select the word at the specified position or an expression
      * which is a balanced set parenthesis, braces or brackets
      */
-    public func selectWordOrExpression (at position: Position, in buffer: Buffer)
+    public func selectWordOrExpression (at uncheckedPosition: Position, in buffer: Buffer)
     {
+        let position = Position(
+            col: max (min (uncheckedPosition.col, buffer.cols-1), 0),
+            row: max (min (uncheckedPosition.row, buffer.rows-1), 0))
         switch buffer.getChar(at: position).getCharacter() {
         case Character(UnicodeScalar(0)):
             simpleScanSelection (from: position, in: buffer) { ch in ch == nullChar }

+ 91 - 12
Sources/SwiftTerm/Terminal.swift

@@ -160,7 +160,7 @@ open class Terminal {
     // Whether the terminal is operating in application keypad mode
     var applicationKeypad : Bool = false
     // Whether the terminal is operating in application cursor mode
-    var applicationCursor : Bool = false
+    public var applicationCursor : Bool = false
     
     // You can ignore most of the defaults set here, the function
     // reset() will do that again
@@ -343,13 +343,13 @@ open class Terminal {
             buffers!.active
         }
     }
-    
-    /// Returns the character at the specified column and row, these are zero-based
+
+    /// Returns the CharData at the specified column and row, these are zero-based
     /// - Parameter col: column to retrieve, starts at 0
     /// - Parameter row: row to retrieve, starts at 0
-    /// - Returns: nil if the col or row are out of bounds, or the Character contained in that cell otherwise
+    /// - Returns: nil if the col or row are out of bounds, or the CharData contained in that cell otherwise
     
-    public func getCharacter (col: Int, row: Int) -> Character?
+    public func getCharData (col: Int, row: Int) -> CharData?
     {
         if row < 0 || row >= rows {
             return nil
@@ -357,7 +357,17 @@ open class Terminal {
         if col < 0 || col >= cols {
             return nil
         }
-        return buffer.lines [row + buffer.yDisp][col].getCharacter()
+        return buffer.lines [row + buffer.yDisp][col]
+    }
+
+    /// Returns the character at the specified column and row, these are zero-based
+    /// - Parameter col: column to retrieve, starts at 0
+    /// - Parameter row: row to retrieve, starts at 0
+    /// - Returns: nil if the col or row are out of bounds, or the Character contained in that cell otherwise
+    
+    public func getCharacter (col: Int, row: Int) -> Character?
+    {
+        return getCharData(col: col, row: row)?.getCharacter()
     }
     
     func setup (isReset: Bool = false)
@@ -608,8 +618,10 @@ open class Terminal {
         //
         // ESC handlers
         //
+        parser.setEscHandler("6", { collect, flags in self.columnIndex (back: true) })
         parser.setEscHandler ("7",  { collect, flag in self.cmdSaveCursor ([], []) })
         parser.setEscHandler ("8",  { collect, flag in self.cmdRestoreCursor ([], []) })
+        parser.setEscHandler ("9",  { collect, flag in self.columnIndex(back: false) })
         parser.setEscHandler ("D",  { collect, flag in self.cmdIndex() })
         parser.setEscHandler ("E",  { collect, flag in self.cmdNextLine () })
         parser.setEscHandler ("H",  { collect, flag in self.cmdTabSet ()})
@@ -938,7 +950,7 @@ open class Terminal {
         // we already made sure above, that buffer.x + chWidth will not overflow right
         if chWidth > 0 {
             chWidth -= 1
-            while chWidth != 0 {
+            while chWidth != 0 && buffer.x < buffer.cols {
                 bufferRow [buffer.x] = empty
                 buffer.x += 1
                 chWidth -= 1
@@ -1252,14 +1264,18 @@ open class Terminal {
     //
     func cmdCursorForward (_ pars: [Int], _ collect: cstring)
     {
-        let param = max (pars.count > 0 ? pars [0] : 1, 1)
+        cursorForward(count: pars.count > 0 ? pars [0] : 1)
+    }
+    
+    func cursorForward (count: Int)
+    {
         var right = marginMode ? buffer.marginRight : cols-1
         
         // When the cursor starts after the right margin, CUF moves to the full width
         if buffer.x > right {
             right = buffer.cols - 1
         }
-        buffer.x += param
+        buffer.x += (max (count, 1))
         if buffer.x > right {
             buffer.x = right
         }
@@ -1271,7 +1287,11 @@ open class Terminal {
     //
     func cmdCursorBackward (_ pars: [Int], _ collect: cstring)
     {
-        let param = max (pars.count > 0 ? pars [0] : 1, 1)
+        cursorBackward(count: pars.count > 0 ? pars [0] : 1)
+    }
+    
+    func cursorBackward (count: Int)
+    {
         let buffer = self.buffer
         
         // What is our left margin - depending on the settings.
@@ -1281,7 +1301,7 @@ open class Terminal {
         if buffer.x < left {
             left = 0
         }
-        let newX = buffer.x - param
+        let newX = buffer.x - max (1, count)
         if newX < left {
                 buffer.x = left
         } else {
@@ -1714,7 +1734,9 @@ open class Terminal {
         }
         //top = min (top, bottom)
         //left = min (left, right)
-        return (top-1, left-1, bottom-1, right-1)
+        let rowBound = rows-1
+        let colBound = cols-1
+        return (min (rowBound, top-1), min (colBound, left-1), min (rowBound, bottom-1), min (colBound, right-1))
     }
     
     //
@@ -3609,6 +3631,19 @@ open class Terminal {
         refreshEnd = -1
     }
     
+    /**
+     * Zero-based (row, column) of cursor location relative to visible part of display.
+     */
+    public func getCursorLocation() -> (Int, Int) {
+        return (buffer.x, buffer.y)
+    }
+    
+    /**
+     * Uppermost visible row.
+     */
+    public func getTopVisibleRow() -> Int {
+        return buffer.yDisp
+    }
     
     // ESC c Full Reset (RIS)
     /// This performs a full reset of the terminal, like a soft reset, but additionally resets the buffer conents and scroll area.
@@ -3624,6 +3659,50 @@ open class Terminal {
         syncScrollArea ()
     }
 
+    // Support for:
+    // ESC 6 Back Index (DECBI) and
+    // ESC 9 Forward Index (DECFI)
+    func columnIndex (back: Bool)
+    {
+        let buffer = self.buffer
+        let x = buffer.x
+        let leftMargin = buffer.marginLeft
+        if back {
+            if x == leftMargin {
+                columnScroll (back: back, at: x)
+            } else {
+                cursorBackward(count: 1)
+            }
+        } else {
+            let rightMargin = buffer.marginRight
+            if x == rightMargin  {
+                columnScroll (back: back, at: leftMargin)
+            } else if x == buffer.cols {
+                // on the boundaries, we ignore, test_DECFI_WholeScreenScrolls
+            } else {
+                cursorForward(count: 1)
+            }
+        }
+    }
+    
+    func columnScroll (back: Bool, at: Int)
+    {
+        if buffer.y < buffer.scrollTop || buffer.y > buffer.scrollBottom || buffer.x < buffer.marginLeft || buffer.x > buffer.marginRight {
+            return
+        }
+        for y in buffer.scrollTop...buffer.scrollBottom {
+            let line = buffer.lines [buffer.yBase + y]
+            if back {
+                line.insertCells(pos: at, n: 1, rightMargin: marginMode ? buffer.marginRight : cols-1, fillData: buffer.getNullCell())
+            } else {
+                line.deleteCells(pos: at, n: 1, rightMargin: marginMode ? buffer.marginRight : cols-1, fillData: buffer.getNullCell(attribute: eraseAttr()))
+            }
+            //line.isWrapped = false
+        }
+        updateRange (buffer.scrollTop)
+        updateRange (buffer.scrollBottom)
+    }
+    
     // ESC D Index (Index is 0x84) - IND
     func cmdIndex ()
     {

+ 13 - 11
Sources/SwiftTerm/iOS/iOSTerminalView.swift

@@ -26,7 +26,7 @@ import CoreGraphics
  * Users are notified of interesting events in their implementation of the `TerminalViewDelegate`
  * methods - an instance must be provided to the constructor of `TerminalView`.
  */
-open class TerminalView: UIView, UITextInputTraits, UIKeyInput {
+open class TerminalView: UIScrollView, UITextInputTraits, UIKeyInput, UIScrollViewDelegate {
     // User facing, customizable view options
     public struct Options {
         
@@ -91,7 +91,7 @@ open class TerminalView: UIView, UITextInputTraits, UIKeyInput {
     /**
      * The delegate that the TerminalView uses to interact with its hosting
      */
-    public weak var delegate: TerminalViewDelegate?
+    public weak var terminalDelegate: TerminalViewDelegate?
     
     var accessibility: AccessibilityService = AccessibilityService()
     var search: SearchService!
@@ -154,11 +154,11 @@ open class TerminalView: UIView, UITextInputTraits, UIKeyInput {
     }
     
     open func bufferActivated(source: Terminal) {
-        //X updateScroller ()
+        updateScroller ()
     }
     
     open func send(source: Terminal, data: ArraySlice<UInt8>) {
-        delegate?.send (source: self, data: data)
+        terminalDelegate?.send (source: self, data: data)
     }
     
     /**
@@ -180,8 +180,8 @@ open class TerminalView: UIView, UITextInputTraits, UIKeyInput {
     
     open func scrolled(source terminal: Terminal, yDisp: Int) {
         //XselectionView.notifyScrolled(source: terminal)
-        //XupdateScroller()
-        delegate?.scrolled(source: self, position: scrollPosition)
+        updateScroller()
+        terminalDelegate?.scrolled(source: self, position: scrollPosition)
     }
     
     open func linefeed(source: Terminal) {
@@ -190,7 +190,9 @@ open class TerminalView: UIView, UITextInputTraits, UIKeyInput {
     
     func updateScroller ()
     {
-        //Xscroller.isEnabled = canScroll
+        contentSize = CGSize (width: CGFloat (terminal.buffer.cols) * cellDimension.width,
+                              height: CGFloat (terminal.buffer.lines.count) * cellDimension.height)
+        // contentOffset = CGPoint (x: 0, y: CGFloat (terminal.buffer.lines.count-terminal.rows)*cellDimension.height)
         //Xscroller.doubleValue = scrollPosition
         //Xscroller.knobProportion = scrollThumbsize
     }
@@ -238,7 +240,7 @@ open class TerminalView: UIView, UITextInputTraits, UIKeyInput {
             accessibility.invalidate ()
             search.invalidate ()
             
-            delegate?.sizeChanged (source: self, newCols: newCols, newRows: newRows)
+            terminalDelegate?.sizeChanged (source: self, newCols: newCols, newRows: newRows)
             setNeedsDisplay (frame)
         }
     }
@@ -438,12 +440,12 @@ extension TerminalView: TerminalDelegate {
     }
   
     open func setTerminalTitle(source: Terminal, title: String) {
-        delegate?.setTerminalTitle(source: self, title: title)
+        terminalDelegate?.setTerminalTitle(source: self, title: title)
     }
   
     open func sizeChanged(source: Terminal) {
-        delegate?.sizeChanged(source: self, newCols: source.cols, newRows: source.rows)
-        //X iOS TODO: updateScroller ()
+        terminalDelegate?.sizeChanged(source: self, newCols: source.cols, newRows: source.rows)
+        updateScroller()
     }
   
     open func setTerminalIconTitle(source: Terminal, title: String) {

+ 19 - 47
Sources/SwiftTermFuzz/main.swift

@@ -45,61 +45,33 @@ func testInput (d: Data)
 func testCrashes ()
 {
     let crashes = [
-        "timeout-a9539e3703587af2fe071ece51e17fa168ac6d2d",
-        "timeout-c7784cb0fcb8cd15fe71cd670e64a8bd6800a499",
-        "crash-dda8a48c04d1461c3b1cf179ae2f6367c8d4ec7b",
-        "crash-98664e18a4536bf5b581833b4316b19d30d1fc50",
-        "crash-dd8d21f5b5b50b1f44c46b7d62079317b5dfba92",
-        "crash-8be177cdaef621d1ca821effcea130e4a0367435",
-        "crash-78efb40b60415603381a78d4658d516daacbf734",
-        "crash-1a406725874a3abfd50d2f0afc5763b942eacb0a",
-        "crash-f7cfa2f5bdd849060e3801853ca4d3b64e0c03b0",
-        "crash-11101292c68ab9046a2d9cbb8590ceaf797eb076",
-        "crash-3fdb4cba0474412d2c3dc07f8d15936b420d7a81",
-        "crash-0c3dd84afd1b451fb0fdd4709851fab1c8082fae",
-        "crash-36b4fd080b9c7dd54b231a9325200dcc9e71a342",
-        "crash-d289c73f90080308c483da206a97ccd3d511c749",
-        "crash-36c36f0f7ad470e606763d49a75736fa0ee04d84",
-        "crash-f6066c221374836a036f30e106489754549966c6",
-        "crash-d214965096e8bbec69a90933332420253acad416",
-        "crash-586179f846efe82d17ab7eac4dd05d37142a1698",
-        "crash-4a6396e7ddc51a06e57a3e4998eb07df7274fd3f",
-        "timeout-c7784cb0fcb8cd15fe71cd670e64a8bd6800a499",
-        "crash-1166668d31fc739b2e728ae818329cedd52f46dd",
-        "crash-89fbe8f483d3870f127aae78034fac288f2ca378",
-        "crash-0bd46f43af414faa30387902d2b9f797d84e4815",
-        "crash-c77af4da2eeae0c8167ae8c1aa00cde34d13365b",
-        "slow-unit-c616385dc739fd46b2cdbe69cbf6d296b648a2c4",
-        "crash-9722bf1001188e1582b36b017d35ede811d930c7",
-        "crash-2cb86239bead50163f9673d077fff9d31d991f76",
-        "crash-05d3371777486e9c21da9edaf37b64852b6a59ac",
-        "crash-11e430733f1cb1dc91db8b2f0bf3fc6d47eae752",
-        "crash-2a107abe9c27809af563e78ec5885a468b17cc85",
-        "crash-40b78df2bca60d1f9bcd04719c671e9fb3ed4ac0",
-        "crash-4ed9cd0097d80a3cd2a03a42f793f8bbcb7cb564",
-        "crash-6eff71f43731490fbf02cfae5603d1c3d87007ac",
-        "crash-8ec6dcd7ed7a5979b553a487333765aae0ca083b",
-        "crash-98ce0e0b8d286505f093cca705ac3e2230d2bd80",
-        "crash-b725370fac397e8db4818587957571938ac47163",
-        "crash-bf4aad6f8ca36da6dfc61ba6a5724ef98df662c5",
-        "crash-cebfabfae04b3fe4a6959b089d87e6b9cfe8708d",
-        "crash-d4dea30dde6d0e9cbd3d8338a34c3b46867bfe19",
-        "crash-dd110df2dae9279f883536052f91751ceb197196",
-        "crash-e34608d8acd5a503bde845ba56cb42004e348b3a",
-        "crash-e7023d5355113a5967bef15c34611e6ec177b312",
-        "crash-f733f8bc1beecddd58e33bbd04fd43cf21a68cd0",
+        "crash-039a0b21c56b1e3a7a51056dd4f8daa9130c7312",
+        "crash-36eb1fbfdb3a61e7b17b166d190ffd85ad9c80ab",
+        "crash-4ba9dc95bc1c5d691fd9e80a4de72d65184e5c56",
+        "crash-59fb9d3b7ab81c1782d26dfc69a962fae49ec449",
+        "crash-64300317b2f97db7bfacfd77ba4d879e9726fd68",
+        "crash-b926cdde789b73ff9680ff9ab643f13fa36c0571",
+        "crash-c1147059ce893629e13289b43ae2b2ad1edcf44f",
+        "crash-de2a0b4222547592208f7f85e2cd5b2730194daa",
+        "crash-e1f2f0f2ef07d6d728316fa1bc336e6d1d699b99",
+        "crash-ec47d21af677ee8eb18f91e150cdfb5d41d931c1",
+
         
     ]
     
     for crash in crashes {
-        let url = URL(fileURLWithPath: "/Users/miguel/cvs/SwiftTerm/results-fuzzer/\(crash)")
+        let url = URL(fileURLWithPath: "/Users/miguel/cvs/SwiftTerm/\(crash)")
+        let data: Data
         do {
             print ("Running test \(crash)")
-            let data = try Data(contentsOf: url)
-            testInput (d: data)
+            data = try Data(contentsOf: url)
         } catch {
-            print ("Caught error in test \(crash)")
+            print ("Caught error loading \(crash)")
+            continue
         }
+
+        testInput (d: data)
+        
         print ("passed crash \(crash)")
     }
     print ("Happy!")

+ 41 - 0
Tests/SwiftTermTests/SelectionTests.swift

@@ -0,0 +1,41 @@
+//
+//  File.swift
+//  
+//
+//  Created by Miguel de Icaza on 4/29/20.
+//
+
+import Foundation
+import XCTest
+
+@testable import SwiftTerm
+
+final class SelectionTests: XCTestCase, TerminalDelegate {
+    func send(source: Terminal, data: ArraySlice<UInt8>) {
+        print ("here")
+    }
+    
+    func testDoesNotCrashWhenSelectingWordOrExpressionOutsideColumnRange ()
+    {
+        let terminal = Terminal(delegate: self, options: TerminalOptions (cols: 10, rows: 10))
+        let selection = SelectionService(terminal: terminal)
+        terminal.feed (text: "1234567890")
+        
+        // depending on the size of terminal view, there might be a space near the margin where the user
+        // clicks which might result in a col or row outside the bounds of terminal,
+        selection.selectWordOrExpression(at: Position(col: -1, row: 0), in: terminal.buffer)
+        selection.selectWordOrExpression(at: Position(col: 11, row: 0), in: terminal.buffer)
+    }
+    
+    func testDoesNotCrashWhenSelectingWordOrExpressionOutsideRowRange ()
+    {
+        let terminal = Terminal(delegate: self, options: TerminalOptions (cols: 10, rows: 10))
+        let selection = SelectionService(terminal: terminal)
+        terminal.feed (text: "1234567890")
+
+        // depending on the size of terminal view, there might be a space near the margin where the user
+        // clicks which might result in a col or row outside the bounds of terminal,
+        selection.selectWordOrExpression(at: Position (col: 0, row: -1), in: terminal.buffer)
+
+    }
+}

+ 5 - 13
Tests/SwiftTermTests/SwiftTermTests.swift

@@ -57,7 +57,7 @@ final class SwiftTermTests: XCTestCase {
     func testKnownGood() {
         let good = [
             "BS", "CAT", "CHA", "CHT", "CNL", "CPL", "CR", "CUB", "CUD", "CUF", "CUP", "CUU",
-            "DCH", "DCS", "DECDC", "DECDSR", "DECERA", "DECFRA", "DECIC", "DECSTBM", "DECSTR", "DL",
+            "DCH", "DCS", "DECBI", "DECDC", "DECDSR", "DECERA", "DECFRA", "DECIC", "DECSTBM", "DECSTR", "DL",
             "FF", "HPR", "HTS", "HVP", "ICH", "IL", "LF",
             "PM", "REP", "RM", "SM", "SOS", "SU", "TBC", "VPR", "VT",
         
@@ -68,14 +68,6 @@ final class SwiftTermTests: XCTestCase {
                 // Failing:
                 // test_DECALN_ClearsMargin
             
-            // DECBI, 1 passes, 4 fail
-            "DECBI_NoWrapOnLeftEdge",
-                // Failing:
-                // test_DECBI_Basic
-                // test_DECBI_LeftOfMargin
-                // test_DECBI_Scroll
-                // test_DECBI_WholeScreenScroll
-            
             // DECCRA, 8 pass, 2 fail
             "DECCRA_cursorDoesNotMove", "DECCRA_defaultValuesInDest", "DECCRA_defaultValuesInSource",
             "DECCRA_destinationPartiallyOffscreen", "DECCRA_ignoresMargins", "DECCRA_invalidSourceRectDoesNothing",
@@ -84,12 +76,12 @@ final class SwiftTermTests: XCTestCase {
                 // test_DECCRA_overlyLargeSourceClippedToScreenSize
                 // test_DECCRA_respectsOriginMode
             
-            // DECFI, 1 pass, 6 fail
+            // DECFI, 4 pass, 1 fail
             "DECFI_NoWrapOnRightEdge",
+            "DECFI_Basic",
+            "DECFI_RightOfMargin",
+            "DECFI_Scroll",
                 // Failing:
-                // test_DECFI_Basic
-                // test_DECFI_RightOfMargin
-                // test_DECFI_Scroll
                 // test_DECFI_WholeScreenScroll
             
             // DECRQSS, 4 pass, 2 fail