Initial commit

This commit is contained in:
david-swift 2024-07-10 14:41:11 +02:00
commit 8e79a2bd23
34 changed files with 1841 additions and 0 deletions

40
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: Bug report
description: Something is not working as expected.
title: Description of the bug
labels: bug
body:
- type: textarea
attributes:
label: Describe the bug
description: >-
A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
attributes:
label: To Reproduce
description: >-
Steps to reproduce the behavior.
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
attributes:
label: Expected behavior
description: >-
A clear and concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Additional context
description: >-
Add any other context about the problem here.

View File

@ -0,0 +1,36 @@
name: Feature request
description: Suggest an idea for this project
title: Description of the feature request
labels: enhancement
body:
- type: input
attributes:
label: Is your feature request related to a problem? Please describe.
placeholder: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
validations:
required: false
- type: textarea
attributes:
label: Describe the solution you'd like
placeholder: >-
A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Describe alternatives you've considered
placeholder: >-
A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: textarea
attributes:
label: Additional context
placeholder: >-
Add any other context or screenshots about the feature request here.
validations:
required: true

11
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,11 @@
## Steps
- [ ] Build the project on your machine. If it does not compile, fix the errors.
- [ ] Describe the purpose and approach of your pull request below.
- [ ] Submit the pull request. Thank you very much for your contribution!
## Purpose
_Describe the problem or feature._
_If there is a related issue, add the link._
## Approach
_Describe how this pull request solves the problem or adds the feature._

45
.github/workflows/docs.yml vendored Normal file
View File

@ -0,0 +1,45 @@
name: Deploy Docs
on:
push:
branches: ["main"]
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
Deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Build Docs
run: |
xcrun xcodebuild docbuild \
-scheme TermKitBackend \
-destination 'generic/platform=macOS' \
-derivedDataPath "$PWD/.derivedData" \
-skipPackagePluginValidation
xcrun docc process-archive transform-for-static-hosting \
"$PWD/.derivedData/Build/Products/Debug/TermKitBackend.doccarchive" \
--output-path "docs" \
--hosting-base-path "TermKitBackend"
- name: Modify Docs
run: |
echo "<script>window.location.href += \"/documentation/termkitbackend\"</script>" > docs/index.html;
sed -i '' 's/,2px/,10px/g' docs/css/index.038e887c.css
- name: Upload Artifact
uses: actions/upload-pages-artifact@v3
with:
path: 'docs'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

30
.github/workflows/swiftlint.yml vendored Normal file
View File

@ -0,0 +1,30 @@
name: SwiftLint
on:
push:
paths:
- '.github/workflows/swiftlint.yml'
- '.swiftlint.yml'
- '**/*.swift'
pull_request:
paths:
- '.github/workflows/swiftlint.yml'
- '.swiftlint.yml'
- '**/*.swift'
workflow_dispatch:
paths:
- '.github/workflows/swiftlint.yml'
- '.swiftlint.yml'
- '**/*.swift'
jobs:
SwiftLint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: SwiftLint
uses: norio-nomura/action-swiftlint@3.2.1
with:
args: --strict
env:
WORKING_DIRECTORY: Source

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
/.swiftpm
/Package.resolved

165
.swiftlint.yml Normal file
View File

@ -0,0 +1,165 @@
# Opt-In Rules
opt_in_rules:
- anonymous_argument_in_multiline_closure
- array_init
- attributes
- closure_body_length
- closure_end_indentation
- closure_spacing
- collection_alignment
- comma_inheritance
- conditional_returns_on_newline
- contains_over_filter_count
- contains_over_filter_is_empty
- contains_over_first_not_nil
- contains_over_range_nil_comparison
- convenience_type
- discouraged_none_name
- discouraged_object_literal
- discouraged_optional_boolean
- discouraged_optional_collection
- empty_collection_literal
- empty_count
- empty_string
- enum_case_associated_values_count
- explicit_init
- fallthrough
- file_header
- file_name
- file_name_no_space
- first_where
- flatmap_over_map_reduce
- force_unwrapping
- function_default_parameter_at_end
- identical_operands
- implicit_return
- implicitly_unwrapped_optional
- joined_default_parameter
- last_where
- legacy_multiple
- let_var_whitespace
- literal_expression_end_indentation
- local_doc_comment
- lower_acl_than_parent
- missing_docs
- modifier_order
- multiline_arguments
- multiline_arguments_brackets
- multiline_function_chains
- multiline_literal_brackets
- multiline_parameters
- multiline_parameters_brackets
- no_extension_access_modifier
- no_grouping_extension
- no_magic_numbers
- number_separator
- operator_usage_whitespace
- optional_enum_case_matching
- prefer_self_in_static_references
- prefer_self_type_over_type_of_self
- prefer_zero_over_explicit_init
- prohibited_interface_builder
- redundant_nil_coalescing
- redundant_type_annotation
- return_value_from_void_function
- shorthand_optional_binding
- sorted_first_last
- sorted_imports
- static_operator
- strict_fileprivate
- switch_case_on_newline
- toggle_bool
- trailing_closure
- type_contents_order
- unneeded_parentheses_in_closure_argument
- yoda_condition
# Disabled Rules
disabled_rules:
- block_based_kvo
- class_delegate_protocol
- dynamic_inline
- is_disjoint
- no_fallthrough_only
- notification_center_detachment
- ns_number_init_as_function_reference
- nsobject_prefer_isequal
- private_over_fileprivate
- redundant_objc_attribute
- self_in_property_initialization
- todo
- unavailable_condition
- valid_ibinspectable
- xctfail_message
# Custom Rules
custom_rules:
github_issue:
name: 'GitHub Issue'
regex: '//.(TODO|FIXME):.(?!.*(https://github\.com/david-swift/TermKitBackend/issues/\d))'
message: 'The related GitHub issue must be included in a TODO or FIXME.'
severity: warning
fatal_error:
name: 'Fatal Error'
regex: 'fatalError.*\(.*\)'
message: 'Fatal error should not be used.'
severity: error
enum_case_parameter:
name: 'Enum Case Parameter'
regex: 'case [a-zA-Z0-9]*\([a-zA-Z0-9\.<>?,\n\t =]+\)'
message: 'The associated values of an enum case should have parameters.'
severity: warning
tab:
name: 'Whitespaces Instead of Tab'
regex: '\t'
message: 'Spaces should be used instead of tabs.'
severity: warning
# Thanks to the creator of the SwiftLint rule
# "empty_first_line"
# https://github.com/coteditor/CotEditor/blob/main/.swiftlint.yml
# in the GitHub repository
# "CotEditor"
# https://github.com/coteditor/CotEditor
empty_first_line:
name: 'Empty First Line'
regex: '(^[ a-zA-Z ]*(?:protocol|extension|class|struct) (?!(?:var|let))[ a-zA-Z:]*\{\n *\S+)'
message: 'There should be an empty line after a declaration'
severity: error
# Analyzer Rules
analyzer_rules:
- unused_declaration
- unused_import
# Options
file_header:
required_pattern: '(// swift-tools-version: .+)?//\n// .*.swift\n// TermKitBackend\n//\n// Created by .* on .*\.(\n// Edited by (.*,)+\.)*\n(\n// Thanks to .* for the .*:\n// ".*"\n// https://.* \(\d\d.\d\d.\d\d\))*//\n'
missing_docs:
warning: [internal, private]
error: [open, public]
excludes_extensions: false
excludes_inherited_types: false
type_contents_order:
order:
- case
- type_alias
- associated_type
- type_property
- instance_property
- ib_inspectable
- ib_outlet
- subscript
- initializer
- deinitializer
- subtype
- type_method
- view_life_cycle_method
- ib_action
- other_method
excluded:
- Sources/TestApp/
- .build/

34
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,34 @@
# Contributing
Thank you very much for taking the time for contributing to this project.
## Report a Bug
Just open a new issue on GitHub and describe the bug. It helps if your description is detailed. Thank you very much for your contribution!
## Suggest a New Feature
Just open a new issue on GitHub and describe the idea. Thank you very much for your contribution!
## Pull Requests
I am happy for every pull request, you do not have to follow these guidelines. However, it might help you to understand the project structure and make it easier for me to merge your pull request. Thank you very much for your contribution!
### 1. Fork & Clone this Project
Start by clicking on the `Fork` button at the top of the page. Then, clone this repository to your computer.
### 2. Open the Project
Open the project folder in GNOME Builder, Xcode or another IDE.
### 3. Understand the Project Structure
- The `README.md` file contains a description of the app or package.
- The `LICENSE.md` contains an MIT license.
- `CONTRIBUTING.md` is this file.
- Directory `Icons` that contains SVG files for the images used in the app and guides.
- `Sources` contains the source code of the project as well as a test app.
### 4. Edit the Code
Edit the code. If you add a new type, add documentation in the code.
### 5. Commit to the Fork
Commit and push the fork.
### 6. Pull Request
Open GitHub to submit a pull request. Thank you very much for your contribution!

21
LICENSE.md Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 david-swift
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

35
Package.swift Normal file
View File

@ -0,0 +1,35 @@
// swift-tools-version: 5.9
//
// Package.swift
// TermKitBackend
//
// Created by david-swift on 01.07.2024.
//
import PackageDescription
/// The TermKitBackend package.
let package = Package(
name: "TermKitBackend",
platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],
products: [
.library(
name: "TermKitBackend",
targets: ["TermKitBackend"]
)
],
dependencies: [
.package(url: "https://github.com/AparokshaUI/Meta", branch: "main"),
.package(url: "https://github.com/david-swift/TermKit", branch: "main")
],
targets: [
.target(
name: "TermKitBackend",
dependencies: ["TermKit", "Meta"]
),
.executableTarget(
name: "TestApp",
dependencies: ["TermKitBackend"]
)
]
)

45
README.md Normal file
View File

@ -0,0 +1,45 @@
<p align="center">
<h1 align="center">TermKitBackend</h1>
</p>
<p align="center">
<a href="https://david-swift.github.io/termkitbackend/">
Documentation
</a>
·
<a href="https://github.com/david-swift/TermKitBackend">
GitHub
</a>
</p>
_TermKitBackend_ is a declarative framework allowing the creation of user interface for the terminal. It works on Linux, macOS and Windows thanks to the [TermKit project](https://github.com/migueldeicaza/TermKit).
## Table of Contents
- [Overview](#overview)
- [Usage](#usage)
- [Thanks](#thanks)
## Overview
The declarative approach is based on the [Meta package](https://aparokshaui.github.io/meta/) which can be found on [GitHub](https://github.com/AparokshaUI/Meta).
It is powered by [a fork of TermKit for Swift](https://github.com/david-swift/TermKit).
Detailed information about the declarative approach can be found in the [Meta docs](https://aparokshaui.github.io/meta/). Find the available widgets [here](https://david-swift.github.io/termkitbackend).
## Usage
Follow the tutorial in the [docs](https://david-swift.github.io/termkitbackend).
## Thanks
### Dependencies
- [TermKit](https://github.com/david-swift/TermKit) licensed under the [MIT License](https://github.com/david-swift/TermKit/blob/main/LICENSE)
- [Meta](https://github.com/AparokshaUI/Meta) licensed under the [MIT License](https://github.com/AparokshaUI/Meta/blob/main/LICENSE.md)
### Other Thanks
- [DocC](https://github.com/apple/swift-docc) used for the documentation
- [SwiftLint](https://github.com/realm/SwiftLint) for checking whether code style conventions are violated
- The programming language [Swift](https://github.com/swiftlang/swift)

View File

@ -0,0 +1,55 @@
//
// Menu.swift
// TermKitBackend
//
// Created by david-swift on 07.07.2024.
//
import TermKit
/// A menu is an item of a ``MenuBar``.
public struct Menu: Renderable {
/// The menu's label, displayed in the menu bar.
var label: String
/// The content of the menu.
var content: [Button]
/// Initialize a menu.
/// - Parameters:
/// - label: The menu's label, displayed in the menu bar.
/// - content: The content of the menu.
public init(_ label: String, @Builder<Button> content: () -> [Button]) {
self.label = label
self.content = content()
}
/// The view storage.
/// - Parameters:
/// - type: The type of the renderable elements.
/// - fields: More information.
public func container<RenderableType>(type: RenderableType.Type, fields: [String: Any]) -> RenderableStorage {
let children = (content as [Renderable]).storages(type: Button.self, fields: [:])
let menu = MenuBarItem(title: label, children: children.compactMap { $0.pointer as? MenuItem })
return .init(menu, content: [.mainContent: children])
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - updateProperties: Whether to update the properties.
/// - type: The type of the renderable elements.
/// - fields: More information.
public func update<RenderableType>(
_ storage: RenderableStorage,
updateProperties: Bool,
type: RenderableType.Type,
fields: [String: Any]
) {
guard let storages = storage.content[.mainContent] else {
return
}
(content as [Renderable]).update(storages, updateProperties: updateProperties, type: type, fields: [:])
}
}

View File

@ -0,0 +1,50 @@
//
// AnyView.swift
// TermKitBackend
//
// Created by david-swift on 07.07.2024.
//
import TermKit
extension AnyView {
/// Set a view's width and height.
/// - Parameters:
/// - width: The width.
/// - height: The height.
/// - Returns: The view.
public func frame(width: Int? = nil, height: Int? = nil) -> AnyView {
inspect { storage, updateProperties in
guard updateProperties, let pointer = storage.pointer as? TermKit.View else {
return
}
pointer.set(x: nil, y: nil, width: width, height: height)
}
}
/// Center a view vertically.
/// - Parameter center: Whether to center the view.
/// - Returns: The view.
public func vcenter(_ center: Bool = true) -> AnyView {
inspect { storage, updateProperties in
guard updateProperties, let pointer = storage.pointer as? TermKit.View else {
return
}
pointer.y = center ? .center() : nil
}
}
/// Center a view horizontally.
/// - Parameter center: Whether to center the view.
/// - Returns: The view.
public func hcenter(_ center: Bool = true) -> AnyView {
inspect { storage, updateProperties in
guard updateProperties, let pointer = storage.pointer as? TermKit.View else {
return
}
pointer.x = center ? .center() : nil
}
}
}

View File

@ -0,0 +1,47 @@
//
// Button.swift
// TermKitBackend
//
// Created by david-swift on 07.07.2024.
//
import TermKit
extension Button: Renderable {
/// An identifier for the button's action.
var actionID: String { "action" }
/// The view storage.
/// - Parameters:
/// - type: The type of the renderable elements.
/// - fields: More information.
public func container<RenderableType>(type: RenderableType.Type, fields: [String: Any]) -> RenderableStorage {
let storage = RenderableStorage(nil)
let menuItem = MenuItem(title: label) {
(storage.fields[actionID] as? () -> Void)?()
}
storage.pointer = menuItem
storage.fields[actionID] = action
return storage
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - updateProperties: Whether to update the properties.
/// - type: The type of the renderable elements.
/// - fields: More information.
public func update<RenderableType>(
_ storage: RenderableStorage,
updateProperties: Bool,
type: RenderableType.Type,
fields: [String: Any]
) {
guard updateProperties else {
return
}
storage.fields[actionID] = action
}
}

View File

@ -0,0 +1,47 @@
//
// TermKitSceneElement.swift
// TermKitBackend
//
// Created by david-swift on 01.07.2024.
//
@_exported import Meta
import TermKit
/// The Meta app storage for the TermKit backend.
public class TermKitApp: AppStorage {
/// The scene element type of the TermKit backend.
public typealias SceneElementType = TermKitSceneElement
/// The widget type of the TermKit backend.
public typealias WidgetType = TermKitWidget
/// The wrapper type of the TermKit backend.
public typealias WrapperType = VStack
/// The application.
public var app: () -> any App
/// The app storage.
public var storage: StandardAppStorage = .init()
/// Initialize the app storage.
/// - Parameters:
/// - id: The identifier.
/// - app: The application.
public required init(id: String, app: @escaping () -> any App) {
self.app = app
}
/// Execute the app.
/// - Parameter setup: Set the scene elements up.
public func run(setup: @escaping () -> Void) {
Application.prepare()
setup()
Application.run()
}
/// Quit the app.
public func quit() {
Application.shutdown()
}
}

View File

@ -0,0 +1,9 @@
//
// TermKitSceneElement.swift
// TermKitBackend
//
// Created by david-swift on 01.07.2024.
//
/// The type of scene elements of the TermKit backend.
public protocol TermKitSceneElement: SceneElement { }

View File

@ -0,0 +1,9 @@
//
// TermKitWidget.swift
// TermKitBackend
//
// Created by david-swift on 01.07.2024.
//
/// The type of widgets of the TermKit backend.
public protocol TermKitWidget: Widget { }

View File

@ -0,0 +1,62 @@
//
// MenuBar.swift
// TermKitBackend
//
// Created by david-swift on 01.07.2024.
//
import TermKit
/// The menu bar scene element adds a menu bar to the top of the app.
public struct MenuBar: TermKitSceneElement {
/// The identifier of the menu bar.
public var id: String
/// The menu bar's content.
var content: [Menu]
/// Initialize the menu bar.
/// - Parameters:
/// - id: The identifier of the menu bar.
/// - content: The menu bar's content.
public init(id: String = "main-menu", @Builder<Menu> content: () -> [Menu]) {
self.id = id
self.content = content()
}
/// Set up the initial scene storages.
/// - Parameter app: The app storage.
public func setupInitialContainers<Storage>(app: Storage) where Storage: AppStorage {
app.storage.sceneStorage.append(container(app: app))
}
/// The scene storage.
/// - Parameter app: The app storage.
public func container<Storage>(app: Storage) -> SceneStorage where Storage: AppStorage {
let menubar = TermKit.MenuBar(
menus: content.compactMap { $0.container(type: Menu.self, fields: [:]).pointer as? MenuBarItem }
)
Task {
for element in Application.top.subviews {
element.y = .bottom(of: menubar)
}
Application.top.addSubview(menubar)
}
return .init(id: id, pointer: menubar) {
menubar.ensureFocus()
}
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - updateProperties: Whether to update the view's properties.
public func update<Storage>(
_ storage: SceneStorage,
app: Storage,
updateProperties: Bool
) where Storage: AppStorage {
Application.refresh()
}
}

View File

@ -0,0 +1,68 @@
//
// Window.swift
// TermKitBackend
//
// Created by david-swift on 01.07.2024.
//
import TermKit
/// The window scene elements holds views.
public struct Window: TermKitSceneElement {
/// The identifier of the window.
public var id: String
/// The title of the window.
var title: String?
/// The window's content.
var content: Body
/// Initialize a window.
/// - Parameters:
/// - title: The title of the window.
/// - id: The identifier of the window.
/// - content: The window's content.
public init(_ title: String? = nil, id: String = "main", @ViewBuilder content: () -> Body) {
self.id = id
self.title = title
self.content = content()
}
/// Set up the initial scene storages.
/// - Parameter app: The app storage.
public func setupInitialContainers<Storage>(app: Storage) where Storage: AppStorage {
app.storage.sceneStorage.append(container(app: app))
}
/// The scene storage.
/// - Parameter app: The app storage.
public func container<Storage>(app: Storage) -> SceneStorage where Storage: AppStorage {
let win = TermKit.Window(title)
win.fill()
Application.top.addSubview(win)
let viewStorage = content.storage(modifiers: [], type: Storage.self)
if let pointer = viewStorage.pointer as? TermKit.View {
win.addSubview(pointer)
}
return .init(id: id, pointer: win, content: [.mainContent: [viewStorage]]) {
win.ensureFocus()
}
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - updateProperties: Whether to update the view's properties.
public func update<Storage>(
_ storage: SceneStorage,
app: Storage,
updateProperties: Bool
) where Storage: AppStorage {
guard let viewStorage = storage.content[.mainContent]?.first else {
return
}
content.updateStorage(viewStorage, modifiers: [], updateProperties: updateProperties, type: Storage.self)
Application.refresh()
}
}

View File

@ -0,0 +1,8 @@
# ``TermKitBackend``
_TermKitBackend_ is a declarative framework allowing the creation of user interface for the terminal. It works on Linux, macOS and Windows thanks to the [TermKit project](https://github.com/migueldeicaza/TermKit).
## Overview
The declarative approach is based on the [Meta package](https://aparokshaui.github.io/meta/) which can be found on [GitHub](https://github.com/AparokshaUI/Meta).
It is powered by [a fork of TermKit for Swift](https://github.com/david-swift/TermKit).

View File

@ -0,0 +1,46 @@
{
"theme": {
"border-radius": "10px",
"button": {
"border-radius": "20px"
},
"color": {
"button-background": "#ea3358",
"button-background-active": "#ea3358",
"button-background-hover": "#fc557a",
"button-text": "#ffffff",
"header": "#7f313b",
"documentation-intro-accent": "var(--color-header)",
"documentation-intro-fill": "radial-gradient(circle at top, var(--color-header) 30%, #000 100%)",
"link": "#ea3358",
"nav-link-color": "#ea3358",
"nav-dark-link-color": "#ea3358",
"tutorials-overview-link": "#fb4469",
"step-background": {
"light": "#fffaff",
"dark": "#302c2d"
},
"step-focused": "#ea3358",
"tabnav-item-border-color": "#ea3358",
"tutorial-background": {
"light": "",
"dark": "#1d1d1f"
},
"tutorials-overview-background": "linear-gradient(180deg, rgba(43,20,23,1) 0%, rgba(41, 3, 8, 0.808) 60%, rgba(0,0,0,1) 100%)",
"fill-light-blue-secondary": "#ea3358",
"fill-blue": "#ea3358",
"figure-blue": "#ea3358",
"standard-blue-documentation-intro-fill": "#ea3358",
"figure-blue": "#ea3358",
"tutorial-hero-background": "#100a0b",
"navigator-item-hover": {
"light": "#ea335815",
"dark": "#7f313b"
}
},
"additionalProperties": "#ea3358",
"tutorial-step": {
"border-radius": "15px"
}
}
}

View File

@ -0,0 +1,60 @@
//
// Button.swift
// TermKitBackend
//
// Created by david-swift on 01.07.2024.
//
import TermKit
/// A simple button widget.
public struct Button: TermKitWidget {
/// The button's label.
var label: String
/// The action.
var action: () -> Void
/// Initialize a button.
/// - Parameters:
/// - The button's label.
/// - The action.
public init(_ label: String, action: @escaping () -> Void) {
self.label = label
self.action = action
}
/// The view storage.
/// - Parameters:
/// - modifiers: Modify views before being updated.
/// - type: The type of the app storage.
public func container<Storage>(
modifiers: [(any AnyView) -> any AnyView],
type: Storage.Type
) -> ViewStorage where Storage: AppStorage {
let button = TermKit.Button(label, clicked: action)
return .init(button)
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - modifiers: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
/// - type: The type of the app storage.
public func update<Storage>(
_ storage: ViewStorage,
modifiers: [(any AnyView) -> any AnyView],
updateProperties: Bool,
type: Storage.Type
) where Storage: AppStorage {
guard let storage = storage.pointer as? TermKit.Button else {
return
}
storage.clicked = { _ in action() }
if updateProperties {
storage.text = label
}
}
}

View File

@ -0,0 +1,64 @@
//
// Checkbox.swift
// TermKitBackend
//
// Created by david-swift on 07.07.2024.
//
import TermKit
/// A simple checkbox widget.
public struct Checkbox: TermKitWidget {
/// The label of the checkbox.
var label: String
/// Whether the checkbox is on.
var isOn: Binding<Bool>
/// Initialize the checkbox.
/// - Parameters:
/// - label: The label.
/// - isOn: Whether the checkbox is on.
public init(_ label: String, isOn: Binding<Bool>) {
self.label = label
self.isOn = isOn
}
/// The view storage.
/// - Parameters:
/// - modifiers: Modify views before being updated.
/// - type: The type of the app storage.
public func container<Storage>(
modifiers: [(any AnyView) -> any AnyView],
type: Storage.Type
) -> ViewStorage where Storage: AppStorage {
let button = TermKit.Checkbox(label, checked: isOn.wrappedValue)
button.toggled = { _ in
isOn.wrappedValue = button.checked
}
return .init(button)
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - modifiers: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
/// - type: The type of the app storage.
public func update<Storage>(
_ storage: ViewStorage,
modifiers: [(any AnyView) -> any AnyView],
updateProperties: Bool,
type: Storage.Type
) where Storage: AppStorage {
guard let storage = storage.pointer as? TermKit.Checkbox, updateProperties else {
return
}
storage.text = label
storage.checked = isOn.wrappedValue
storage.toggled = { _ in
isOn.wrappedValue = storage.checked
}
}
}

View File

@ -0,0 +1,123 @@
//
// Box.swift
// TermKitBackend
//
// Created by david-swift on 10.07.2024.
//
import TermKit
/// A dialog box, either a query, error, or info box.
struct Box: TermKitWidget {
/// The title.
var title: String
/// The message.
var message: String
/// The signal.
var signal: Signal
/// The buttons (info box if none).
var buttons: [Button]
/// The content behind the box.
var content: AnyView
/// Whether it is an error box.
var error = false
/// The identifier for the buttons of a box.
let boxButtonsID = "box-buttons"
/// The view storage.
/// - Parameters:
/// - modifiers: Modify views before being updated.
/// - type: The type of the app storage.
func container<Storage>(
modifiers: [(any AnyView) -> any AnyView],
type: Storage.Type
) -> ViewStorage where Storage: AppStorage {
let storage = ViewStorage(nil)
let contentStorage = content.storage(modifiers: modifiers, type: type)
storage.pointer = contentStorage.pointer
storage.content = [.mainContent: [contentStorage]]
storage.fields[boxButtonsID] = buttons
return storage
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - modifiers: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
/// - type: The type of the app storage.
func update<Storage>(
_ storage: ViewStorage,
modifiers: [(any AnyView) -> any AnyView],
updateProperties: Bool,
type: Storage.Type
) where Storage: AppStorage {
guard let storage = storage.content[.mainContent]?.first else {
return
}
content.updateStorage(storage, modifiers: modifiers, updateProperties: updateProperties, type: type)
storage.fields[boxButtonsID] = buttons
if signal.update {
if buttons.isEmpty {
MessageBox.info(title, message: message)
} else if error {
MessageBox.error(title, message: message, buttons: buttons.map { $0.label }) { selection in
(storage.fields[boxButtonsID] as? [Button])?[safe: selection]?.action()
}
} else {
MessageBox.query(title, message: message, buttons: buttons.map { $0.label }) { selection in
(storage.fields[boxButtonsID] as? [Button])?[safe: selection]?.action()
}
}
}
}
}
extension AnyView {
/// Add a query box.
/// - Parameters:
/// - title: The title.
/// - message: The message.
/// - signal: The box appears when the signal is emitted.
/// - buttons: The buttons.
/// - Returns: The view.
public func queryBox(
_ title: String,
message: String,
signal: Signal,
@Builder<Button> buttons: @escaping () -> [Button]
) -> AnyView {
Box(title: title, message: message, signal: signal, buttons: buttons(), content: self)
}
/// Add an error box.
/// - Parameters:
/// - title: The title.
/// - message: The message.
/// - signal: The box appears when the signal is emitted.
/// - buttons: The buttons.
/// - Returns: The view.
public func errorBox(
_ title: String,
message: String,
signal: Signal,
@Builder<Button> buttons: @escaping () -> [Button]
) -> AnyView {
Box(title: title, message: message, signal: signal, buttons: buttons(), content: self, error: true)
}
/// Add an info box.
/// - Parameters:
/// - title: The title.
/// - message: The message.
/// - signal: The box appears when the signal is emitted.
/// - Returns: The view.
public func infoBox(_ title: String, message: String, signal: Signal) -> AnyView {
Box(title: title, message: message, signal: signal, buttons: [], content: self)
}
}

View File

@ -0,0 +1,64 @@
//
// Frame.swift
// TermKitBackend
//
// Created by david-swift on 06.07.2024.
//
import TermKit
/// A container which draws a frame around its contents.
public struct Frame: TermKitWidget {
/// The frame's label.
var label: String?
/// The content.
var view: Body
/// Initialize a frame.
/// - Parameters:
/// - label: The frame's label.
/// - content: The content.
public init(_ label: String? = nil, @ViewBuilder content: @escaping () -> Body) {
self.label = label
self.view = content()
}
/// The view storage.
/// - Parameters:
/// - modifiers: Modify views before being updated.
/// - type: The type of the app storage.
public func container<Storage>(
modifiers: [(any AnyView) -> any AnyView],
type: Storage.Type
) -> ViewStorage where Storage: AppStorage {
let frame = TermKit.Frame(label)
let subview = view.storage(modifiers: modifiers, type: type)
if let pointer = subview.pointer as? TermKit.View {
frame.addSubview(pointer)
}
return .init(frame, content: [.mainContent: [subview]])
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - modifiers: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
/// - type: The type of the app storage.
public func update<Storage>(
_ storage: ViewStorage,
modifiers: [(any AnyView) -> any AnyView],
updateProperties: Bool,
type: Storage.Type
) where Storage: AppStorage {
if let storage = storage.content[.mainContent]?.first {
view.updateStorage(storage, modifiers: modifiers, updateProperties: updateProperties, type: type)
}
guard let storage = storage.pointer as? TermKit.Frame, updateProperties else {
return
}
storage.title = label
}
}

View File

@ -0,0 +1,64 @@
//
// HStack.swift
// TermKitBackend
//
// Created by david-swift on 06.07.2024.
//
import TermKit
/// Arrange multiple views horizontally.
public struct HStack: Wrapper, TermKitWidget {
/// The content.
var content: Body
/// Initialize the container.
/// - Parameter content: The content.
public init(@ViewBuilder content: @escaping () -> Body) {
self.content = content()
}
/// The view storage.
/// - Parameters:
/// - modifiers: Modify views before being updated.
/// - type: The type of the app storage.
public func container<Storage>(
modifiers: [(any AnyView) -> any AnyView],
type: Storage.Type
) -> ViewStorage where Storage: AppStorage {
let storages = content.storages(modifiers: modifiers, type: type)
if storages.count == 1 {
return .init(storages[0].pointer, content: [.mainContent: storages])
}
let view = View()
for (index, storage) in storages.enumerated() {
if let pointer = storage.pointer as? TermKit.View {
view.addSubview(pointer)
if let previous = (storages[safe: index - 1]?.pointer as? TermKit.View) {
pointer.x = .right(of: previous)
}
}
}
return .init(view, content: [.mainContent: storages])
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - modifiers: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
/// - type: The type of the app storage.
public func update<Storage>(
_ storage: ViewStorage,
modifiers: [(any AnyView) -> any AnyView],
updateProperties: Bool,
type: Storage.Type
) where Storage: AppStorage {
guard let storages = storage.content[.mainContent] else {
return
}
content.update(storages, modifiers: modifiers, updateProperties: updateProperties, type: type)
}
}

View File

@ -0,0 +1,52 @@
//
// Label.swift
// TermKitBackend
//
// Created by david-swift on 07.07.2024.
//
import TermKit
/// A simple label view.
public struct Label: TermKitWidget {
/// The label.
var label: String
/// Initialize a label.
/// - Parameter label: The label.
public init(_ label: String) {
self.label = label
}
/// The view storage.
/// - Parameters:
/// - modifiers: Modify views before being updated.
/// - type: The type of the app storage.
public func container<Storage>(
modifiers: [(any AnyView) -> any AnyView],
type: Storage.Type
) -> ViewStorage where Storage: AppStorage {
let button = TermKit.Label(label)
return .init(button)
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - modifiers: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
/// - type: The type of the app storage.
public func update<Storage>(
_ storage: ViewStorage,
modifiers: [(any AnyView) -> any AnyView],
updateProperties: Bool,
type: Storage.Type
) where Storage: AppStorage {
guard let storage = storage.pointer as? TermKit.Label, updateProperties else {
return
}
storage.text = label
}
}

View File

@ -0,0 +1,69 @@
//
// ListView.swift
// TermKitBackend
//
// Created by david-swift on 06.07.2024.
//
import TermKit
/// A list view contains multiple clickable rows.
public struct ListView<Element>: TermKitWidget where Element: CustomStringConvertible {
/// The rows.
var items: [Element]
/// Execute when a row gets clicked.
var activate: (Element) -> Void
/// Initialize the list view.
/// - Parameters:
/// - items: The rows.
/// - activate: Execute when a row gets clicked.
public init(_ items: [Element], activate: @escaping (Element) -> Void = { _ in }) {
self.items = items
self.activate = activate
}
/// The view storage.
/// - Parameters:
/// - modifiers: Modify views before being updated.
/// - type: The type of the app storage.
public func container<Storage>(
modifiers: [(any AnyView) -> any AnyView],
type: Storage.Type
) -> ViewStorage where Storage: AppStorage {
let list = TermKit.ListView(items: items.map { $0.description })
setClosure(list: list)
return .init(list)
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - modifiers: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
/// - type: The type of the app storage.
public func update<Storage>(
_ storage: ViewStorage,
modifiers: [(any AnyView) -> any AnyView],
updateProperties: Bool,
type: Storage.Type
) where Storage: AppStorage {
guard let list = storage.pointer as? TermKit.ListView else {
return
}
setClosure(list: list)
}
/// Set the closure executed when the row gets clicked.
/// - Parameter list: The list view object.
func setClosure(list: TermKit.ListView) {
list.activate = { index in
if let item = items[safe: index] {
activate(item)
}
return false
}
}
}

View File

@ -0,0 +1,58 @@
//
// ProgressBar.swift
// TermKitBackend
//
// Created by david-swift on 07.07.2024.
//
import TermKit
/// A simple progress bar view.
public struct ProgressBar: TermKitWidget {
/// The current value.
var value: Double
/// The maximum value.
var max: Double
/// Initialize a progress bar.
/// - Parameters:
/// - value: The current value.
/// - max: The maximum value.
public init(value: Double, max: Double = 1) {
self.value = value
self.max = max
}
/// The view storage.
/// - Parameters:
/// - modifiers: Modify views before being updated.
/// - type: The type of the app storage.
public func container<Storage>(
modifiers: [(any AnyView) -> any AnyView],
type: Storage.Type
) -> ViewStorage where Storage: AppStorage {
let bar = TermKit.ProgressBar()
bar.fraction = .init(value / max)
return .init(bar)
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - modifiers: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
/// - type: The type of the app storage.
public func update<Storage>(
_ storage: ViewStorage,
modifiers: [(any AnyView) -> any AnyView],
updateProperties: Bool,
type: Storage.Type
) where Storage: AppStorage {
guard let storage = storage.pointer as? TermKit.ProgressBar, updateProperties else {
return
}
storage.fraction = .init(value / max)
}
}

View File

@ -0,0 +1,64 @@
//
// ScrollView.swift
// TermKitBackend
//
// Created by david-swift on 07.07.2024.
//
import TermKit
/// A scroll view container.
public struct ScrollView: TermKitWidget {
/// The content.
var content: Body
/// Initialize the container.
/// - Parameter content: The content.
public init(@ViewBuilder content: @escaping () -> Body) {
self.content = content()
}
/// The view storage.
/// - Parameters:
/// - modifiers: Modify views before being updated.
/// - type: The type of the app storage.
public func container<Storage>(
modifiers: [(any AnyView) -> any AnyView],
type: Storage.Type
) -> ViewStorage where Storage: AppStorage {
let storages = content.storages(modifiers: modifiers, type: type)
if storages.count == 1 {
return .init(storages[0].pointer, content: [.mainContent: storages])
}
let view = TermKit.ScrollView()
for (index, storage) in storages.enumerated() {
if let pointer = storage.pointer as? TermKit.View {
view.addSubview(pointer)
if let previous = (storages[safe: index - 1]?.pointer as? TermKit.View) {
pointer.y = .bottom(of: previous)
}
}
}
return .init(view, content: [.mainContent: storages])
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - modifiers: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
/// - type: The type of the app storage.
public func update<Storage>(
_ storage: ViewStorage,
modifiers: [(any AnyView) -> any AnyView],
updateProperties: Bool,
type: Storage.Type
) where Storage: AppStorage {
guard let storages = storage.content[.mainContent] else {
return
}
content.update(storages, modifiers: modifiers, updateProperties: updateProperties, type: type)
}
}

View File

@ -0,0 +1,84 @@
//
// Button.swift
// TermKitBackend
//
// Created by david-swift on 07.07.2024.
//
import TermKit
/// A simple text field view.
public struct TextField: TermKitWidget {
/// The text.
var text: Binding<String>
/// Whether the text field is secret.
var secret = false
/// The identifier for the closure.
let closureID = "closure"
/// Initialize the text field.
/// - Parameter text: The text.
public init(text: Binding<String>) {
self.text = text
}
/// The view storage.
/// - Parameters:
/// - modifiers: Modify views before being updated.
/// - type: The type of the app storage.
public func container<Storage>(
modifiers: [(any AnyView) -> any AnyView],
type: Storage.Type
) -> ViewStorage where Storage: AppStorage {
let field = TermKit.TextField(text.wrappedValue)
let storage = ViewStorage(field)
field.secret = secret
field.textChanged = { _, _ in
(storage.fields[closureID] as? () -> Void)?()
}
storage.fields[closureID] = {
if field.text != text.wrappedValue {
text.wrappedValue = field.text
}
}
return storage
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - modifiers: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
/// - type: The type of the app storage.
public func update<Storage>(
_ storage: ViewStorage,
modifiers: [(any AnyView) -> any AnyView],
updateProperties: Bool,
type: Storage.Type
) where Storage: AppStorage {
guard let field = storage.pointer as? TermKit.TextField else {
return
}
storage.fields[closureID] = {
if field.text != text.wrappedValue {
text.wrappedValue = field.text
}
}
if updateProperties {
field.secret = secret
if field.text != text.wrappedValue {
field.text = text.wrappedValue
}
}
}
/// Set whether the text field is secret.
/// - Parameter secret: Whether the text field is secret.
/// - Returns: The view.
public func secret(_ secret: Bool) -> Self {
modify { $0.secret = secret }
}
}

View File

@ -0,0 +1,64 @@
//
// VStack.swift
// TermKitBackend
//
// Created by david-swift on 01.07.2024.
//
import TermKit
/// Arrange multiple views vertically.
public struct VStack: Wrapper, TermKitWidget {
/// The content.
var content: Body
/// Initialize the container.
/// - Parameter content: The content.
public init(@ViewBuilder content: @escaping () -> Body) {
self.content = content()
}
/// The view storage.
/// - Parameters:
/// - modifiers: Modify views before being updated.
/// - type: The type of the app storage.
public func container<Storage>(
modifiers: [(any AnyView) -> any AnyView],
type: Storage.Type
) -> ViewStorage where Storage: AppStorage {
let storages = content.storages(modifiers: modifiers, type: type)
if storages.count == 1 {
return .init(storages[0].pointer, content: [.mainContent: storages])
}
let view = View()
for (index, storage) in storages.enumerated() {
if let pointer = storage.pointer as? TermKit.View {
view.addSubview(pointer)
if let previous = (storages[safe: index - 1]?.pointer as? TermKit.View) {
pointer.y = .bottom(of: previous)
}
}
}
return .init(view, content: [.mainContent: storages])
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - modifiers: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
/// - type: The type of the app storage.
public func update<Storage>(
_ storage: ViewStorage,
modifiers: [(any AnyView) -> any AnyView],
updateProperties: Bool,
type: Storage.Type
) where Storage: AppStorage {
guard let storages = storage.content[.mainContent] else {
return
}
content.update(storages, modifiers: modifiers, updateProperties: updateProperties, type: type)
}
}

View File

@ -0,0 +1,61 @@
//
// ZStack.swift
// TermKitBackend
//
// Created by david-swift on 06.07.2024.
//
import TermKit
/// Arrange multiple views behind each other.
public struct ZStack: Wrapper, TermKitWidget {
/// The content.
var content: Body
/// Initialize the container.
/// - Parameter content: The content.
public init(@ViewBuilder content: @escaping () -> Body) {
self.content = content()
}
/// The view storage.
/// - Parameters:
/// - modifiers: Modify views before being updated.
/// - type: The type of the app storage.
public func container<Storage>(
modifiers: [(any AnyView) -> any AnyView],
type: Storage.Type
) -> ViewStorage where Storage: AppStorage {
let storages = content.reversed().storages(modifiers: modifiers, type: type)
if storages.count == 1 {
return .init(storages[0].pointer, content: [.mainContent: storages])
}
let view = View()
for storage in storages {
if let pointer = storage.pointer as? TermKit.View {
view.addSubview(pointer)
}
}
return .init(view, content: [.mainContent: storages])
}
/// Update the stored content.
/// - Parameters:
/// - storage: The storage to update.
/// - modifiers: Modify views before being updated
/// - updateProperties: Whether to update the view's properties.
/// - type: The type of the app storage.
public func update<Storage>(
_ storage: ViewStorage,
modifiers: [(any AnyView) -> any AnyView],
updateProperties: Bool,
type: Storage.Type
) where Storage: AppStorage {
guard let storages = storage.content[.mainContent] else {
return
}
content.reversed().update(storages, modifiers: modifiers, updateProperties: updateProperties, type: type)
}
}

View File

@ -0,0 +1,141 @@
//
// TestApp.swift
// TermKitBackend
//
// Created by david-swift on 01.07.2024.
//
import TermKitBackend
@main
struct TestApp: App {
@State private var about: Signal = .init()
@State private var state = false
let id = "io.github.AparokshaUI.TestApp"
var app: TermKitApp!
var scene: Scene {
menuBar
Window {
VStack {
Demos()
.hcenter()
Controls()
.hcenter()
}
.frame(height: 14)
.vcenter()
.infoBox("About TermKitBackend", message: aboutInfo, signal: about)
}
}
var menuBar: MenuBar {
.init {
Menu("File") {
Button("_About TermKitBackend") {
about.signal()
}
Button("_Quit") {
app.quit()
}
}
Menu("_Actions") {
Button("_Hello, world!") { }
}
}
}
var aboutInfo: String {
"""
This is a sample backend for the Meta package of the Aparoksha project.
It is based on TermKit, the terminal UI toolkit for Swift.
"""
}
}
struct Demos: View {
@State private var state = false
@State private var dialog: Signal = .init()
@State private var error: Signal = .init()
let demos = Demo.allCases
var view: Body {
Frame("Demos (state \(state ? 2 : 1))") {
ListView(demos) { $0.action(state: $state, dialog: $dialog, error: $error) }
}
.frame(width: 40, height: 7)
.queryBox("Dialog Demo", message: "Choose wisely", signal: dialog) {
Button("Yes") {
state.toggle()
}
Button("No") { }
}
.errorBox("Error Demo", message: "This is an error message", signal: error) {
Button("Close") { }
}
}
}
struct Controls: View {
@State private var isOn = false
@State private var fraction = 0
@State private var text = "Controls"
var view: Body {
Frame(text) {
HStack {
Button("Button (progress)") {
if fraction == 10 {
fraction = 0
} else {
fraction += 1
}
}
Button("Button (text)") {
text = "Hello"
}
}
.frame(height: 1)
Checkbox(isOn ? "On" : "Off", isOn: $isOn)
TextField(text: $text)
.secret(isOn)
ProgressBar(value: .init(fraction), max: 10)
}
.frame(width: 40, height: 7)
}
}
enum Demo: String, CaseIterable, CustomStringConvertible {
case state
case dialog
case error
var description: String {
switch self {
case .state:
"Toggle State"
default:
rawValue.capitalized
}
}
func action(state: Binding<Bool>, dialog: Binding<Signal>, error: Binding<Signal>) {
switch self {
case .state:
state.wrappedValue.toggle()
case .dialog:
dialog.wrappedValue.signal()
case .error:
error.wrappedValue.signal()
}
}
}