Initial commit
This commit is contained in:
commit
8e79a2bd23
40
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
40
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal 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.
|
36
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
36
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal 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
11
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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
45
.github/workflows/docs.yml
vendored
Normal 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
30
.github/workflows/swiftlint.yml
vendored
Normal 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
10
.gitignore
vendored
Normal 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
165
.swiftlint.yml
Normal 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
34
CONTRIBUTING.md
Normal 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
21
LICENSE.md
Normal 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
35
Package.swift
Normal 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
45
README.md
Normal 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)
|
55
Sources/TermKitBackend/Menu/Menu.swift
Normal file
55
Sources/TermKitBackend/Menu/Menu.swift
Normal 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: [:])
|
||||
}
|
||||
|
||||
}
|
50
Sources/TermKitBackend/Model/Extensions/AnyView.swift
Normal file
50
Sources/TermKitBackend/Model/Extensions/AnyView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
47
Sources/TermKitBackend/Model/Extensions/Button+.swift
Normal file
47
Sources/TermKitBackend/Model/Extensions/Button+.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
47
Sources/TermKitBackend/Model/TermKitApp.swift
Normal file
47
Sources/TermKitBackend/Model/TermKitApp.swift
Normal 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()
|
||||
}
|
||||
|
||||
}
|
9
Sources/TermKitBackend/Model/TermKitSceneElement.swift
Normal file
9
Sources/TermKitBackend/Model/TermKitSceneElement.swift
Normal 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 { }
|
9
Sources/TermKitBackend/Model/TermKitWidget.swift
Normal file
9
Sources/TermKitBackend/Model/TermKitWidget.swift
Normal 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 { }
|
62
Sources/TermKitBackend/Scene/MenuBar.swift
Normal file
62
Sources/TermKitBackend/Scene/MenuBar.swift
Normal 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()
|
||||
}
|
||||
|
||||
}
|
68
Sources/TermKitBackend/Scene/Window.swift
Normal file
68
Sources/TermKitBackend/Scene/Window.swift
Normal 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()
|
||||
}
|
||||
|
||||
}
|
@ -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).
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
60
Sources/TermKitBackend/View/Button.swift
Normal file
60
Sources/TermKitBackend/View/Button.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
64
Sources/TermKitBackend/View/Checkbox.swift
Normal file
64
Sources/TermKitBackend/View/Checkbox.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
123
Sources/TermKitBackend/View/Dialogs/Box.swift
Normal file
123
Sources/TermKitBackend/View/Dialogs/Box.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
64
Sources/TermKitBackend/View/Frame.swift
Normal file
64
Sources/TermKitBackend/View/Frame.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
64
Sources/TermKitBackend/View/HStack.swift
Normal file
64
Sources/TermKitBackend/View/HStack.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
52
Sources/TermKitBackend/View/Label.swift
Normal file
52
Sources/TermKitBackend/View/Label.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
69
Sources/TermKitBackend/View/ListView.swift
Normal file
69
Sources/TermKitBackend/View/ListView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
58
Sources/TermKitBackend/View/ProgressBar.swift
Normal file
58
Sources/TermKitBackend/View/ProgressBar.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
64
Sources/TermKitBackend/View/ScrollView.swift
Normal file
64
Sources/TermKitBackend/View/ScrollView.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
84
Sources/TermKitBackend/View/TextField.swift
Normal file
84
Sources/TermKitBackend/View/TextField.swift
Normal 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 }
|
||||
}
|
||||
|
||||
}
|
64
Sources/TermKitBackend/View/VStack.swift
Normal file
64
Sources/TermKitBackend/View/VStack.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
61
Sources/TermKitBackend/View/ZStack.swift
Normal file
61
Sources/TermKitBackend/View/ZStack.swift
Normal 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)
|
||||
}
|
||||
|
||||
}
|
141
Sources/TestApp/TestApp.swift
Normal file
141
Sources/TestApp/TestApp.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user