通往開源庫 Terminus 的曲折路徑。
在生物學中,像鱷魚這樣的動物有時被稱為活化石,因為它們似乎與過去地質層中發現的標本幾乎沒有什么不同。計算機技術有一些自己的活化石。終端,或更可能的終端仿真器,就是一個這樣的例子。 70 年代的終端(如 VT100)是具有鍵盤、屏幕和有限邏輯的物理設備,可以使用共享計算機發送和接收命令。快進到 2022 年。終端仍然有大量使用?;谠频姆?、Web 服務、遠程工作和腳本編寫是我想到的幾個例子。今天我想講一個開發者的故事,它涉及在 swift 中尋求命令行工具,與 ncurses 的史詩般的戰斗,以及最終開發 Terminus,一個開源包,我希望你們中的一些讀者,將考慮給予嘗試。
用于 Swift 中的命令行應用程序開發的包
在完成了生物醫學信息學方面的培訓后,我大量使用了 shell 和 R 和 Python/ target=_blank class=infotextkey>Python 等語言,然后通過學習 Swift 回到了 Apple 設備編程。我很快意識到 Swift 是一門了不起的語言,我希望看到它成長為我可以在所有編程任務中使用的東西,而不僅僅是用于編寫 IOS 和 mac 應用程序。遺憾的是,在 Swift 中做其他事情的基礎設施還有很多成熟的工作要做。
我喜歡在數據科學領域做的很多事情(探索性數據分析、數據處理、機器學習等)都在命令行上進行。 當我的屏幕左側有一個腳本而右側有 iPython 時,我有賓至如歸的感覺。
對于那些不熟悉的人,iPython 是一個交互式 Python shell,或在終端中運行的 REPL(讀取-評估-打印-循環)。它提供了語法高亮、代碼完成和一大堆其他漂亮的功能。我心想……你可以在 iPython 中用著色和編輯文本做這么多巧妙的事情,而且自動完成菜單系統真的很酷。我們在 Swift 中有什么可以讓我們在終端中做一些引人注目的事情?
經過一番谷歌搜索后,我確實設法找到了一些用于命令行工具的有趣包。請隨意閱讀以下列表:
- Rainbow— 一個漂亮的文本著色和樣式包
- ANSITerminal— 提供文本顏色和樣式、光標功能(移動、隱藏/顯示、保存/恢復)、屏幕功能(清除屏幕、清除行等)和鍵盤捕獲(一次一個字符,沒有內置行編輯器)。
- Swift CommandLineKit— 來自 google 的 Matthias Zenger。包括用于處理命令行參數、文本顏色和樣式、單行和多行輸入、文本完成和提示的系統。
- ConsolKit— 來自 Vapor(Swift 中的后端 Web 框架)的制造商。一個強大的包,提供活動指示、參數處理(標志、選項等)、文本樣式和著色、日志記錄等。
所有這些包都提供了對文本顏色和樣式的基本支持,ConsolKit 和 CommandLineKit 具有大量高級功能。我遇到的問題是我想要菜單,對在屏幕上移動光標的細粒度支持,以及更好地控制選擇顏色。這讓我想到了著名的 ncurses C 庫。
我與 ncurses 的史詩般的戰斗
對于那些不熟悉的人,ncurses 是一個 C 包,最初是在 80 年代初編寫的,旨在在各種終端上創建用戶界面。成百上千的程序使用 ncurses 來創建文本用戶界面 (TUI)。由于 Swift 與 C 的配合非常好,我認為圍繞 ncurses 編寫一個 Swift 包裝器是一個好主意,它具有一些感興趣的功能,例如菜單。
在大多數情況下,將 C 庫合并到 Swift 中是一個相對輕松的過程。 您提供一個模塊映射,告訴編譯器您的 C 庫位于何處(在本地項目或系統中)以及在包裝文件中使用 import 時模塊的名稱應該是什么。 從那里您可以開始以您喜歡的任何方式圍繞 C 庫編寫包裝器。 我的計劃是在我的包清單中使用 Homebrew(linux 上的 apt),并將我的包與系統安裝的 ncurses 庫鏈接,如下所示:
// swift-tools-version:5.5// The swift-tools-version declares the minimum version of Swift required to build this package.import PackageDescriptionlet package = Package(name: "SwiftNCurses",products: [,targets: [.systemLibrary(name: "Cncurses", pkgConfig: pkgConfig, providers: [.apt(["ncurses"]), .brew(["ncurses"])]),
如果你在 Linux 系統上,每一個都很好。 事實上,我是從 TheCoderMerlin 的這個項目開始的,它就是這樣做的。 但這是我從 Mac 上的編譯器收到的令人討厭的消息:
好粗魯。事實證明,Darwin 模塊引入了它自己的 ncurses 版本,該版本包含在 MacOS 開發人員 SDK 中!這意味著頭文件已經被導入,我們正在嘗試重新定義之前聲明的東西。此外,MacOS SDK 中的 ncurses 版本在 5.8 版上已經過時了……據我所知,這大約是 2011 年。為什么?!我花了幾個小時在 StackOverflow 上尋找修復程序,就在我準備在我的電腦上郵寄時,我遇到了一個解決方案。簡而言之,您可以在運行 swift build 時傳遞 -Xcc -D__NCURSES_H ,這會告訴編譯器忽略所有 ncurses 頭文件。這樣做的問題是,同樣的問題也出現在 brew 安裝的 ncurses 版本中......并且要解決這個問題,您必須在本地復制所有 ncurses 標頭并用其他東西替換 __NCURSES_H 的實例。
為了使這個冗長的故事簡短,我終于得到了在 Mac 和 Linux 上編譯和工作的東西,但是所有的頭文件混合都會讓維護變得非常痛苦,而且仍然有一個交易破壞者。任何對使用 wrapper 包感興趣的用戶都必須在他們自己的項目中包含 -Xcc -D__NCURSES_H C 編譯器標志。這不會給開發人員帶來愉快的體驗……所以我存檔了項目并繼續前進。安息吧 SwiftNCurses。
終點站
從那時起,我編寫了一個純粹基于 ANSI 的 Swift 包,名為 Terminus,現在我想與您分享。
這是演示如何使用樣式和顏色寫入終端的示例代碼:
import Terminuslet Terminal = Terminal.sharedterminal.write("I am bold and underlined.n", attributes: [.bold, .underline])let greenColor = Color(r:0, g:255, b:0)terminal.write("Grass is green.n", attributes: [.color(greenColor)])let palette = XTermPalette()let blueOneYellow = ColorPair(foreground: palette.Blue1, background: palette.Yellow1)terminal.write("Blue on yellow", attributes: [.colorPair(blueOneYellow)])
請注意,您可以為文本添加任意數量的樣式和/或顏色。 可以使用 RGB 或使用來自內置調色板之一的命名顏色來指定顏色,例如我在此處使用的 XTerm 調色板。 該文檔為每個調色板提供了一個可視化圖表。
Terminus 還支持 AttributedStrings。
import Foundationimport Terminuslet terminal = Terminal.sharedvar attributedString = AttributedString("Hello, bold, underlined, world.")if let boldRange = attributedString.range(of: "bold") {attributedString[boldRange].terminalTextAttributes = [.bold]if let underlinedRange = attributedString.range(of: "underlined") {attributedString[underlinedRange].terminalTextAttributes = [.underline]terminal.write(attributedString: attributedString)
現在來看一些更有趣的東西。 這是制作菜單的一些代碼。
import Foundationimport Terminuslet terminal = Terminal.sharedterminal.clearScreen()terminal.cursor.moveToHome()let palette = XTermPalette()let itemColor = palette.Aquamarine2let selectionColor = palette.Green5let menuItems = ["Life", "Death", "Taxes"]let menu = Menu(items: menuItems, maxColumns: 1, scrollDirection: .vertical, itemAttributes: [.color(itemColor)], selectionAttributes: [.reverse, .color(selectionColor)])let selection = menu.getSelection()
最后但并非最不重要的......使用采用文本突出顯示的行編輯器。
import Terminuslet terminal = Terminal.sharedlet lineEditor = LineEditor()lineEditor.bufferHandler = {var shouldWriteBuffer = falseif let greenRange = lineEditor.buffer.range(of: "green") {lineEditor.buffer[greenRange].terminalTextAttributes = [.color(Color(r: 0, g: 255, b: 0))]shouldWriteBuffer = trueif let yellowRange = lineEditor.buffer.range(of: "yellow") {lineEditor.buffer[yellowRange].terminalTextAttributes = [.color(Color(r: 255, g: 255, b: 0))]shouldWriteBuffer = trueif let redRange = lineEditor.buffer.range(of: "red") {lineEditor.buffer[redRange].terminalTextAttributes = [.color(Color(r: 255, g: 0, b: 0))]shouldWriteBuffer = truereturn shouldWriteBufferlet input = lineEditor.getInput()
結論
對于那些堅持到最后的人,感謝您的閱讀! Terminus 是一個新的軟件包,絕不是完整的。 我正在積極尋找合作者來添加功能、修復錯誤等。