Curses

Curses 提供了一个 Swift 对象包装器,用于 Linux 的 curses 编程库(已在 Ubuntu 16.04、18.04 上测试)

什么是 Curses?

Curses 能够以与终端无关的方式创建更好看的基于文本(控制台)的应用程序。 此包装器通过 Swift 对象提供对某些此功能的访问。 当前功能支持

Sample Screen

用法

为了使用该库,请将此资源作为依赖项包含在 Package.swift 中

// swift-tools-version:4.0
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
  name:  "myApplication",
  dependencies: [
    .package(url: "https://github.com/TheCoderMerlin/Curses.git", from: "1.0.0"),
  ],
  targets: [
    .target(
      name:"myApplication",
      dependencies:["Curses"],
      path: "Sources"),
  ]
)

打开/关闭库

要开始使用该库,请导入 Curses,访问共享的 Screen 对象,并调用 startUp()

import Curses

let screen = Screen.shared
screen.startUp()

使用完该库后,请务必通过调用 shutDown() 来干净地退出。

screen.shutDown()

中断处理程序

一个常见的范例是继续运行应用程序,直到被 ^C 中断。 一种实现此目的的简单方法如下

// Define an interrupt handler
let screen = Screen.shared

class Handler : CursesHandlerProtocol {
    func interruptHandler() {
        screen.shutDown()
        print("Good bye!")
        exit(0)
    }

    func windowChangedHandler() {
    }
}

// Start up
screen.startUp(handler:Handler())

// Do some fun stuff

// Wait forever, or until the user presses ^C
screen.wait()

窗口

提供对屏幕窗口的单个实例的快捷访问。 可以使用此实例,也可以在屏幕上创建单独的窗口,但通常不建议混用。

仅使用主窗口

let mainWindow = screen.window

仅使用单独的窗口

let firstWindow = screen.newWindow(position:Point(x:10, y:20), size:Size(width:100, height:20))
let secondWindow = screen.newWindow(position:Point(x:120, y:10), size:Size(width:100, height:20))

写入控制台

写入将在当前光标位置使用当前活动的属性进行

mainWindow.write("Hello, world!")

将更改刷新到控制台

重要提示:在调用 refresh() 之前,更改在控制台上不可见

mainWindow.refresh()

启用滚动

mainWindow.setScroll(enabled:true)

移动光标

// Move the cursor to an absolute position
let cursor = mainWindow.cursor
cursor.position = Point(x:10, y:10)

// Temporarily move the cursor to a new position
cursor.pushPosition(newPosition:Point(x:0, y:0))
mainWindow.write("Cursor is at: \(cursor.position)")
cursor.popPosition()

使用属性

可以通过让窗口 turnOn() 属性来打开属性。 完成后,最好 turnOff() 该属性。 要设置特定属性而不考虑该位置的当前属性,请执行 setTo()

// Display text in various attributes
mainWindow.turnOn(Attribute.normal)
mainWindow.write("Normal")
mainWindow.turnOff(Attribute.normal)
mainWindow.write(" ")                                                                                                                          

mainWindow.turnOn(Attribute.standout)
mainWindow.write("Standout")
mainWindow.turnOff(Attribute.standout)
mainWindow.write(" ")

mainWindow.turnOn(Attribute.underline)
mainWindow.write("Underline")
mainWindow.turnOff(Attribute.underline)
mainWindow.write(" ")

mainWindow.turnOn(Attribute.reverse)
mainWindow.write("Reverse")
mainWindow.turnOff(Attribute.reverse)
mainWindow.write(" ")

mainWindow.turnOn(Attribute.blink)
mainWindow.write("Blink")
mainWindow.turnOff(Attribute.blink)
mainWindow.write(" ")

mainWindow.turnOn(Attribute.dim)
mainWindow.write("Dim")
mainWindow.turnOff(Attribute.dim)
mainWindow.write(" ")

mainWindow.turnOn(Attribute.bold)
mainWindow.write("Bold")
mainWindow.turnOff(Attribute.bold)

使用颜色

在使用颜色之前,需要启动 Colors。 重要的是要确保终端支持颜色,然后再继续。 颜色只能在启动 Curses 之后启动。

screen.startUp(handler:handler)

let colors = Colors.shared
precondition(colors.areSupported, "This terminal doesn't support colors")
colors.startUp()

可以使用枚举访问标准颜色

public enum StandardColor : String, CaseIterable {
    case black
    case red
    case green
    case yellow
    case blue
    case magenta
    case cyan
    case white
}  

颜色总是成对使用,一个用于前景色,一个用于背景色。 要使用红色作为前景色和白色作为背景色写入屏幕,我们创建一个颜色对,将其打开,然后在完成后礼貌地将其关闭。

let red = Color.standard(.red)
let white = Color.standard(.white)
let redOnWhite = colors.newPair(foreground:red, background:white)

mainWindow.cursor.position = Point(x:0, y:0)
mainWindow.turnOn(redOnWhite)
mainWindow.write("RED ON WHITE")
mainWindow.turnOff(redOnWhite)

我们可以创建一个颜色对图表并显示它们

// Find the length of the longest color name
// We'll use this for spacing the columns
let maxColorNameLength = Color.StandardColor.allCases.map {$0.rawValue.count}.max()!
let xSpaceBetweenNames = 3
// We'll start the display at the third row 
var row = 3
// Iterate through all of the foreground and background colors
for foregroundColor in Color.StandardColor.allCases {
    var column = 1
    for backgroundColor in Color.StandardColor.allCases { 
        // Create a new color pair
        // Note that the number of color pairs is limited so this may not work on all terminals
        precondition(colors.pairCount + 1 < colors.maxPairCount, "Unable to create more color pairs.")
        let pair = colors.newPair(foreground:Color.standard(foregroundColor), background:Color.standard(backgroundColor)) 
        
        // Turn on the color attribute
        // It's polite to turn off what we turned on (below)
        mainWindow.turnOn(pair)
        // Position the cursor
        mainWindow.cursor.position = Point(x: column * (maxColorNameLength + xSpaceBetweenNames), y:row)
        // Write the color name
        mainWindow.write(backgroundColor.rawValue)
        // Turn off the color attribute
        mainWindow.turnOff(pair) 
        column += 1 
    }
    row += 1
}

如果终端支持此功能,则可以通过指定 0 到 1000 范围内的红色、绿色和蓝色值来创建自定义颜色。 如果支持,则可用的“插槽”数量有限。

precondition(colors.customColorsAreSupported, "Custom colors are not supported.")
precondition(colors.colorCount + 1 < colors.maxColorCount, "No more slots available for custom colors.")
let orange = colors.newColor(red:1000, green:500, blue:0)
precondition(colors.pairCount + 1 < colors.maxPairCount, "Unable to create more color pairs.")
let orangeOnBlack = colors.newPair(foreground:orange, background:Color.standard(.black)) 

mainWindow.turnOn(orangeOnBlack) 
mainWindow.write("Orange") 
mainWindow.turnOff(orangeOnBlack)  

更改背景

let cyan = Color.standard(.cyan)
let black = Color.standard(.black)
let cyanOnBlack = colors.newPair(foreground:black, background:cyan)

mainWindow.backgroundSet(attribute:cyanOnBlack)
mainWindow.clear()
mainWindow.write("Hello, World!")
mainWindow.refresh()

使用键盘

let keyboard = Keyboard.shared

while true {
    let key = keyboard.getKey(window:mainWindow)

    var youPressed : String
    if key.keyType == .isCharacter {
        youPressed = "You pressed: \(key.character!)"
    } else if key.keyType == .isControl {
        youPressed = "You pressed: CONTROL-\(key.control!)"
    } else {
        var label : String
        switch (key.keyType) {
        case .arrowDown: label = "Down"
        case .arrowUp: label = "Up"
        case .arrowRight: label = "Right"
        case .arrowLeft: label = "Left"
 
        case .function1: label = "F1"
        case .function2: label = "F2"
        case .function3: label = "F3"
        case .function4: label = "F4"

        case .enter:     label = "ENTER"
 
        default: label = "Other"
        }
        youPressed = "You pressed a special key: \(label)"
    }
    mainWindow.cursor.position = Point(x:1, y:2)
    mainWindow.write(youPressed)
    mainWindow.clearToEndOfLine()
    mainWindow.refresh()
}

从字段获取文本

let cyan = Color.standard(.cyan)
let black = Color.standard(.black)
let cyanOnBlack = colors.newPair(foreground:black, background:cyan)
let string = mainWindow.getStringFromTextField(at:Point(x:10, y:10), maxCharacters:10, fieldColorPair:cyanOnBlack)