From cdb29a47e5fcca80e5d60c63beb30ef117cc60fb Mon Sep 17 00:00:00 2001 From: stackotter Date: Thu, 24 Aug 2023 11:11:23 +1000 Subject: [PATCH] Create experimental interactive terminal backend (using TermKit) --- .github/workflows/swift-linux.yml | 13 ++- .github/workflows/swift-macos.yml | 13 ++- Package.resolved | 36 +++++++ Package.swift | 16 +++ Sources/CursesBackend/CursesBackend.swift | 120 ++++++++++++++++++++++ 5 files changed, 196 insertions(+), 2 deletions(-) create mode 100644 Sources/CursesBackend/CursesBackend.swift diff --git a/.github/workflows/swift-linux.yml b/.github/workflows/swift-linux.yml index f8d89c06..b08bd011 100644 --- a/.github/workflows/swift-linux.yml +++ b/.github/workflows/swift-linux.yml @@ -16,6 +16,17 @@ jobs: sudo apt update && \ sudo apt install -y libgtk-4-dev clang - name: Build - run: swift build -v + run: | + swift build SwiftCrossUI && \ + swift build GtkBackend && \ + swift build CounterExample && \ + swift build RandomNumberGeneratorExample && \ + swift build WindowPropertiesExample && \ + swift build GreetingGeneratorExample && \ + swift build FileViewerExample && \ + swift build NavigationExample && \ + swift build SplitExample && \ + swift build GtkCodeGen && \ + swift build GtkExample - name: Test run: swift test diff --git a/.github/workflows/swift-macos.yml b/.github/workflows/swift-macos.yml index 638c436a..5fc198ac 100644 --- a/.github/workflows/swift-macos.yml +++ b/.github/workflows/swift-macos.yml @@ -17,6 +17,17 @@ jobs: - name: Patch libffi run: sed -i '' 's/-I..includedir.//g' /usr/local/Homebrew/Library/Homebrew/os/mac/pkgconfig/13/libffi.pc - name: Build - run: swift build -v + run: | + swift build SwiftCrossUI && \ + swift build GtkBackend && \ + swift build CounterExample && \ + swift build RandomNumberGeneratorExample && \ + swift build WindowPropertiesExample && \ + swift build GreetingGeneratorExample && \ + swift build FileViewerExample && \ + swift build NavigationExample && \ + swift build SplitExample && \ + swift build GtkCodeGen && \ + swift build GtkExample - name: Test run: swift test diff --git a/Package.resolved b/Package.resolved index 3ad2a999..89e3cc69 100644 --- a/Package.resolved +++ b/Package.resolved @@ -19,6 +19,15 @@ "version": null } }, + { + "package": "OpenCombine", + "repositoryURL": "https://github.com/OpenCombine/OpenCombine.git", + "state": { + "branch": null, + "revision": "8576f0d579b27020beccbccc3ea6844f3ddfc2c2", + "version": "0.14.0" + } + }, { "package": "Qlift", "repositoryURL": "https://github.com/Longhanks/qlift", @@ -55,6 +64,33 @@ "version": "508.0.1" } }, + { + "package": "SwiftTerm", + "repositoryURL": "https://github.com/migueldeicaza/SwiftTerm.git", + "state": { + "branch": null, + "revision": "116b795e4de2324d4a1b1bb2db02141294bc9229", + "version": "1.2.4" + } + }, + { + "package": "TermKit", + "repositoryURL": "https://github.com/migueldeicaza/TermKit", + "state": { + "branch": null, + "revision": "3bce85d1bafbbb0336b3b7b7e905c35754cb9adf", + "version": null + } + }, + { + "package": "TextBufferKit", + "repositoryURL": "https://github.com/migueldeicaza/TextBufferKit.git", + "state": { + "branch": null, + "revision": "7f3ed5b1d7288de34ad853544d802647be11cfcf", + "version": "0.3.0" + } + }, { "package": "XMLCoder", "repositoryURL": "https://github.com/CoreOffice/XMLCoder", diff --git a/Package.swift b/Package.swift index 5ee0b009..f91378a4 100644 --- a/Package.swift +++ b/Package.swift @@ -141,6 +141,22 @@ if checkSDL2Installed() { ) } +// TODO: Conditionally include TermKit backend +conditionalTargets.append( + .target( + name: "CursesBackend", + dependencies: ["SwiftCrossUI", "TermKit"] + ) +) +backendTargets.append("CursesBackend") +exampleDependencies.append("CursesBackend") +dependencies.append( + .package( + url: "https://github.com/migueldeicaza/TermKit", + revision: "3bce85d1bafbbb0336b3b7b7e905c35754cb9adf" + ) +) + let package = Package( name: "swift-cross-ui", platforms: [.macOS(.v10_15)], diff --git a/Sources/CursesBackend/CursesBackend.swift b/Sources/CursesBackend/CursesBackend.swift new file mode 100644 index 00000000..02a448ba --- /dev/null +++ b/Sources/CursesBackend/CursesBackend.swift @@ -0,0 +1,120 @@ +import Foundation +import SwiftCrossUI +import TermKit + +public struct CursesBackend: AppBackend { + public typealias Widget = TermKit.View + + public init(appIdentifier: String) {} + + public func run( + _ app: AppRoot, + _ setViewGraph: @escaping (ViewGraph) -> Void + ) where AppRoot.Backend == Self { + let viewGraph = ViewGraph(for: app, backend: self) + setViewGraph(viewGraph) + + Application.prepare() + let root = RootView() + root.addSubview(viewGraph.rootNode.widget) + Application.top.addSubview(root) + Application.run() + } + + public func runInMainThread(action: @escaping () -> Void) { + DispatchQueue.main.async { + action() + } + } + + public func show(_ widget: Widget) { + widget.setNeedsDisplay() + } + + public func createVStack(spacing: Int) -> Widget { + return View() + } + + public func addChild(_ child: Widget, toVStack container: Widget) { + // TODO: Properly calculate layout + child.y = Pos.at(container.subviews.count) + container.addSubview(child) + } + + public func setSpacing(ofVStack container: Widget, to spacing: Int) {} + + public func createHStack(spacing: Int) -> Widget { + return View() + } + + public func addChild(_ child: Widget, toHStack container: Widget) { + // TODO: Properly calculate layout + child.y = Pos.at(container.subviews.count) + container.addSubview(child) + } + + public func setSpacing(ofHStack container: Widget, to spacing: Int) {} + + public func createTextView(content: String, shouldWrap: Bool) -> Widget { + let label = Label(content) + label.width = Dim.fill() + return label + } + + public func setContent(ofTextView textView: Widget, to content: String) { + let label = textView as! Label + label.text = content + } + + public func setWrap(ofTextView textView: Widget, to shouldWrap: Bool) {} + + public func createButton(label: String, action: @escaping () -> Void) -> Widget { + let button = TermKit.Button(label, clicked: action) + button.height = Dim.sized(1) + return button + } + + public func setLabel(ofButton button: Widget, to label: String) { + (button as! TermKit.Button).text = label + } + + public func setAction(ofButton button: Widget, to action: @escaping () -> Void) { + (button as! TermKit.Button).clicked = { _ in + action() + } + } + + // TODO: Properly implement padding container. Perhaps use a conversion factor to + // convert the pixel values to 'characters' of padding + public func createPaddingContainer(for child: Widget) -> Widget { + return child + } + + public func getChild(ofPaddingContainer container: Widget) -> Widget { + return container + } + + public func setPadding( + ofPaddingContainer container: Widget, + top: Int, + bottom: Int, + leading: Int, + trailing: Int + ) {} +} + +class RootView: TermKit.View { + override func processKey(event: KeyEvent) -> Bool { + if super.processKey(event: event) { + return true + } + + switch event.key { + case .controlC, .esc: + Application.requestStop() + return true + default: + return false + } + } +}