├── .editorconfig
├── .github
├── FUNDING.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── Help Book.xctemplate
├── TemplateInfo.plist
├── helpbook.js
└── index.html
├── LICENSE
├── Package.resolved
├── Package.swift
├── Projects
└── WelpExample
│ ├── WelpExample.xcodeproj
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── WelpExample
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
│ ├── Base.lproj
│ └── MainMenu.xib
│ └── WelpExample.entitlements
├── README.md
├── Sources
├── Welp
│ └── Hiutil.swift
├── WelpBook
│ ├── Documentation.docc
│ │ └── WelpBook.md
│ └── Welp.swift
└── clitool
│ └── main.swift
└── Tests
└── WelpTests
└── WelpTests.swift
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | indent_size = 4
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [mattmassicotte]
2 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths-ignore:
8 | - 'README.md'
9 | - 'CODE_OF_CONDUCT.md'
10 | - '.editorconfig'
11 | - '.spi.yml'
12 | pull_request:
13 | branches:
14 | - main
15 |
16 | jobs:
17 | test:
18 | name: Test
19 | runs-on: macOS-14
20 | env:
21 | DEVELOPER_DIR: /Applications/Xcode_16.0.app/Contents/Developer
22 | steps:
23 | - uses: actions/checkout@v4
24 | - name: Validate template
25 | run: plutil -lint "Help Book.xctemplate/TemplateInfo.plist"
26 | - name: Test
27 | run: set -o pipefail && xcodebuild -scheme Welp-Package -destination "platform=macOS" test | xcbeautify
28 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/config/registries.json
8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 | .netrc
10 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 |
2 | # Contributor Covenant Code of Conduct
3 |
4 | ## Our Pledge
5 |
6 | We as members, contributors, and leaders pledge to make participation in our
7 | community a harassment-free experience for everyone, regardless of age, body
8 | size, visible or invisible disability, ethnicity, sex characteristics, gender
9 | identity and expression, level of experience, education, socio-economic status,
10 | nationality, personal appearance, race, caste, color, religion, or sexual
11 | identity and orientation.
12 |
13 | We pledge to act and interact in ways that contribute to an open, welcoming,
14 | diverse, inclusive, and healthy community.
15 |
16 | ## Our Standards
17 |
18 | Examples of behavior that contributes to a positive environment for our
19 | community include:
20 |
21 | * Demonstrating empathy and kindness toward other people
22 | * Being respectful of differing opinions, viewpoints, and experiences
23 | * Giving and gracefully accepting constructive feedback
24 | * Accepting responsibility and apologizing to those affected by our mistakes,
25 | and learning from the experience
26 | * Focusing on what is best not just for us as individuals, but for the overall
27 | community
28 |
29 | Examples of unacceptable behavior include:
30 |
31 | * The use of sexualized language or imagery, and sexual attention or advances of
32 | any kind
33 | * Trolling, insulting or derogatory comments, and personal or political attacks
34 | * Public or private harassment
35 | * Publishing others' private information, such as a physical or email address,
36 | without their explicit permission
37 | * Other conduct which could reasonably be considered inappropriate in a
38 | professional setting
39 |
40 | ## Enforcement Responsibilities
41 |
42 | Community leaders are responsible for clarifying and enforcing our standards of
43 | acceptable behavior and will take appropriate and fair corrective action in
44 | response to any behavior that they deem inappropriate, threatening, offensive,
45 | or harmful.
46 |
47 | Community leaders have the right and responsibility to remove, edit, or reject
48 | comments, commits, code, wiki edits, issues, and other contributions that are
49 | not aligned to this Code of Conduct, and will communicate reasons for moderation
50 | decisions when appropriate.
51 |
52 | ## Scope
53 |
54 | This Code of Conduct applies within all community spaces, and also applies when
55 | an individual is officially representing the community in public spaces.
56 | Examples of representing our community include using an official e-mail address,
57 | posting via an official social media account, or acting as an appointed
58 | representative at an online or offline event.
59 |
60 | ## Enforcement
61 |
62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
63 | reported to the community leaders responsible for enforcement at
64 | support@chimehq.com.
65 | All complaints will be reviewed and investigated promptly and fairly.
66 |
67 | All community leaders are obligated to respect the privacy and security of the
68 | reporter of any incident.
69 |
70 | ## Enforcement Guidelines
71 |
72 | Community leaders will follow these Community Impact Guidelines in determining
73 | the consequences for any action they deem in violation of this Code of Conduct:
74 |
75 | ### 1. Correction
76 |
77 | **Community Impact**: Use of inappropriate language or other behavior deemed
78 | unprofessional or unwelcome in the community.
79 |
80 | **Consequence**: A private, written warning from community leaders, providing
81 | clarity around the nature of the violation and an explanation of why the
82 | behavior was inappropriate. A public apology may be requested.
83 |
84 | ### 2. Warning
85 |
86 | **Community Impact**: A violation through a single incident or series of
87 | actions.
88 |
89 | **Consequence**: A warning with consequences for continued behavior. No
90 | interaction with the people involved, including unsolicited interaction with
91 | those enforcing the Code of Conduct, for a specified period of time. This
92 | includes avoiding interactions in community spaces as well as external channels
93 | like social media. Violating these terms may lead to a temporary or permanent
94 | ban.
95 |
96 | ### 3. Temporary Ban
97 |
98 | **Community Impact**: A serious violation of community standards, including
99 | sustained inappropriate behavior.
100 |
101 | **Consequence**: A temporary ban from any sort of interaction or public
102 | communication with the community for a specified period of time. No public or
103 | private interaction with the people involved, including unsolicited interaction
104 | with those enforcing the Code of Conduct, is allowed during this period.
105 | Violating these terms may lead to a permanent ban.
106 |
107 | ### 4. Permanent Ban
108 |
109 | **Community Impact**: Demonstrating a pattern of violation of community
110 | standards, including sustained inappropriate behavior, harassment of an
111 | individual, or aggression toward or disparagement of classes of individuals.
112 |
113 | **Consequence**: A permanent ban from any sort of public interaction within the
114 | community.
115 |
116 | ## Attribution
117 |
118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
119 | version 2.1, available at
120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
121 |
122 | Community Impact Guidelines were inspired by
123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
124 |
125 | For answers to common questions about this code of conduct, see the FAQ at
126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
127 | [https://www.contributor-covenant.org/translations][translations].
128 |
129 | [homepage]: https://www.contributor-covenant.org
130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
131 | [Mozilla CoC]: https://github.com/mozilla/diversity
132 | [FAQ]: https://www.contributor-covenant.org/faq
133 | [translations]: https://www.contributor-covenant.org/translations
134 |
--------------------------------------------------------------------------------
/Help Book.xctemplate/TemplateInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Kind
6 | Xcode.Xcode3.ProjectTemplateUnitKind
7 | Identifier
8 | com.chimehq.HelpBookBundle
9 | Ancestors
10 |
11 | com.apple.dt.unit.bundleBase
12 | com.apple.dt.unit.osxBase
13 |
14 | Concrete
15 |
16 | DefaultCompletionName
17 | HelpBook
18 | Description
19 | This template creates an empty macOS help book bundle.
20 | TargetOnly
21 |
22 | AssociatedTargetSpecification
23 |
24 | PopUpTitle
25 | Embed in Application:
26 | PopUpDescription
27 | The application target that will host this new help book. The application will be set up to embed the new framework.
28 | AllowableProductTypes
29 |
30 | com.apple.product-type.application
31 |
32 | AssociatedTargetIsDependent
33 |
34 |
35 |
36 | Image
37 |
38 | SystemSymbolName
39 | book
40 |
41 | Project
42 |
43 | Configurations
44 |
45 | Debug
46 |
47 | Release
48 |
49 |
50 |
51 | Targets
52 |
53 |
54 | ProductType
55 | com.apple.product-type.bundle
56 | TargetIdentifier
57 | com.apple.dt.bundleTarget
58 | SharedSettings
59 |
60 | INSTALL_PATH
61 | $(LOCAL_LIBRARY_DIR)/Bundles
62 | SKIP_INSTALL
63 | YES
64 | WRAPPER_EXTENSION
65 | help
66 | COMBINE_HIDPI_IMAGES
67 | YES
68 | INFOPLIST_KEY_NSPrincipalClass
69 |
70 | INFOPLIST_KEY_NSHumanReadableCopyright
71 | ___COPYRIGHT___
72 | INFOPLIST_FILE
73 | ___PACKAGENAME___/Info.plist
74 | ENABLE_USER_SCRIPT_SANDBOXING
75 | NO
76 |
77 | BuildPhases
78 |
79 |
80 | Class
81 | Sources
82 |
83 |
84 | Class
85 | Frameworks
86 |
87 |
88 | Class
89 | Resources
90 |
91 |
92 | SortOrder
93 | 0
94 | Name
95 | Re-build Index
96 | Class
97 | ShellScript
98 | ShellPath
99 | /bin/sh
100 | ShellScript
101 |
114 |
115 |
116 |
117 |
118 | Nodes
119 |
120 | Info.plist:helpBook
121 | Base.lproj/index.html
122 | helpbook.js
123 |
124 | Definitions
125 |
126 | Info.plist:helpBook
127 | HPDBookTitle
128 | ___PACKAGENAMEASXML___ Help
129 | HPDBookAccessPath
130 | index.html
131 | HPDBookKBProduct
132 | ___PACKAGENAMEASXML___1
133 | HPDBookCSIndexPath
134 | ___PACKAGENAMEASXML___.cshelpindex
135 | HPDBookIndexPath
136 | ___PACKAGENAMEASXML___.helpindex
137 | HPDBookType
138 | 3
139 | CFBundleSignature
140 | hbwr
141 | ]]>
142 | Base.lproj/index.html
143 |
144 | Path
145 | index.html
146 |
147 | helpbook.js
148 |
149 | Path
150 | helpbook.js
151 |
152 |
153 |
154 |
155 |
--------------------------------------------------------------------------------
/Help Book.xctemplate/helpbook.js:
--------------------------------------------------------------------------------
1 | window.onload = function() {
2 | setupTOC();
3 | }
4 |
5 | function isHelpViewerAvailable() {
6 | return "HelpViewer" in window && "showTOCButton" in window.HelpViewer;
7 | }
8 |
9 | function setupTOC() {
10 | if (!isHelpViewerAvailable()) {
11 | return;
12 | }
13 |
14 | window.setTimeout(function () {
15 | window.HelpViewer.showTOCButton(true, toggleNavigation, toggleNavigation);
16 | }, 100);
17 | }
18 |
19 | function toggleNavigation() {
20 | }
21 |
--------------------------------------------------------------------------------
/Help Book.xctemplate/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Example Help
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | Help!
17 |
18 |
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2022, Chime
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "pins" : [
3 | {
4 | "identity" : "swift-argument-parser",
5 | "kind" : "remoteSourceControl",
6 | "location" : "https://github.com/apple/swift-argument-parser",
7 | "state" : {
8 | "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b",
9 | "version" : "1.4.0"
10 | }
11 | }
12 | ],
13 | "version" : 2
14 | }
15 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.8
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "Welp",
7 | platforms: [.macOS(.v10_15)],
8 | products: [
9 | .executable(name: "welp", targets: ["clitool"]),
10 | .library(name: "Welp", targets: ["Welp"]),
11 | .library(name: "WelpBook", targets: ["WelpBook"]),
12 | ],
13 | dependencies: [
14 | .package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.3"),
15 | ],
16 | targets: [
17 | .executableTarget(name: "clitool", dependencies: [
18 | "Welp",
19 | .product(name: "ArgumentParser", package: "swift-argument-parser"),
20 | ]),
21 | .target(name: "Welp", dependencies: []),
22 | .target(name: "WelpBook", dependencies: []),
23 | .testTarget(name: "WelpTests", dependencies: ["Welp"]),
24 | ]
25 | )
26 |
27 | let swiftSettings: [SwiftSetting] = [
28 | .enableExperimentalFeature("StrictConcurrency"),
29 | .enableExperimentalFeature("GlobalActorIsolatedTypesUsability"),
30 | ]
31 |
32 | for target in package.targets {
33 | var settings = target.swiftSettings ?? []
34 | settings.append(contentsOf: swiftSettings)
35 | target.swiftSettings = settings
36 | }
37 |
--------------------------------------------------------------------------------
/Projects/WelpExample/WelpExample.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | C97918B12A9D330B0046EAF1 /* Welp in Frameworks */ = {isa = PBXBuildFile; productRef = C97918B02A9D330B0046EAF1 /* Welp */; };
11 | C9D3700B2900C0150050984D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D3700A2900C0150050984D /* AppDelegate.swift */; };
12 | C9D3700D2900C0160050984D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C9D3700C2900C0160050984D /* Assets.xcassets */; };
13 | C9D370102900C0160050984D /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = C9D3700E2900C0160050984D /* MainMenu.xib */; };
14 | /* End PBXBuildFile section */
15 |
16 | /* Begin PBXFileReference section */
17 | C97918AC2A9D32F40046EAF1 /* Welp */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Welp; path = ../..; sourceTree = ""; };
18 | C9D370072900C0150050984D /* WelpExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WelpExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
19 | C9D3700A2900C0150050984D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
20 | C9D3700C2900C0160050984D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
21 | C9D3700F2900C0160050984D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; };
22 | C9D370112900C0160050984D /* WelpExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WelpExample.entitlements; sourceTree = ""; };
23 | /* End PBXFileReference section */
24 |
25 | /* Begin PBXFrameworksBuildPhase section */
26 | C9D370042900C0150050984D /* Frameworks */ = {
27 | isa = PBXFrameworksBuildPhase;
28 | buildActionMask = 2147483647;
29 | files = (
30 | C97918B12A9D330B0046EAF1 /* Welp in Frameworks */,
31 | );
32 | runOnlyForDeploymentPostprocessing = 0;
33 | };
34 | /* End PBXFrameworksBuildPhase section */
35 |
36 | /* Begin PBXGroup section */
37 | C97918AF2A9D330B0046EAF1 /* Frameworks */ = {
38 | isa = PBXGroup;
39 | children = (
40 | );
41 | name = Frameworks;
42 | sourceTree = "";
43 | };
44 | C9D36FFE2900C0150050984D = {
45 | isa = PBXGroup;
46 | children = (
47 | C97918AC2A9D32F40046EAF1 /* Welp */,
48 | C9D370092900C0150050984D /* WelpExample */,
49 | C9D370082900C0150050984D /* Products */,
50 | C97918AF2A9D330B0046EAF1 /* Frameworks */,
51 | );
52 | sourceTree = "";
53 | };
54 | C9D370082900C0150050984D /* Products */ = {
55 | isa = PBXGroup;
56 | children = (
57 | C9D370072900C0150050984D /* WelpExample.app */,
58 | );
59 | name = Products;
60 | sourceTree = "";
61 | };
62 | C9D370092900C0150050984D /* WelpExample */ = {
63 | isa = PBXGroup;
64 | children = (
65 | C9D3700A2900C0150050984D /* AppDelegate.swift */,
66 | C9D3700C2900C0160050984D /* Assets.xcassets */,
67 | C9D3700E2900C0160050984D /* MainMenu.xib */,
68 | C9D370112900C0160050984D /* WelpExample.entitlements */,
69 | );
70 | path = WelpExample;
71 | sourceTree = "";
72 | };
73 | /* End PBXGroup section */
74 |
75 | /* Begin PBXNativeTarget section */
76 | C9D370062900C0150050984D /* WelpExample */ = {
77 | isa = PBXNativeTarget;
78 | buildConfigurationList = C9D370142900C0160050984D /* Build configuration list for PBXNativeTarget "WelpExample" */;
79 | buildPhases = (
80 | C9D370032900C0150050984D /* Sources */,
81 | C9D370042900C0150050984D /* Frameworks */,
82 | C9D370052900C0150050984D /* Resources */,
83 | );
84 | buildRules = (
85 | );
86 | dependencies = (
87 | C97918AE2A9D33070046EAF1 /* PBXTargetDependency */,
88 | );
89 | name = WelpExample;
90 | packageProductDependencies = (
91 | C97918B02A9D330B0046EAF1 /* Welp */,
92 | );
93 | productName = WelpExample;
94 | productReference = C9D370072900C0150050984D /* WelpExample.app */;
95 | productType = "com.apple.product-type.application";
96 | };
97 | /* End PBXNativeTarget section */
98 |
99 | /* Begin PBXProject section */
100 | C9D36FFF2900C0150050984D /* Project object */ = {
101 | isa = PBXProject;
102 | attributes = {
103 | BuildIndependentTargetsInParallel = 1;
104 | LastSwiftUpdateCheck = 1410;
105 | LastUpgradeCheck = 1410;
106 | TargetAttributes = {
107 | C9D370062900C0150050984D = {
108 | CreatedOnToolsVersion = 14.1;
109 | };
110 | };
111 | };
112 | buildConfigurationList = C9D370022900C0150050984D /* Build configuration list for PBXProject "WelpExample" */;
113 | compatibilityVersion = "Xcode 14.0";
114 | developmentRegion = en;
115 | hasScannedForEncodings = 0;
116 | knownRegions = (
117 | en,
118 | Base,
119 | );
120 | mainGroup = C9D36FFE2900C0150050984D;
121 | productRefGroup = C9D370082900C0150050984D /* Products */;
122 | projectDirPath = "";
123 | projectRoot = "";
124 | targets = (
125 | C9D370062900C0150050984D /* WelpExample */,
126 | );
127 | };
128 | /* End PBXProject section */
129 |
130 | /* Begin PBXResourcesBuildPhase section */
131 | C9D370052900C0150050984D /* Resources */ = {
132 | isa = PBXResourcesBuildPhase;
133 | buildActionMask = 2147483647;
134 | files = (
135 | C9D3700D2900C0160050984D /* Assets.xcassets in Resources */,
136 | C9D370102900C0160050984D /* MainMenu.xib in Resources */,
137 | );
138 | runOnlyForDeploymentPostprocessing = 0;
139 | };
140 | /* End PBXResourcesBuildPhase section */
141 |
142 | /* Begin PBXSourcesBuildPhase section */
143 | C9D370032900C0150050984D /* Sources */ = {
144 | isa = PBXSourcesBuildPhase;
145 | buildActionMask = 2147483647;
146 | files = (
147 | C9D3700B2900C0150050984D /* AppDelegate.swift in Sources */,
148 | );
149 | runOnlyForDeploymentPostprocessing = 0;
150 | };
151 | /* End PBXSourcesBuildPhase section */
152 |
153 | /* Begin PBXTargetDependency section */
154 | C97918AE2A9D33070046EAF1 /* PBXTargetDependency */ = {
155 | isa = PBXTargetDependency;
156 | productRef = C97918AD2A9D33070046EAF1 /* Welp */;
157 | };
158 | /* End PBXTargetDependency section */
159 |
160 | /* Begin PBXVariantGroup section */
161 | C9D3700E2900C0160050984D /* MainMenu.xib */ = {
162 | isa = PBXVariantGroup;
163 | children = (
164 | C9D3700F2900C0160050984D /* Base */,
165 | );
166 | name = MainMenu.xib;
167 | sourceTree = "";
168 | };
169 | /* End PBXVariantGroup section */
170 |
171 | /* Begin XCBuildConfiguration section */
172 | C9D370122900C0160050984D /* Debug */ = {
173 | isa = XCBuildConfiguration;
174 | buildSettings = {
175 | ALWAYS_SEARCH_USER_PATHS = NO;
176 | CLANG_ANALYZER_NONNULL = YES;
177 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
178 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
179 | CLANG_ENABLE_MODULES = YES;
180 | CLANG_ENABLE_OBJC_ARC = YES;
181 | CLANG_ENABLE_OBJC_WEAK = YES;
182 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
183 | CLANG_WARN_BOOL_CONVERSION = YES;
184 | CLANG_WARN_COMMA = YES;
185 | CLANG_WARN_CONSTANT_CONVERSION = YES;
186 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
187 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
188 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
189 | CLANG_WARN_EMPTY_BODY = YES;
190 | CLANG_WARN_ENUM_CONVERSION = YES;
191 | CLANG_WARN_INFINITE_RECURSION = YES;
192 | CLANG_WARN_INT_CONVERSION = YES;
193 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
194 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
195 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
196 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
197 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
198 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
199 | CLANG_WARN_STRICT_PROTOTYPES = YES;
200 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
201 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
202 | CLANG_WARN_UNREACHABLE_CODE = YES;
203 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
204 | COPY_PHASE_STRIP = NO;
205 | DEBUG_INFORMATION_FORMAT = dwarf;
206 | ENABLE_STRICT_OBJC_MSGSEND = YES;
207 | ENABLE_TESTABILITY = YES;
208 | GCC_C_LANGUAGE_STANDARD = gnu11;
209 | GCC_DYNAMIC_NO_PIC = NO;
210 | GCC_NO_COMMON_BLOCKS = YES;
211 | GCC_OPTIMIZATION_LEVEL = 0;
212 | GCC_PREPROCESSOR_DEFINITIONS = (
213 | "DEBUG=1",
214 | "$(inherited)",
215 | );
216 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
217 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
218 | GCC_WARN_UNDECLARED_SELECTOR = YES;
219 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
220 | GCC_WARN_UNUSED_FUNCTION = YES;
221 | GCC_WARN_UNUSED_VARIABLE = YES;
222 | MACOSX_DEPLOYMENT_TARGET = 13.0;
223 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
224 | MTL_FAST_MATH = YES;
225 | ONLY_ACTIVE_ARCH = YES;
226 | SDKROOT = macosx;
227 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
228 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
229 | };
230 | name = Debug;
231 | };
232 | C9D370132900C0160050984D /* Release */ = {
233 | isa = XCBuildConfiguration;
234 | buildSettings = {
235 | ALWAYS_SEARCH_USER_PATHS = NO;
236 | CLANG_ANALYZER_NONNULL = YES;
237 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
238 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
239 | CLANG_ENABLE_MODULES = YES;
240 | CLANG_ENABLE_OBJC_ARC = YES;
241 | CLANG_ENABLE_OBJC_WEAK = YES;
242 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
243 | CLANG_WARN_BOOL_CONVERSION = YES;
244 | CLANG_WARN_COMMA = YES;
245 | CLANG_WARN_CONSTANT_CONVERSION = YES;
246 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
247 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
248 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
249 | CLANG_WARN_EMPTY_BODY = YES;
250 | CLANG_WARN_ENUM_CONVERSION = YES;
251 | CLANG_WARN_INFINITE_RECURSION = YES;
252 | CLANG_WARN_INT_CONVERSION = YES;
253 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
254 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
255 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
256 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
257 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
258 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
259 | CLANG_WARN_STRICT_PROTOTYPES = YES;
260 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
261 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
262 | CLANG_WARN_UNREACHABLE_CODE = YES;
263 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
264 | COPY_PHASE_STRIP = NO;
265 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
266 | ENABLE_NS_ASSERTIONS = NO;
267 | ENABLE_STRICT_OBJC_MSGSEND = YES;
268 | GCC_C_LANGUAGE_STANDARD = gnu11;
269 | GCC_NO_COMMON_BLOCKS = YES;
270 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
271 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
272 | GCC_WARN_UNDECLARED_SELECTOR = YES;
273 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
274 | GCC_WARN_UNUSED_FUNCTION = YES;
275 | GCC_WARN_UNUSED_VARIABLE = YES;
276 | MACOSX_DEPLOYMENT_TARGET = 13.0;
277 | MTL_ENABLE_DEBUG_INFO = NO;
278 | MTL_FAST_MATH = YES;
279 | SDKROOT = macosx;
280 | SWIFT_COMPILATION_MODE = wholemodule;
281 | SWIFT_OPTIMIZATION_LEVEL = "-O";
282 | };
283 | name = Release;
284 | };
285 | C9D370152900C0160050984D /* Debug */ = {
286 | isa = XCBuildConfiguration;
287 | buildSettings = {
288 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
289 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
290 | CODE_SIGN_ENTITLEMENTS = WelpExample/WelpExample.entitlements;
291 | CODE_SIGN_STYLE = Automatic;
292 | COMBINE_HIDPI_IMAGES = YES;
293 | CURRENT_PROJECT_VERSION = 1;
294 | DEVELOPMENT_TEAM = 5GXRS83U4Z;
295 | GENERATE_INFOPLIST_FILE = YES;
296 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
297 | INFOPLIST_KEY_NSMainNibFile = MainMenu;
298 | INFOPLIST_KEY_NSPrincipalClass = NSApplication;
299 | LD_RUNPATH_SEARCH_PATHS = (
300 | "$(inherited)",
301 | "@executable_path/../Frameworks",
302 | );
303 | MARKETING_VERSION = 1.0;
304 | PRODUCT_BUNDLE_IDENTIFIER = com.chimehq.WelpExample;
305 | PRODUCT_NAME = "$(TARGET_NAME)";
306 | SWIFT_EMIT_LOC_STRINGS = YES;
307 | SWIFT_VERSION = 5.0;
308 | };
309 | name = Debug;
310 | };
311 | C9D370162900C0160050984D /* Release */ = {
312 | isa = XCBuildConfiguration;
313 | buildSettings = {
314 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
315 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
316 | CODE_SIGN_ENTITLEMENTS = WelpExample/WelpExample.entitlements;
317 | CODE_SIGN_STYLE = Automatic;
318 | COMBINE_HIDPI_IMAGES = YES;
319 | CURRENT_PROJECT_VERSION = 1;
320 | DEVELOPMENT_TEAM = 5GXRS83U4Z;
321 | GENERATE_INFOPLIST_FILE = YES;
322 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
323 | INFOPLIST_KEY_NSMainNibFile = MainMenu;
324 | INFOPLIST_KEY_NSPrincipalClass = NSApplication;
325 | LD_RUNPATH_SEARCH_PATHS = (
326 | "$(inherited)",
327 | "@executable_path/../Frameworks",
328 | );
329 | MARKETING_VERSION = 1.0;
330 | PRODUCT_BUNDLE_IDENTIFIER = com.chimehq.WelpExample;
331 | PRODUCT_NAME = "$(TARGET_NAME)";
332 | SWIFT_EMIT_LOC_STRINGS = YES;
333 | SWIFT_VERSION = 5.0;
334 | };
335 | name = Release;
336 | };
337 | /* End XCBuildConfiguration section */
338 |
339 | /* Begin XCConfigurationList section */
340 | C9D370022900C0150050984D /* Build configuration list for PBXProject "WelpExample" */ = {
341 | isa = XCConfigurationList;
342 | buildConfigurations = (
343 | C9D370122900C0160050984D /* Debug */,
344 | C9D370132900C0160050984D /* Release */,
345 | );
346 | defaultConfigurationIsVisible = 0;
347 | defaultConfigurationName = Release;
348 | };
349 | C9D370142900C0160050984D /* Build configuration list for PBXNativeTarget "WelpExample" */ = {
350 | isa = XCConfigurationList;
351 | buildConfigurations = (
352 | C9D370152900C0160050984D /* Debug */,
353 | C9D370162900C0160050984D /* Release */,
354 | );
355 | defaultConfigurationIsVisible = 0;
356 | defaultConfigurationName = Release;
357 | };
358 | /* End XCConfigurationList section */
359 |
360 | /* Begin XCSwiftPackageProductDependency section */
361 | C97918AD2A9D33070046EAF1 /* Welp */ = {
362 | isa = XCSwiftPackageProductDependency;
363 | productName = Welp;
364 | };
365 | C97918B02A9D330B0046EAF1 /* Welp */ = {
366 | isa = XCSwiftPackageProductDependency;
367 | productName = Welp;
368 | };
369 | /* End XCSwiftPackageProductDependency section */
370 | };
371 | rootObject = C9D36FFF2900C0150050984D /* Project object */;
372 | }
373 |
--------------------------------------------------------------------------------
/Projects/WelpExample/WelpExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Projects/WelpExample/WelpExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Projects/WelpExample/WelpExample/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 | import SwiftUI
3 |
4 | import Welp
5 |
6 | struct ContentView: View {
7 | var body: some View {
8 | Text("jello")
9 | .toolbar {
10 | ToolbarItem(placement: .navigation) {
11 | Button(action: toggleSidebar, label: {
12 | Image(systemName: "sidebar.leading")
13 | })
14 | }
15 | }
16 | .frame(maxWidth: .infinity, maxHeight: .infinity)
17 | }
18 |
19 | private func toggleSidebar() {
20 | }
21 | }
22 |
23 | @main
24 | class AppDelegate: NSObject, NSApplicationDelegate {
25 | lazy var window: NSWindow = {
26 | let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 400, height: 300),
27 | styleMask: [.titled, .closable, .unifiedTitleAndToolbar, .resizable],
28 | backing: .buffered,
29 | defer: true)
30 |
31 | return window
32 | }()
33 |
34 | func applicationDidFinishLaunching(_ aNotification: Notification) {
35 | Help.register(.init(items: self.helpSearch, allTopics: self.allTopics))
36 |
37 | window.title = "Something Help"
38 | window.contentView = NSHostingView(rootView: ContentView())
39 | window.toolbarStyle = .unifiedCompact
40 | window.setContentSize(NSSize(width: 400, height: 300))
41 | window.center()
42 |
43 | window.makeKeyAndOrderFront(self)
44 |
45 |
46 | // window.styleMask.update(with: .unifiedTitleAndToolbar)
47 | }
48 |
49 | func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
50 | return true
51 | }
52 |
53 | func helpSearch(_ query: String, limit: Int) -> [Help.Item] {
54 | return [
55 | Help.Item(localizedTitles: ["a"], action: { print("a") }),
56 | Help.Item(localizedTitles: ["a", "b"], action: { print("a") }),
57 | Help.Item(localizedTitles: ["a", "b", "c"], action: { print("c") }),
58 | ]
59 | }
60 |
61 | func allTopics(_ query: String) {
62 | print("all topics for:", query)
63 | }
64 | }
65 |
66 | //extension AppDelegate: NSUserInterfaceItemSearching {
67 | // enum HelpItem {
68 | // case value(String)
69 | // }
70 | //
71 | // func searchForItems(withSearch searchString: String, resultLimit: Int, matchedItemHandler handleMatchedItems: @escaping ([Any]) -> Void) {
72 | // let items = [
73 | // HelpItem.value("a"),
74 | // HelpItem.value("B"),
75 | // HelpItem.value("CCC"),
76 | // ]
77 | //
78 | // handleMatchedItems(items)
79 | // }
80 | //
81 | // func localizedTitles(forItem item: Any) -> [String] {
82 | // switch item as? HelpItem {
83 | // case .value(let v):
84 | // return [v, "thing", "other"]
85 | // default:
86 | // return ["nothing"]
87 | // }
88 | // }
89 | //
90 | // func performAction(forItem item: Any) {
91 | // print("perform: \(item)")
92 | // }
93 | //
94 | // func showAllHelpTopics(forSearch searchString: String) {
95 | // print("show help for ", searchString)
96 | // }
97 | //}
98 |
99 | @MainActor
100 | func doThing() {
101 | Help.register(Help.Handlers(items: searchItems, allTopics: allTopics))
102 | }
103 |
104 | func searchItems(_ query: String, limit: Int) async -> [Help.Item] {
105 | // perform your query and produce at most `limit` items
106 |
107 | return items
108 | }
109 |
110 | func allTopics(_ query: String) {
111 | // Handle the "Show All Help Topics" items for a given query string
112 | }
113 |
--------------------------------------------------------------------------------
/Projects/WelpExample/WelpExample/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Projects/WelpExample/WelpExample/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "scale" : "1x",
6 | "size" : "16x16"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "2x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "1x",
16 | "size" : "32x32"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "2x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "1x",
26 | "size" : "128x128"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "2x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "1x",
36 | "size" : "256x256"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "2x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "1x",
46 | "size" : "512x512"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "2x",
51 | "size" : "512x512"
52 | }
53 | ],
54 | "info" : {
55 | "author" : "xcode",
56 | "version" : 1
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Projects/WelpExample/WelpExample/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Projects/WelpExample/WelpExample/Base.lproj/MainMenu.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
679 |
680 |
681 |
--------------------------------------------------------------------------------
/Projects/WelpExample/WelpExample/WelpExample.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [![Build Status][build status badge]][build status]
4 | [![Matrix][matrix badge]][matrix]
5 |
6 |
7 |
8 | # Welp
9 | Tooling for macOS help books.
10 |
11 | The help book format for macOS is effectively abandoned. Yet, it still remains one of the best ways to deliver in-app help content. Nearly all of the built-in macOS apps use it. But, it can be a real struggle. Welp is all about building tooling to make the process easier.
12 |
13 | The project has five components:
14 |
15 | - `Welp` library for working with help books programmatically
16 | - `welp` cli tool for help book automation
17 | - `Help Book.xctemplate` an Xcode template that sets up a help book bundle target
18 | - `WelpBook` a (very) experimental library that replaces the help book format entirely
19 |
20 | I have some [JavaScript](Help%20Book.xctemplate/helpbook.js) here that is needed to drive the in-browser navigation system. It does not work quite yet, and I also do not know the best way to distribute it. If you have ideas here, I'd love some help!
21 |
22 | ## Installation
23 |
24 | Welp is available as both a command-line tool and a library.
25 |
26 | Tool:
27 |
28 | ```
29 | brew tap ChimeHQ/Welp https://github.com/ChimeHQ/Welp.git
30 | brew install welp
31 | ```
32 |
33 | Package:
34 |
35 | ```swift
36 | dependencies: [
37 | .package(url: "https://github.com/ChimeHQ/Welp", branch: "main")
38 | ],
39 | targets: [
40 | .target(name: "MyTarget", dependencies: ["Welp"]),
41 | .target(name: "MyOtherTarget", dependencies: ["WelpBook"]),
42 | ]
43 | ```
44 |
45 | ## Tool Usage
46 |
47 | *forthcoming - tool needs to be built first*
48 |
49 | ## Template Usage
50 |
51 | First, the template must be installed. The on-disk path determines where Xcode displays it in its UI. These instructions place it in `macOS` > `Other`, under the `Project Templates` main group.
52 |
53 | ```bash
54 | cd Welp
55 | mkdir -p ~/Library/Developer/Xcode/Templates/Project\ Templates/macOS/Other
56 | cp -r Help\ Book.xctemplate ~/Library/Developer/Xcode/Templates/Project\ Templates/macOS/Other/
57 | ```
58 |
59 | Unfortunately, this template is missing a few features. This means there are some extra steps required to make it actually work.
60 |
61 | #### Copy
62 |
63 | Add the help book bundle into your main app via the "Copy Bundle Resources" phase.
64 |
65 | I *thought* that `AssociatedTargetNeedsProductBuildPhaseInjection` would make this automatic, but I cannot figure out how to control how Xcode adds it into the associated target's build phases.
66 |
67 | #### Info.plist Addition
68 |
69 | I *think* it is necessary to add an entry into your main app's Info.plist. `CFBundleHelpBookFolder` should be set to `YourHelpBook.help`.
70 |
71 | ## WelpBook Usage
72 |
73 | Please do keep in mind this is experimental. This provides an interface to the `NSUserInterfaceItemSearching` API.
74 |
75 | ```swift
76 | Help.register(Help.Handlers(items: searchItems, allTopics: allTopics))
77 |
78 | func searchItems(_ query: String, limit: Int) async -> [Help.Item] {
79 | // perform your query and produce at most `limit` items
80 |
81 | return items
82 | }
83 |
84 | @MainActor
85 | func allTopics(_ query: String) {
86 | // Handle the "Show All Help Topics" items for a given query string
87 | }
88 | ```
89 |
90 | ## References
91 |
92 | - [Authoring Apple Help](https://developer.apple.com/library/archive/documentation/Carbon/Conceptual/ProvidingUserAssitAppleHelp/authoring_help/authoring_help_book.html)
93 | - [Authoring macOS Help Books in 2020 (and beyond)](https://marioaguzman.wordpress.com/2020/09/12/auth/)
94 | - [Why won't that help book open](https://eclecticlight.co/2021/11/16/why-wont-that-help-book-open/)
95 | - [Phel: Publish help books for your Mac apps](https://www.checksimsoftware.com/phel/)
96 |
97 | ## Contributing and Collaboration
98 |
99 | I'd love to hear from you! Get in touch via an issue or pull request.
100 |
101 | I prefer collaboration, and would love to find ways to work together if you have a similar project.
102 |
103 | I prefer indentation with tabs for improved accessibility. But, I'd rather you use the system you want and make a PR than hesitate because of whitespace.
104 |
105 | By participating in this project you agree to abide by the [Contributor Code of Conduct](CODE_OF_CONDUCT.md).
106 |
107 | [build status]: https://github.com/ChimeHQ/Welp/actions
108 | [build status badge]: https://github.com/ChimeHQ/Welp/workflows/CI/badge.svg
109 | [matrix]: https://matrix.to/#/%23chimehq%3Amatrix.org
110 | [matrix badge]: https://img.shields.io/matrix/chimehq%3Amatrix.org?label=Matrix
111 |
--------------------------------------------------------------------------------
/Sources/Welp/Hiutil.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct Hiutil {
4 |
5 | }
6 |
--------------------------------------------------------------------------------
/Sources/WelpBook/Documentation.docc/WelpBook.md:
--------------------------------------------------------------------------------
1 | # ``WelpBook``
2 |
3 | A macOS help book alternative that shouldn't need to exist.
4 |
5 | ## Overview
6 |
7 | The help book format for macOS is effectively abandoned.
8 |
--------------------------------------------------------------------------------
/Sources/WelpBook/Welp.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 |
3 | public struct Help {
4 | /// An item returned to the help system.
5 | ///
6 | /// This value is unchecked because even though it passes through concurrency domains, we know that `action` will only ever actually be accessed from the MainActor.
7 | public struct Item: Sendable {
8 | public typealias Action = @MainActor () -> Void
9 |
10 | public let localizedTitles: [String]
11 | public let action: Action
12 |
13 | public init(localizedTitles: [String], action: @escaping Action) {
14 | self.localizedTitles = localizedTitles
15 | self.action = action
16 | }
17 |
18 | public init(localizedTitle: String, action: @escaping Action) {
19 | self.init(localizedTitles: [localizedTitle], action: action)
20 | }
21 | }
22 |
23 | public typealias ItemHandler = (String, Int) async -> [Item]
24 | public typealias AllTopicsHandler = @MainActor (String) -> Void
25 |
26 | public struct Handlers {
27 | public let items: ItemHandler
28 | public let allTopics: AllTopicsHandler
29 |
30 | public init(items: @escaping ItemHandler, allTopics: @escaping AllTopicsHandler) {
31 | self.items = items
32 | self.allTopics = allTopics
33 | }
34 | }
35 |
36 | /// Register with the help search system.
37 | @MainActor
38 | public static func register(_ handlers: Handlers) {
39 | let searcher = InterfaceSearcher.shared
40 |
41 | searcher.deregister()
42 |
43 | searcher.handlers = handlers
44 |
45 | searcher.register()
46 | }
47 |
48 | /// Deregister from the help search system.
49 | ///
50 | /// This is safe to call more than once.
51 | @MainActor
52 | public static func deregister() {
53 | InterfaceSearcher.shared.deregister()
54 | }
55 | }
56 |
57 | @MainActor
58 | final class InterfaceSearcher: NSObject {
59 | static var shared = InterfaceSearcher()
60 |
61 | var handlers: Help.Handlers
62 | private var currentSearch: Task?
63 |
64 | override init() {
65 | self.handlers = .init(items: { _, _ in [] }, allTopics: { _ in })
66 |
67 | super.init()
68 | }
69 |
70 | func register() {
71 | NSApp.registerUserInterfaceItemSearchHandler(self)
72 | }
73 |
74 | func deregister() {
75 | // oh you want to put this into deinit right? You cannot, because NSApp is MainActor...
76 | NSApp.unregisterUserInterfaceItemSearchHandler(self)
77 | }
78 | }
79 |
80 | #if compiler(>=6.0)
81 | extension InterfaceSearcher: @preconcurrency NSUserInterfaceItemSearching {
82 | }
83 | #else
84 | extension InterfaceSearcher: NSUserInterfaceItemSearching {
85 | }
86 | #endif
87 |
88 | extension InterfaceSearcher {
89 | nonisolated func searchForItems(withSearch searchString: String, resultLimit: Int, matchedItemHandler handleMatchedItems: @escaping ([Any]) -> Void) {
90 | // by my reading of the documentation, `matchedItemHandler` is actually sendable but is not correctly annotated
91 | let sendableMatchHandler = unsafeBitCast(handleMatchedItems, to: (@Sendable ([Any]) -> Void).self)
92 |
93 | let task = Task { @MainActor in
94 | self.currentSearch?.cancel() // cancel any active search
95 |
96 | let handler = self.handlers.items
97 |
98 | let items = await handler(searchString, resultLimit)
99 |
100 | sendableMatchHandler(items)
101 | }
102 |
103 | // this is very awkward
104 | Task { @MainActor in
105 | self.currentSearch = task
106 | }
107 | }
108 |
109 | nonisolated func localizedTitles(forItem item: Any) -> [String] {
110 | guard let item = item as? Help.Item else {
111 | print("unexpected value returned in NSUserInterfaceItemSearching \(item)")
112 | return []
113 | }
114 |
115 | return item.localizedTitles
116 | }
117 |
118 | func performAction(forItem item: Any) {
119 | guard let item = item as? Help.Item else {
120 | print("unexpected value returned in NSUserInterfaceItemSearching \(item)")
121 | return
122 | }
123 |
124 | item.action()
125 | }
126 |
127 | func showAllHelpTopics(forSearch searchString: String) {
128 | self.handlers.allTopics(searchString)
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/Sources/clitool/main.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import ArgumentParser
3 | import Welp
4 |
5 | struct WelpCommand: ParsableCommand {
6 | static let configuration = CommandConfiguration(commandName: "welp")
7 |
8 | @Flag(
9 | name: .shortAndLong,
10 | help: "Print the version and exit."
11 | )
12 | var version: Bool = false
13 |
14 | func run() throws {
15 | if version {
16 | throw CleanExit.message("0.0.1")
17 | }
18 | }
19 | }
20 |
21 | WelpCommand.main()
22 |
--------------------------------------------------------------------------------
/Tests/WelpTests/WelpTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Welp
3 |
4 | final class WelpTests: XCTestCase {
5 | func testExample() throws {
6 | }
7 | }
8 |
--------------------------------------------------------------------------------