Selaa lähdekoodia

Assorted porting, assorted bug fixes

Miguel de Icaza 5 vuotta sitten
vanhempi
commit
b41e22027e

+ 1 - 1
SwiftTerm/Sources/SwiftTerm/Buffer.swift

@@ -726,11 +726,11 @@ class Buffer {
             var countInsertedSoFar = 0
             var i = min (lines.maxLength - 1, originalLinesLength + countToInsert - 1)
             while i >= 0 {
+                defer { i = i-1 }
                 if !nextToInsert.isNull && nextToInsert.start > originalLineIndex + countInsertedSoFar {
                         // Insert extra lines here, adjusting i as needed
                     for nexti in (0..<nextToInsert.lines.count).reversed() {
                         lines [i] = nextToInsert.lines [nexti]
-                        i -= 1
                     }
 
                     i += 1

+ 3 - 2
SwiftTerm/Sources/SwiftTerm/CharSets.swift

@@ -9,7 +9,7 @@
 import Foundation
 
 public class CharSets {
-    public static var all: [UInt8:[UInt8:String]?] = initAll ()
+    public static var all: [UInt8:[UInt8:String]] = initAll ()
     
     // This is the "B" charset, null
     public static var defaultCharset: [UInt8:String]? = nil
@@ -78,7 +78,8 @@ public class CharSets {
          * United States character set
          * ESC (B
          */
-        all [Character("B").asciiValue!] = nil
+        all [Character("B").asciiValue!] = [:]
+        
         /**
         * Dutch character set
         * ESC (4

+ 93 - 0
SwiftTerm/Sources/SwiftTerm/Line.swift

@@ -0,0 +1,93 @@
+//
+//  Line.swift
+//  
+//
+//  Created by Miguel de Icaza on 3/13/20.
+//
+
+import Foundation
+
+struct LineFragment {
+    var text: String?
+    var line: Int
+    var location: Int
+    var length: Int
+    
+    static func newLine (line: Int) -> LineFragment
+    {
+        LineFragment(text: "\n", line: line, location: -1, length: 1)
+    }
+}
+
+class Line : CustomDebugStringConvertible {
+    var fragments: [LineFragment] = []
+    
+    public init ()
+    {
+    }
+    
+    // gets the line number of the first fragment
+    var startLine: Int? {
+        get {
+            return fragments.first?.line ?? nil
+        }
+    }
+    
+    var startLocation: Int? {
+        get {
+            fragments.first?.location ?? nil
+        }
+    }
+    
+    private(set) var length: Int = 0
+    
+    public var debugDescription: String {
+        get {
+            if fragments.count == 0 {
+                return "[]"
+            }
+            var result = "\(fragments.count)/\(length): ["
+            for fragment in fragments {
+                if fragment.text == "\n" {
+                    result += "\\n"
+                } else {
+                    result += fragment.text ?? ""
+                }
+                result += "]["
+            }
+            return result
+        }
+    }
+    
+    func add (fragment: LineFragment)
+    {
+        fragments.append(fragment)
+        length += fragment.length
+    }
+    
+    func toString () -> String
+    {
+        var result = ""
+        for x in fragments {
+            result += x.text ?? ""
+        }
+        return result
+    }
+    
+    func getFragmentIndex (forPosition: Int) -> Int
+    {
+        var count = 0
+        for i in 0..<fragments.count {
+            count += fragments[i].length
+            if count > forPosition {
+                return i
+            }
+        }
+        return fragments.count - 1
+    }
+    
+    subscript (idx: Int) -> LineFragment {
+        return fragments [idx]
+    }
+    
+}

+ 45 - 9
SwiftTerm/Sources/SwiftTerm/MacTerminalView.swift

@@ -46,6 +46,10 @@ public protocol TerminalViewDelegate {
  * wiring this up to a pseudo-terminal.
  */
 public class TerminalView: NSView, TerminalDelegate, NSTextInputClient, NSUserInterfaceValidations {
+    public func selectionChanged(source: Terminal) {
+        abort()
+    }
+    
     public func setTerminalIconTitle(source: Terminal, title: String) {
         //
     }
@@ -66,7 +70,7 @@ public class TerminalView: NSView, TerminalDelegate, NSTextInputClient, NSUserIn
     var search: SearchService!
     
     var selectionView: SelectionView!
-    var selection: SelectionService = SelectionService ()
+    var selection: SelectionService!
     var scroller: NSScroller!
     
     public override init (frame: CGRect)
@@ -95,6 +99,8 @@ public class TerminalView: NSView, TerminalDelegate, NSTextInputClient, NSUserIn
         terminal = Terminal(delegate: self, options: options)
         fullBufferUpdate ()
         
+        selection = SelectionService (terminal: terminal)
+        
         caretView = CaretView (frame: CGRect (x: 0, y: cellDelta, width: cellWidth, height: cellHeight))
         caretView.focused = false
         
@@ -114,7 +120,24 @@ public class TerminalView: NSView, TerminalDelegate, NSTextInputClient, NSUserIn
     @objc
     func scrollerActivated ()
     {
-        
+        switch scroller.hitPart {
+        case .decrementPage:
+            pageUp()
+            scroller.doubleValue =  scrollPosition
+        case .incrementPage:
+            pageDown()
+            scroller.doubleValue =  scrollPosition
+        case .knob:
+            scroll(toPosition: scroller.doubleValue)
+        case .knobSlot:
+            print ("Scroller .knobSlot clicked")
+        case .noPart:
+            print ("Scroller .noPart clicked")
+        case .decrementLine:
+            print ("Scroller .decrementLine clicked")
+        case .incrementLine:
+            print ("Scroller .incrementLine clicked")
+        }
     }
     
     func getScrollerFrame (_ terminalFrame: CGRect) -> CGRect
@@ -716,17 +739,30 @@ public class TerminalView: NSView, TerminalDelegate, NSTextInputClient, NSUserIn
                 let arr = [UInt8](ch.utf8)
                 if arr.count == 1 {
                     let ch = Character (UnicodeScalar (arr [0]))
-                    
-                    let d = ch.uppercased ()
-                    if d >= "A" && d <= "Z" {
-                        let ch2 = d.first!
-                        
-                        send ([ (ch2.asciiValue! - 0x40 /* - 'A' + 1 */) ])
+                    var value: UInt8
+                    switch ch {
+                    case "A"..."Z":
+                        value = (ch.asciiValue! - 0x40 /* - 'A' + 1 */)
+                    case "a"..."z":
+                        value = (ch.asciiValue! - 0x60 /* - 'a' + 1 */)
+                    case "\\":
+                        value = 0x1c
+                    case "_":
+                        value = 0x1f
+                    case "]":
+                        value = 0x1d
+                    case "[":
+                        value = 0x1b
+                    case "^":
+                        value = 0x1e
+                    default:
+                        return
                     }
+                    send ([value])
                     return
                 }
             }
-        } else if eventFlags.contains (.function) && false {
+        } else if eventFlags.contains (.function) {
             if let str = event.charactersIgnoringModifiers {
                 if let fs = str.unicodeScalars.first {
                     let c = Int (fs.value)

+ 12 - 0
SwiftTerm/Sources/SwiftTerm/Position.swift

@@ -0,0 +1,12 @@
+//
+//  Position.swift
+//  
+//
+//  Created by Miguel de Icaza on 3/13/20.
+//
+
+import Foundation
+
+struct Position {
+    var col, row: Int
+}

+ 280 - 2
SwiftTerm/Sources/SwiftTerm/SelectionService.swift

@@ -9,9 +9,287 @@
 import Foundation
 
 class SelectionService {
-    public var active: Bool = false
-    public init ()
+    var terminal: Terminal
+    
+    public init (terminal: Terminal)
+    {
+        self.terminal = terminal
+        _active = false
+        start = Position(col: 0, row: 0)
+        end = Position(col: 0, row: 0)
+    }
+    
+    /**
+     * Controls whether the selection is active or not
+     */
+    var _active: Bool = false
+    public var active: Bool {
+        get {
+            return _active
+        }
+        set(newValue) {
+            let emit = newValue != _active
+            _active = newValue
+            if emit {
+                terminal.tdel.selectionChanged (source: terminal)
+            }
+        }
+    }
+    
+    /**
+     * Returns the selection starting point in buffer coordinates
+     */
+    public private(set) var start: Position
+
+    /**
+     * Returns the selection ending point in buffer coordinates
+     */
+    public private(set) var end: Position
+    
+    /**
+     * Starts the selection from the specific location
+     */
+    public func startSelection (row: Int, col: Int)
+    {
+        setSoftStart(row: row, col: col)
+        active = true
+    }
+    
+    /**
+     * Starts selection, the range is determined by the last start position
+     */
+    public func startSelection ()
+    {
+        end = start
+    }
+    
+    /**
+     * Sets the start and end positions but does not start selection
+     * this lets us record the last position of mouse clicks so that
+     * drag and shift+click operations know from where to start selection
+     * from
+     */
+    public func setSoftStart (row: Int, col: Int)
+    {
+        let p = Position(col: col, row: row)
+        start = p
+        end = p
+    }
+    
+    enum compareResult {
+        case before
+        case after
+        case equal
+    }
+    // Compares two positions for ordering
+    // -1 a comes before b
+    //  1 a comes after b
+    //  0 a and b are the same
+    func compare (_ a: Position, _ b: Position) -> compareResult
+    {
+        if a.row < b.row { return .before }
+        if a.row > b.row { return .after }
+        // a and b are on the same row, compare columns
+        if a.col < b.col { return .before }
+        if a.col > b.col { return .after }
+        return .equal
+    }
+    /**
+     * Extends the selection based on the user "shift" clicking. This has
+     * slightly different semantics than a "drag" extension because we can
+     * shift the start to be the last prior end point if the new extension
+     * is before the current start point.
+     */
+    public func shiftExtend (row: Int, col: Int)
+    {
+        active = true
+        let newEnd = Position  (col: col, row: row + terminal.buffer.yDisp)
+        
+        var shouldSwapStart = false
+        if compare (start, end) == .before {
+            // start is before end, is the new end before Start
+            if compare (newEnd, start) == .before {
+                // yes, swap Start and End
+                shouldSwapStart = true
+            }
+        } else if compare (start, end) == .after {
+            if compare (newEnd, start) == .after {
+                // yes, swap Start and End
+                shouldSwapStart = true
+            }
+        }
+        
+        if (shouldSwapStart) {
+            start = end
+        }
+        
+        end = newEnd
+        terminal.tdel.selectionChanged(source: terminal)
+    }
+    
+    /**
+     * Extends the selection by moving the end point to the new point.
+     */
+    public func dragExtend (row: Int, col: Int)
+    {
+        end = Position(col: col, row: row+terminal.buffer.yDisp)
+        terminal.tdel.selectionChanged(source: terminal)
+    }
+    
+    /**
+     * Selects the entire buffer
+     */
+    public func selectAll ()
+    {
+        start = Position(col: 0, row: 0)
+        end = Position(col: terminal.cols-1, row: terminal.buffer.lines.maxLength - 1)
+        active = true
+    }
+    
+    /**
+     * Clears the selection
+     */
+    public func selectNone ()
     {
         active = false
     }
+    
+    public func getSelectedText () -> String
+    {
+        let lines = getSelectedLines()
+        if lines.count == 0 {
+            return ""
+        }
+        var r = ""
+        for line in lines {
+            r += line.toString()
+        }
+        return r
+    }
+    
+    func getSelectedLines() -> [Line]
+    {
+        var start = self.start
+        var end = self.end
+        
+        switch compare (start, end) {
+        case .equal:
+            return []
+        case .after:
+            start = end
+            end = start
+        case .before:
+            break
+        }
+        if start.row < 0 || start.row > terminal.buffer.lines.count {
+            return []
+        }
+        
+        if end.row >= terminal.buffer.lines.count {
+            end.row = terminal.buffer.lines.count-1
+        }
+        return getSelectedLines(start, end)
+    }
+    
+    func getSelectedLines(_ start: Position, _ end: Position) -> [Line]
+    {
+        var lines: [Line] = []
+        var buffer = terminal.buffer
+        var str = ""
+        var currentLine = Line ()
+        lines.append(currentLine)
+        
+        // keep a list of blank lines that we see. if we see content after a group
+        // of blanks, add those blanks but skip all remaining / trailing blanks
+        // these will be blank lines in the selected text output
+        var blanks: [LineFragment] = []
+        
+        func addBlanks () {
+            var lastLine = -1;
+            for b in blanks {
+                if lastLine != -1 && b.line != lastLine {
+                    currentLine = Line ()
+                    lines.append(currentLine)
+                }
+                
+                lastLine = b.line
+                currentLine.add(fragment: b)
+            }
+            blanks = []
+        };
+        
+        // get the first line
+        var bufferLine = buffer.lines [start.row]
+        if bufferLine.hasAnyContent() {
+            let str: String = translateBufferLineToString (buffer: buffer, line: start.row, start: start.col, end: start.row < end.row ? -1 : end.col)
+            
+            let fragment = LineFragment (text: str, line: start.row, location: start.col, length: str.count)
+            currentLine.add (fragment: fragment)
+        }
+        
+        // get the middle rows
+        var line = start.row + 1
+        var isWrapped = false
+        while line < end.row {
+            bufferLine = buffer.lines [line]
+            isWrapped = bufferLine.isWrapped
+            
+            str = translateBufferLineToString (buffer: buffer, line: line, start: 0, end: -1)
+            
+            if bufferLine.hasAnyContent () {
+                // add previously gathered blank fragments
+                addBlanks ()
+                
+                if !isWrapped {
+                    // this line is not a wrapped line, so the
+                    // prior line has a hard linefeed
+                    // add a fragment to that line
+                    currentLine.add (fragment: LineFragment.newLine (line: line - 1))
+                    
+                    // start a new line
+                    currentLine = Line ()
+                    lines.append(currentLine)
+                }
+                
+                // add the text we found to the current line
+                currentLine.add (fragment: LineFragment (text: str, line: line, location: 0, length: str.count))
+            } else {
+                // this line has no content, which means that it's a blank line inserted
+                // somehow, or one of the trailing blank lines after the last actual content
+                // make a note of the line
+                // check that this line is a wrapped line, if so, add a line feed fragment
+                if !isWrapped {
+                    blanks.append (LineFragment.newLine (line: line - 1))
+                }
+                
+                blanks.append(LineFragment (text: str, line: line, location: 0, length: str.count));
+            }
+            
+            line += 1
+        }
+        
+        // get the last row
+        if end.row != start.row {
+            bufferLine = buffer.lines [end.row]
+            if bufferLine.hasAnyContent () {
+                addBlanks ()
+                
+                isWrapped = bufferLine.isWrapped
+                str = translateBufferLineToString (buffer: buffer, line: end.row, start: 0, end: end.col)
+                if !isWrapped {
+                    currentLine.add(fragment: LineFragment.newLine (line: line - 1))
+                    currentLine = Line ()
+                    lines.append(currentLine)
+                }
+                
+                currentLine.add (fragment: LineFragment (text: str, line: line, location: 0, length: str.count))
+            }
+        }
+        return lines
+    }
+
+    func translateBufferLineToString (buffer: Buffer, line: Int, start: Int, end: Int) -> String
+    {
+        buffer.translateBufferLineToString(lineIndex: line, trimRight: true, startCol: start, endCol: end).replacingOccurrences(of: "\u{0}", with: " ")
+    }
 }

+ 33 - 14
SwiftTerm/Sources/SwiftTerm/Terminal.swift

@@ -71,6 +71,13 @@ public protocol TerminalDelegate {
     
     // Should raise the bell
     func bell (source: Terminal)
+    
+    /**
+     * This is invoked when the selection has changed, or has been turned on.   The status is
+     * available in `terminal.selection.active`, and the range relative to the buffer is
+     * in `terminal.selection.start` and `terminal.selection.end`
+     */
+    func selectionChanged (source: Terminal)
 }
 
 /**
@@ -103,7 +110,6 @@ public class Terminal {
     
     // Whether the terminal is operating in application keypad mode
     var applicationKeypad : Bool = false
-    var savedCols: Int = 0
     // Whether the terminal is operating in application cursor mode
     var applicationCursor : Bool = false
     
@@ -255,10 +261,10 @@ public class Terminal {
     {
         parser.csiHandlerFallback = { (pars: [Int], collect: cstring, code: UInt8) -> () in
             let ch = Character(UnicodeScalar(code))
-            self.error ("Unknown CSI Code (collect=\(collect) code=\(ch) pars=\(pars)")
+            self.error ("Unknown CSI Code (collect=\(collect) code=\(ch) pars=\(pars))")
         }
         parser.escHandlerFallback = { (txt: cstring, flag: UInt8) in
-            self.error ("Unknown ESC Code (txt=\(txt) flag=\(flag)")
+            self.error ("Unknown ESC Code (txt=\(txt) flag=\(flag))")
         }
         parser.executeHandlerFallback = {
             self.error ("Unknown EXECUTE code")
@@ -382,6 +388,7 @@ public class Terminal {
         parser.setEscHandler ("#8", { collect, flag in self.cmdScreenAlignmentPattern () })
         for bflag in CharSets.all.keys {
             let flag = String (UnicodeScalar (bflag))
+            
             parser.setEscHandler ("(" + flag, { code, f in self.selectCharset ([0x28] + [f]) })
             parser.setEscHandler (")" + flag, { code, f in self.selectCharset ([0x29] + [f]) })
             parser.setEscHandler ("*" + flag, { code, f in self.selectCharset ([0x2a] + [f]) })
@@ -756,8 +763,19 @@ public class Terminal {
             cmdIndex ()
     }
 
+    /**
+     * ESC H
+     * C1.HTS
+     *   DEC mnemonic: HTS (https://vt100.net/docs/vt510-rm/HTS.html)
+     *   Sets a horizontal tab stop at the column position indicated by
+     *   the value of the active column when the terminal receives an HTS.
+     *
+     * @vt: #Y   C1    HTS   "Horizontal Tabulation Set" "\x88"    "Places a tab stop at the current cursor position."
+     * @vt: #Y   ESC   HTS   "Horizontal Tabulation Set" "ESC H"   "Places a tab stop at the current cursor position."
+     */
     func cmdTabSet ()
     {
+        print ("SET \(buffer.x)")
         buffer.tabSet (pos: buffer.x)
     }
     
@@ -1097,6 +1115,10 @@ public class Terminal {
     //
     func selectCharset (_ p: ArraySlice<UInt8>)
     {
+        if p.count == 2 {
+            print ("Settin charset to \(p[1])")
+        }
+        
         if (p.count != 2) {
             cmdSelectDefaultCharset ()
         }
@@ -1330,8 +1352,7 @@ public class Terminal {
 
     //
     // CSI Ps ; Ps r
-    //   Set Scrolling Region [top;bottom] (default = full size of win-
-    //   dow) (DECSTBM).
+    //   Set Scrolling Region [top;bottom] (default = full size of window) (DECSTBM).
     // CSI ? Pm r
     //
     func cmdSetScrollRegion (_ pars: [Int], _ collect: cstring)
@@ -1341,8 +1362,7 @@ public class Terminal {
         }
         buffer.scrollTop = pars.count > 0 ? max (pars [0] - 1, 0) : 0
         buffer.scrollBottom = (pars.count > 1 ? min (pars [1], rows) : rows) - 1
-        buffer.x = 0
-        buffer.y = 0
+        setCursor(col: 0, row: 0)
     }
 
     func setCursorStyle (_ style: CursorStyle)
@@ -1807,11 +1827,10 @@ public class Terminal {
                 applicationCursor = false
                 break
             case 3:
-                if cols == 132 && savedCols != 0 {
-                    resize (cols: savedCols, rows: rows)
-                    tdel.sizeChanged(source: self)
-                }
-                savedCols = 0
+                // DECCOLM
+                resize (cols: 80, rows: rows)
+                tdel.sizeChanged(source: self)
+                reset()
                 break;
             case 5:
                 // Reset default color
@@ -2014,8 +2033,8 @@ public class Terminal {
                 // set VT100 mode here
                 
             case 3: // 132 col mode
-                savedCols = cols
                 resize (cols: 132, rows: rows)
+                reset()
                 tdel.sizeChanged(source: self)
             case 5:
                 // Inverted colors
@@ -2118,7 +2137,7 @@ public class Terminal {
     //
     func cmdTabClear (_ pars: [Int], _ collect: cstring)
     {
-        let p = max (pars.count == 0 ? 1 : pars [0], 1)
+        let p = pars.count == 0 ? 0 : pars [0]
         if p == 0 {
             buffer.tabClear(pos: buffer.x)
         } else if (p == 3) {