chore(typing): snapshot local pinyin WIP before syncing cloud branch
Preserve the in-progress local typing/pinyin implementation so feat/pinyin can safely reset to origin/feat/pinyin (cloud English + Chinese typing).
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
// KeyboardChromeLayout.swift
|
||||
// OSGKeyboard · Shared
|
||||
//
|
||||
// Cross-surface dimensions that must stay identical in voice and typing modes.
|
||||
|
||||
import CoreGraphics
|
||||
|
||||
public enum KeyboardChromeLayout {
|
||||
public static let totalHeight: CGFloat = 281
|
||||
public static let actionKeyHeight: CGFloat = 50
|
||||
public static let actionKeyCornerRadius: CGFloat = 10
|
||||
public static let horizontalInset: CGFloat = 8
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// TypingInputConfiguration.swift
|
||||
// OSGKeyboard · Shared
|
||||
//
|
||||
// App Group-backed Chinese input settings shared by the host app and
|
||||
// keyboard extension. Fuzzy pairs are opt-in to avoid noisy candidates.
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
public enum TypingInputSchema: String, CaseIterable, Identifiable, Codable, Sendable {
|
||||
case fullPinyin = "osg_pinyin"
|
||||
case microsoftDoublePinyin = "osg_double_pinyin_mspy"
|
||||
case sogouDoublePinyin = "osg_double_pinyin_sogou"
|
||||
|
||||
public var id: String { rawValue }
|
||||
|
||||
public var shortLabel: String {
|
||||
switch self {
|
||||
case .fullPinyin: return "全"
|
||||
case .microsoftDoublePinyin: return "微"
|
||||
case .sogouDoublePinyin: return "搜"
|
||||
}
|
||||
}
|
||||
|
||||
public var displayName: String {
|
||||
switch self {
|
||||
case .fullPinyin: return "全拼"
|
||||
case .microsoftDoublePinyin: return "微软双拼"
|
||||
case .sogouDoublePinyin: return "搜狗双拼"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum PinyinFuzzyPair: String, CaseIterable, Identifiable, Codable, Sendable {
|
||||
case zhZ
|
||||
case chC
|
||||
case shS
|
||||
case nL
|
||||
case fH
|
||||
case anAng
|
||||
case enEng
|
||||
case inIng
|
||||
|
||||
public var id: String { rawValue }
|
||||
|
||||
public var displayName: String {
|
||||
switch self {
|
||||
case .zhZ: return "zh ↔ z"
|
||||
case .chC: return "ch ↔ c"
|
||||
case .shS: return "sh ↔ s"
|
||||
case .nL: return "n ↔ l"
|
||||
case .fH: return "f ↔ h"
|
||||
case .anAng: return "an ↔ ang"
|
||||
case .enEng: return "en ↔ eng"
|
||||
case .inIng: return "in ↔ ing"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct TypingInputConfigurationSnapshot: Equatable, Sendable {
|
||||
public let schema: TypingInputSchema
|
||||
public let fuzzyPairs: Set<PinyinFuzzyPair>
|
||||
|
||||
public init(schema: TypingInputSchema, fuzzyPairs: Set<PinyinFuzzyPair>) {
|
||||
self.schema = schema
|
||||
self.fuzzyPairs = fuzzyPairs
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public final class TypingInputConfiguration: ObservableObject {
|
||||
public static let shared = TypingInputConfiguration()
|
||||
|
||||
private enum Key {
|
||||
static let schema = "typing.input.schema"
|
||||
static let fuzzyPairs = "typing.input.fuzzyPairs"
|
||||
static let defaultToTyping = "typing.input.defaultToTyping"
|
||||
static let resourceVersion = "typing.rime.resourceVersion"
|
||||
}
|
||||
|
||||
private let defaults: UserDefaults
|
||||
private var isHydrating = true
|
||||
|
||||
@Published public var schema: TypingInputSchema {
|
||||
didSet { persistIfReady() }
|
||||
}
|
||||
|
||||
@Published public var fuzzyPairs: Set<PinyinFuzzyPair> {
|
||||
didSet { persistIfReady() }
|
||||
}
|
||||
|
||||
/// Selects the text keyboard whenever the extension becomes visible.
|
||||
@Published public var defaultToTyping: Bool {
|
||||
didSet { persistIfReady() }
|
||||
}
|
||||
|
||||
public init(defaults: UserDefaults? = nil) {
|
||||
self.defaults = defaults ?? AppGroup.defaults
|
||||
let schemaId = self.defaults.string(forKey: Key.schema) ?? ""
|
||||
schema = TypingInputSchema(rawValue: schemaId) ?? .fullPinyin
|
||||
let fuzzyIds = self.defaults.stringArray(forKey: Key.fuzzyPairs) ?? []
|
||||
fuzzyPairs = Set(fuzzyIds.compactMap(PinyinFuzzyPair.init(rawValue:)))
|
||||
defaultToTyping = self.defaults.bool(forKey: Key.defaultToTyping)
|
||||
isHydrating = false
|
||||
}
|
||||
|
||||
public var snapshot: TypingInputConfigurationSnapshot {
|
||||
TypingInputConfigurationSnapshot(schema: schema, fuzzyPairs: fuzzyPairs)
|
||||
}
|
||||
|
||||
public func setFuzzyPair(_ pair: PinyinFuzzyPair, enabled: Bool) {
|
||||
if enabled {
|
||||
fuzzyPairs.insert(pair)
|
||||
} else {
|
||||
fuzzyPairs.remove(pair)
|
||||
}
|
||||
}
|
||||
|
||||
public func reload() {
|
||||
isHydrating = true
|
||||
schema = TypingInputSchema(rawValue: defaults.string(forKey: Key.schema) ?? "")
|
||||
?? .fullPinyin
|
||||
let fuzzyIds = defaults.stringArray(forKey: Key.fuzzyPairs) ?? []
|
||||
fuzzyPairs = Set(fuzzyIds.compactMap(PinyinFuzzyPair.init(rawValue:)))
|
||||
defaultToTyping = defaults.bool(forKey: Key.defaultToTyping)
|
||||
isHydrating = false
|
||||
}
|
||||
|
||||
nonisolated public static func prefersTypingOnOpen(
|
||||
defaults: UserDefaults? = nil
|
||||
) -> Bool {
|
||||
(defaults ?? AppGroup.defaultsIfAvailable)?.bool(forKey: Key.defaultToTyping) ?? false
|
||||
}
|
||||
|
||||
nonisolated public static func installedResourceVersion(
|
||||
defaults: UserDefaults? = nil
|
||||
) -> String? {
|
||||
(defaults ?? AppGroup.defaultsIfAvailable)?.string(forKey: Key.resourceVersion)
|
||||
}
|
||||
|
||||
nonisolated public static func setInstalledResourceVersion(
|
||||
_ value: String,
|
||||
defaults: UserDefaults? = nil
|
||||
) {
|
||||
(defaults ?? AppGroup.defaultsIfAvailable)?.set(value, forKey: Key.resourceVersion)
|
||||
}
|
||||
|
||||
private func persistIfReady() {
|
||||
guard !isHydrating else { return }
|
||||
defaults.set(schema.rawValue, forKey: Key.schema)
|
||||
defaults.set(fuzzyPairs.map(\.rawValue).sorted(), forKey: Key.fuzzyPairs)
|
||||
defaults.set(defaultToTyping, forKey: Key.defaultToTyping)
|
||||
AppGroupConfigDarwin.postConfigChanged()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Sun Junyi
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 mozillazg
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 mozillazg
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,32 @@
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2026, librime-xcframework contributors
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,29 @@
|
||||
Typing keyboard third-party notices
|
||||
===================================
|
||||
|
||||
Runtime
|
||||
-------
|
||||
librime-xcframework 1.17.0-pack.1 and librime 1.17.0 (BSD-3-Clause).
|
||||
See LICENSE.txt, THIRD_PARTY_NOTICES.md and third-party-notices.zip bundled
|
||||
with this file for full binary dependency license texts.
|
||||
|
||||
Dictionary data
|
||||
---------------
|
||||
- rime-pinyin-simp @ 0c6861ef7420ee780270ca6d993d18d4101049d0
|
||||
Apache License 2.0
|
||||
- fxsjy/jieba @ 67fa2e36e72f69d9134b8a1037b83fbb070b9775
|
||||
MIT License
|
||||
- mozillazg/phrase-pinyin-data @ cee0ed6e6e4898580cafd2bd5e3723e20b214aa0
|
||||
MIT License
|
||||
- mozillazg/pinyin-data @ 923b108dc5d45dee061324c011b478fb649f8b73
|
||||
MIT License
|
||||
|
||||
The generated manifest.json records exact source URLs and SHA-256 values.
|
||||
OSGKeyboard's schema and transformation code is project-owned.
|
||||
|
||||
Explicitly not distributed
|
||||
--------------------------
|
||||
rime-ice, rime-double-pinyin, rime-luna-pinyin, rime-essay and KeyboardKit Pro.
|
||||
|
||||
The Apache-2.0 and MIT license texts are available from the source links above;
|
||||
all required copyright and permission notices must remain with distributions.
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"sources": {
|
||||
"pinyin_simp": {
|
||||
"license": "Apache-2.0",
|
||||
"commit": "0c6861ef7420ee780270ca6d993d18d4101049d0",
|
||||
"url": "https://raw.githubusercontent.com/rime/rime-pinyin-simp/0c6861ef7420ee780270ca6d993d18d4101049d0/pinyin_simp.dict.yaml",
|
||||
"sha256": "e341598343a0f0f2035bb1aafc34a7f3bb7887deeecb3f60796262aaa2983e6b"
|
||||
},
|
||||
"jieba": {
|
||||
"license": "MIT",
|
||||
"commit": "67fa2e36e72f69d9134b8a1037b83fbb070b9775",
|
||||
"url": "https://raw.githubusercontent.com/fxsjy/jieba/67fa2e36e72f69d9134b8a1037b83fbb070b9775/jieba/dict.txt",
|
||||
"sha256": "7197c3211ddd98962b036cdf40324d1ea2bfaa12bd028e68faa70111a88e12a8"
|
||||
},
|
||||
"phrase_pinyin": {
|
||||
"license": "MIT",
|
||||
"commit": "cee0ed6e6e4898580cafd2bd5e3723e20b214aa0",
|
||||
"url": "https://raw.githubusercontent.com/mozillazg/phrase-pinyin-data/cee0ed6e6e4898580cafd2bd5e3723e20b214aa0/pinyin.txt",
|
||||
"sha256": "dcc769607c220b312fea3e71cb63421298b4b891b1f7356a95ab58f2c96fff81"
|
||||
},
|
||||
"character_pinyin": {
|
||||
"license": "MIT",
|
||||
"commit": "923b108dc5d45dee061324c011b478fb649f8b73",
|
||||
"url": "https://raw.githubusercontent.com/mozillazg/pinyin-data/923b108dc5d45dee061324c011b478fb649f8b73/pinyin.txt",
|
||||
"sha256": "621f8ca9eff8519f47e2b17b564fd318161e13bca07eea8c8e04993cd5d3b52e"
|
||||
}
|
||||
},
|
||||
"statistics": {
|
||||
"baselineEntries": 65125,
|
||||
"jiebaWordsAccepted": 337338,
|
||||
"jiebaWordsUsingCharacterFallback": 293794,
|
||||
"outputEntries": 364926
|
||||
},
|
||||
"output": {
|
||||
"file": "osg_pinyin.dict.yaml",
|
||||
"sha256": "35b0664df8906712e4051392d83be35bf76587e4c9a7b068a71e0d6f7b989425"
|
||||
},
|
||||
"excluded": [
|
||||
"rime-ice (GPL-3.0)",
|
||||
"rime-double-pinyin (GPL-3.0)",
|
||||
"rime-essay (LGPL-3.0)",
|
||||
"rime-luna-pinyin (LGPL-3.0)"
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
||||
# Third-Party Notices
|
||||
|
||||
This repository packages upstream `librime` and its build dependencies into
|
||||
macOS and iOS XCFramework release artifacts.
|
||||
|
||||
The wrapper scripts, manifests, and documentation in this repository are
|
||||
licensed under the BSD 3-Clause License. See `LICENSE`.
|
||||
|
||||
Binary release artifacts include upstream `librime` and may include statically
|
||||
linked third-party dependency code resolved by vcpkg. Keep these notices with
|
||||
any redistributed binary artifacts.
|
||||
|
||||
Release assets include:
|
||||
|
||||
- `LICENSE.txt`: the license for this packaging wrapper.
|
||||
- `THIRD_PARTY_NOTICES.md`: this overview and the upstream `librime` notice.
|
||||
- `third-party-notices.zip`: vcpkg-provided license texts for bundled
|
||||
third-party dependencies.
|
||||
|
||||
## Upstream librime
|
||||
|
||||
Upstream project: <https://github.com/rime/librime>
|
||||
|
||||
License: BSD 3-Clause License
|
||||
|
||||
```text
|
||||
Copyright (c) 2014, RIME Developers
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
```
|
||||
Binary file not shown.
@@ -68,7 +68,18 @@ public final class KeyboardState: ObservableObject {
|
||||
public var labelKey: String { "mode.polish" }
|
||||
}
|
||||
|
||||
/// Keyboard chrome surface. Voice is the default product mode; typing is
|
||||
/// a secondary QWERTY / pinyin surface for quick corrections.
|
||||
public enum Surface: String, CaseIterable, Identifiable, Sendable {
|
||||
case voice
|
||||
case typing
|
||||
|
||||
public var id: String { rawValue }
|
||||
}
|
||||
|
||||
@Published public var phase: Phase = .idle
|
||||
/// Active chrome. Forced to `.voice` while recording / processing.
|
||||
@Published public var surface: Surface = .voice
|
||||
@Published public var level: Double = 0
|
||||
@Published public var mode: InputMode = .polish
|
||||
@Published public var localeId: String = "auto"
|
||||
@@ -232,6 +243,20 @@ public final class KeyboardState: ObservableObject {
|
||||
/// Cursor-drag pad press lifecycle — updates `cursorDragActive` and
|
||||
/// lets the view controller reset vertical-navigation stickiness.
|
||||
public var setCursorDragActive: (Bool) -> Void = { _ in }
|
||||
/// Switch voice ↔ typing. No-ops when voice pipeline is active.
|
||||
public var setSurface: (Surface) -> Void = { _ in }
|
||||
|
||||
/// Recording / processing must stay on the voice surface.
|
||||
public var locksTypingSurface: Bool {
|
||||
switch phase {
|
||||
case .requestingPermissions, .recording, .processing:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public var canEnterTypingSurface: Bool { !locksTypingSurface }
|
||||
|
||||
// MARK: - Preview helpers (DEBUG only)
|
||||
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
// LibrimeEngine.swift
|
||||
// OSGKeyboard · Shared
|
||||
//
|
||||
// Production Chinese IME backed by librime. Runtime access stays on the
|
||||
// main actor because UIInputViewController and its text proxy are main-only;
|
||||
// expensive schema deployment is performed by the host app beforehand.
|
||||
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
public final class LibrimeEngine: RimeEngineBridging {
|
||||
public private(set) var composition: TypingComposition = .empty
|
||||
public private(set) var isReady = false
|
||||
public private(set) var schema: TypingInputSchema
|
||||
|
||||
private var language: TypingInputLanguage = .chinese
|
||||
private var bridge: OSGRimeBridge?
|
||||
private let configurationProvider: () -> TypingInputConfigurationSnapshot
|
||||
private let candidateLimit: Int
|
||||
|
||||
public init(
|
||||
schema: TypingInputSchema = .fullPinyin,
|
||||
candidateLimit: Int = 50,
|
||||
configurationProvider: @escaping () -> TypingInputConfigurationSnapshot = {
|
||||
TypingInputConfiguration.shared.snapshot
|
||||
}
|
||||
) {
|
||||
self.schema = schema
|
||||
self.candidateLimit = candidateLimit
|
||||
self.configurationProvider = configurationProvider
|
||||
}
|
||||
|
||||
public func prepare() async throws {
|
||||
if isReady { return }
|
||||
guard RimeResourceInstaller.isReady else {
|
||||
throw RimeResourceError.resourcesNotInstalled
|
||||
}
|
||||
|
||||
let paths = try RimeResourcePaths.resolve()
|
||||
let runtime = OSGRimeBridge(
|
||||
sharedDataDirectory: paths.sharedData.path,
|
||||
userDataDirectory: paths.userData.path,
|
||||
distributionVersion: RimeResourceInstaller.resourceVersion
|
||||
)
|
||||
try runtime.start()
|
||||
|
||||
let configured = configurationProvider()
|
||||
schema = configured.schema
|
||||
guard runtime.selectSchema(schema.rawValue) else {
|
||||
runtime.stopSession()
|
||||
throw LibrimeEngineError.schemaUnavailable(schema.rawValue)
|
||||
}
|
||||
_ = runtime.setASCIIMode(language == .english)
|
||||
bridge = runtime
|
||||
isReady = true
|
||||
_ = refresh()
|
||||
}
|
||||
|
||||
public func teardown() {
|
||||
bridge?.clearComposition()
|
||||
bridge?.stopSession()
|
||||
bridge = nil
|
||||
composition = .empty
|
||||
isReady = false
|
||||
}
|
||||
|
||||
public func setLanguage(_ language: TypingInputLanguage) {
|
||||
self.language = language
|
||||
_ = bridge?.setASCIIMode(language == .english)
|
||||
if language == .english {
|
||||
bridge?.clearComposition()
|
||||
composition = .empty
|
||||
} else {
|
||||
_ = refresh()
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func setSchema(_ schema: TypingInputSchema) -> Bool {
|
||||
guard let bridge else {
|
||||
self.schema = schema
|
||||
return false
|
||||
}
|
||||
bridge.clearComposition()
|
||||
guard bridge.selectSchema(schema.rawValue) else { return false }
|
||||
self.schema = schema
|
||||
composition = .empty
|
||||
return true
|
||||
}
|
||||
|
||||
public func processCharacter(_ character: Character) -> String? {
|
||||
guard language == .chinese,
|
||||
let scalar = character.asciiValue,
|
||||
bridge?.processKeyCode(Int32(scalar), modifiers: 0) == true else {
|
||||
return nil
|
||||
}
|
||||
return refresh()
|
||||
}
|
||||
|
||||
public func processBackspace() -> String? {
|
||||
guard bridge?.processKeyCode(OSGRimeKeyBackSpace, modifiers: 0) == true else {
|
||||
return nil
|
||||
}
|
||||
return refresh()
|
||||
}
|
||||
|
||||
public func processSpace() -> String? {
|
||||
guard bridge?.processKeyCode(32, modifiers: 0) == true else {
|
||||
return " "
|
||||
}
|
||||
return refresh()
|
||||
}
|
||||
|
||||
public func processReturn() -> String? {
|
||||
guard bridge?.processKeyCode(OSGRimeKeyReturn, modifiers: 0) == true else {
|
||||
return "\n"
|
||||
}
|
||||
return refresh()
|
||||
}
|
||||
|
||||
public func selectCandidate(at index: Int) -> String {
|
||||
guard bridge?.selectCandidate(at: index) == true else { return "" }
|
||||
return refresh() ?? ""
|
||||
}
|
||||
|
||||
public func flushPreedit() -> String {
|
||||
let raw = bridge?.rawInput() ?? ""
|
||||
bridge?.clearComposition()
|
||||
composition = .empty
|
||||
return raw
|
||||
}
|
||||
|
||||
public func clearComposition() {
|
||||
bridge?.clearComposition()
|
||||
composition = .empty
|
||||
}
|
||||
|
||||
/// Copies librime-owned memory into Sendable Swift value types and returns
|
||||
/// any commit emitted by the preceding key operation.
|
||||
@discardableResult
|
||||
private func refresh() -> String? {
|
||||
guard let snapshot = bridge?.snapshot(withCandidateLimit: candidateLimit) else {
|
||||
composition = .empty
|
||||
return nil
|
||||
}
|
||||
let preedit = snapshot.preedit
|
||||
composition = TypingComposition(
|
||||
preedit: preedit,
|
||||
candidates: snapshot.candidates.enumerated().map { index, candidate in
|
||||
TypingCandidate(
|
||||
id: "\(preedit)|\(index)|\(candidate.text)",
|
||||
text: candidate.text,
|
||||
annotation: candidate.comment.isEmpty ? nil : candidate.comment
|
||||
)
|
||||
}
|
||||
)
|
||||
return snapshot.commitText.isEmpty ? nil : snapshot.commitText
|
||||
}
|
||||
}
|
||||
|
||||
public enum LibrimeEngineError: LocalizedError {
|
||||
case schemaUnavailable(String)
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .schemaUnavailable(let id):
|
||||
return "输入方案不可用:\(id)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension Character {
|
||||
var asciiValue: UInt8? {
|
||||
guard let scalar = unicodeScalars.first,
|
||||
unicodeScalars.count == 1,
|
||||
scalar.value <= UInt8.max else {
|
||||
return nil
|
||||
}
|
||||
return UInt8(scalar.value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// RimeEngineBridging.swift
|
||||
// OSGKeyboard · Shared
|
||||
//
|
||||
// Engine façade for the typing surface. `LibrimeEngine` is the production
|
||||
// implementation; the protocol keeps SwiftUI independent of the C runtime.
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Input language for the typing surface (not ASR locale).
|
||||
public enum TypingInputLanguage: String, CaseIterable, Identifiable, Sendable {
|
||||
case chinese
|
||||
case english
|
||||
|
||||
public var id: String { rawValue }
|
||||
|
||||
public var shortLabel: String {
|
||||
switch self {
|
||||
case .chinese: return "中"
|
||||
case .english: return "英"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One candidate row item after composing.
|
||||
public struct TypingCandidate: Identifiable, Equatable, Sendable {
|
||||
public let id: String
|
||||
public let text: String
|
||||
public let annotation: String?
|
||||
|
||||
public init(id: String = UUID().uuidString, text: String, annotation: String? = nil) {
|
||||
self.id = id
|
||||
self.text = text
|
||||
self.annotation = annotation
|
||||
}
|
||||
}
|
||||
|
||||
/// Snapshot the UI observes while composing.
|
||||
public struct TypingComposition: Equatable, Sendable {
|
||||
public var preedit: String
|
||||
public var candidates: [TypingCandidate]
|
||||
|
||||
public init(preedit: String = "", candidates: [TypingCandidate] = []) {
|
||||
self.preedit = preedit
|
||||
self.candidates = candidates
|
||||
}
|
||||
|
||||
public static let empty = TypingComposition()
|
||||
}
|
||||
|
||||
/// Bridge between key events and IME state. Keep this small so Phase 2
|
||||
/// can swap KeyboardKit-style shells or librime without UI rewrites.
|
||||
@MainActor
|
||||
public protocol RimeEngineBridging: AnyObject {
|
||||
var composition: TypingComposition { get }
|
||||
var isReady: Bool { get }
|
||||
var schema: TypingInputSchema { get }
|
||||
|
||||
/// Load dictionaries / open session. Safe to call repeatedly.
|
||||
func prepare() async throws
|
||||
/// Drop heavy caches (leave typing mode / memory warning).
|
||||
func teardown()
|
||||
|
||||
func setLanguage(_ language: TypingInputLanguage)
|
||||
@discardableResult
|
||||
func setSchema(_ schema: TypingInputSchema) -> Bool
|
||||
/// Process one key and return newly committed text, if any.
|
||||
func processCharacter(_ character: Character) -> String?
|
||||
func processBackspace() -> String?
|
||||
func processSpace() -> String?
|
||||
func processReturn() -> String?
|
||||
/// Commit candidate at index; returns text to insert (empty if invalid).
|
||||
func selectCandidate(at index: Int) -> String
|
||||
/// Force-commit current preedit as raw latin (or empty).
|
||||
func flushPreedit() -> String
|
||||
func clearComposition()
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// RimeResourceInstaller.swift
|
||||
// OSGKeyboard · Shared
|
||||
//
|
||||
// The host app owns Rime deployment. The keyboard extension only opens
|
||||
// an already-built session, keeping expensive maintenance work out of
|
||||
// the extension's constrained lifecycle.
|
||||
|
||||
import Foundation
|
||||
import Darwin
|
||||
|
||||
public enum RimeResourceError: LocalizedError {
|
||||
case appGroupUnavailable
|
||||
case bundledResourceMissing(String)
|
||||
case lockUnavailable
|
||||
case deploymentFailed
|
||||
case resourcesNotInstalled
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .appGroupUnavailable:
|
||||
return "App Group 不可用"
|
||||
case .bundledResourceMissing(let name):
|
||||
return "缺少输入法资源:\(name)"
|
||||
case .lockUnavailable:
|
||||
return "输入法资源正在被其他进程更新"
|
||||
case .deploymentFailed:
|
||||
return "输入法资源部署失败"
|
||||
case .resourcesNotInstalled:
|
||||
return "请先打开 OSGKeyboard 完成输入法初始化"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct RimeResourcePaths: Sendable {
|
||||
public let root: URL
|
||||
public let sharedData: URL
|
||||
public let userData: URL
|
||||
public let lockFile: URL
|
||||
|
||||
public static func resolve() throws -> RimeResourcePaths {
|
||||
guard let container = FileManager.default.containerURL(
|
||||
forSecurityApplicationGroupIdentifier: AppGroup.identifier
|
||||
) else {
|
||||
throw RimeResourceError.appGroupUnavailable
|
||||
}
|
||||
let root = container.appendingPathComponent("Rime", isDirectory: true)
|
||||
return RimeResourcePaths(
|
||||
root: root,
|
||||
sharedData: root.appendingPathComponent("SharedSupport", isDirectory: true),
|
||||
userData: root.appendingPathComponent("UserData", isDirectory: true),
|
||||
lockFile: root.appendingPathComponent(".deployment.lock")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public actor RimeResourceInstaller {
|
||||
public static let shared = RimeResourceInstaller()
|
||||
public static let resourceVersion = "2.0.0"
|
||||
|
||||
public init() {}
|
||||
|
||||
public static var isReady: Bool {
|
||||
guard TypingInputConfiguration.installedResourceVersion() == resourceVersion,
|
||||
let paths = try? RimeResourcePaths.resolve() else {
|
||||
return false
|
||||
}
|
||||
return FileManager.default.fileExists(
|
||||
atPath: paths.userData.appendingPathComponent("build").path
|
||||
)
|
||||
}
|
||||
|
||||
/// Installs source data and asks librime to prebuild schemas. Call only
|
||||
/// from the host app, never from the keyboard extension.
|
||||
public func installIfNeeded(
|
||||
configuration: TypingInputConfigurationSnapshot,
|
||||
force: Bool = false
|
||||
) throws {
|
||||
if !force, Self.isReady { return }
|
||||
|
||||
let paths = try RimeResourcePaths.resolve()
|
||||
let fileManager = FileManager.default
|
||||
try fileManager.createDirectory(at: paths.root, withIntermediateDirectories: true)
|
||||
try fileManager.createDirectory(at: paths.userData, withIntermediateDirectories: true)
|
||||
|
||||
let descriptor = open(paths.lockFile.path, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR)
|
||||
guard descriptor >= 0 else { throw RimeResourceError.lockUnavailable }
|
||||
defer {
|
||||
flock(descriptor, LOCK_UN)
|
||||
close(descriptor)
|
||||
}
|
||||
guard flock(descriptor, LOCK_EX | LOCK_NB) == 0 else {
|
||||
throw RimeResourceError.lockUnavailable
|
||||
}
|
||||
|
||||
let staging = paths.root.appendingPathComponent(
|
||||
"SharedSupport.staging-\(UUID().uuidString)",
|
||||
isDirectory: true
|
||||
)
|
||||
defer { try? fileManager.removeItem(at: staging) }
|
||||
try fileManager.createDirectory(at: staging, withIntermediateDirectories: true)
|
||||
|
||||
for resource in ["osg_pinyin.dict", "manifest"] {
|
||||
let ext = resource == "manifest" ? "json" : "yaml"
|
||||
guard let source = Bundle(for: RimeResourceBundleToken.self).url(
|
||||
forResource: resource,
|
||||
withExtension: ext
|
||||
) else {
|
||||
throw RimeResourceError.bundledResourceMissing("\(resource).\(ext)")
|
||||
}
|
||||
try fileManager.copyItem(
|
||||
at: source,
|
||||
to: staging.appendingPathComponent("\(resource).\(ext)")
|
||||
)
|
||||
}
|
||||
|
||||
try RimeSchemaGenerator.defaultConfiguration().write(
|
||||
to: staging.appendingPathComponent("default.yaml"),
|
||||
atomically: true,
|
||||
encoding: .utf8
|
||||
)
|
||||
for schema in TypingInputSchema.allCases {
|
||||
try RimeSchemaGenerator.schema(
|
||||
for: schema,
|
||||
fuzzyPairs: configuration.fuzzyPairs
|
||||
).write(
|
||||
to: staging.appendingPathComponent("\(schema.rawValue).schema.yaml"),
|
||||
atomically: true,
|
||||
encoding: .utf8
|
||||
)
|
||||
}
|
||||
|
||||
if fileManager.fileExists(atPath: paths.sharedData.path) {
|
||||
try fileManager.removeItem(at: paths.sharedData)
|
||||
}
|
||||
try fileManager.moveItem(at: staging, to: paths.sharedData)
|
||||
|
||||
let bridge = OSGRimeBridge(
|
||||
sharedDataDirectory: paths.sharedData.path,
|
||||
userDataDirectory: paths.userData.path,
|
||||
distributionVersion: Self.resourceVersion
|
||||
)
|
||||
do {
|
||||
try bridge.deploy(withFullCheck: true)
|
||||
} catch {
|
||||
bridge.finalizeRuntime()
|
||||
throw error
|
||||
}
|
||||
bridge.finalizeRuntime()
|
||||
|
||||
TypingInputConfiguration.setInstalledResourceVersion(Self.resourceVersion)
|
||||
AppGroupConfigDarwin.postConfigChanged()
|
||||
}
|
||||
|
||||
public func syncUserData() throws {
|
||||
let paths = try RimeResourcePaths.resolve()
|
||||
let bridge = OSGRimeBridge(
|
||||
sharedDataDirectory: paths.sharedData.path,
|
||||
userDataDirectory: paths.userData.path,
|
||||
distributionVersion: Self.resourceVersion
|
||||
)
|
||||
try bridge.start()
|
||||
// Destroying the session and finalizing librime flushes LevelDB
|
||||
// user dictionaries. `sync_user_data` is for external Rime sync
|
||||
// deployments and is intentionally not needed here.
|
||||
bridge.finalizeRuntime()
|
||||
}
|
||||
}
|
||||
|
||||
private final class RimeResourceBundleToken: NSObject {}
|
||||
@@ -0,0 +1,187 @@
|
||||
// RimeSchemaGenerator.swift
|
||||
// OSGKeyboard · Shared
|
||||
//
|
||||
// Generates OSG-owned schemas from public Microsoft/Sogou key maps.
|
||||
// No GPL Rime schema files are copied or distributed.
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum RimeSchemaGenerator {
|
||||
public static func defaultConfiguration() -> String {
|
||||
"""
|
||||
# Generated by OSGKeyboard.
|
||||
config_version: "1.0"
|
||||
schema_list:
|
||||
- schema: \(TypingInputSchema.fullPinyin.rawValue)
|
||||
- schema: \(TypingInputSchema.microsoftDoublePinyin.rawValue)
|
||||
- schema: \(TypingInputSchema.sogouDoublePinyin.rawValue)
|
||||
switcher:
|
||||
caption: 输入方案
|
||||
hotkeys: []
|
||||
menu:
|
||||
page_size: 9
|
||||
ascii_composer:
|
||||
good_old_caps_lock: true
|
||||
switch_key:
|
||||
Shift_L: noop
|
||||
Shift_R: noop
|
||||
Control_L: noop
|
||||
Control_R: noop
|
||||
key_binder:
|
||||
bindings: []
|
||||
recognizer:
|
||||
patterns: {}
|
||||
"""
|
||||
}
|
||||
|
||||
public static func schema(
|
||||
for inputSchema: TypingInputSchema,
|
||||
fuzzyPairs: Set<PinyinFuzzyPair>
|
||||
) -> String {
|
||||
let alphabet = inputSchema == .fullPinyin
|
||||
? "zyxwvutsrqponmlkjihgfedcba"
|
||||
: "zyxwvutsrqponmlkjihgfedcba;"
|
||||
let algebra = fuzzyRules(fuzzyPairs) + algebraRules(for: inputSchema)
|
||||
let algebraYAML = algebra.map { " - '\($0)'" }.joined(separator: "\n")
|
||||
|
||||
return """
|
||||
# Generated by OSGKeyboard. Do not edit; change settings in the host app.
|
||||
schema:
|
||||
schema_id: \(inputSchema.rawValue)
|
||||
name: \(inputSchema.displayName)
|
||||
version: "1.0"
|
||||
author:
|
||||
- OSGKeyboard contributors
|
||||
description: |
|
||||
Commercially permissive OSG schema backed by osg_pinyin.
|
||||
|
||||
switches:
|
||||
- name: ascii_mode
|
||||
reset: 0
|
||||
states: [中, 英]
|
||||
|
||||
engine:
|
||||
processors:
|
||||
- ascii_composer
|
||||
- recognizer
|
||||
- key_binder
|
||||
- speller
|
||||
- punctuator
|
||||
- selector
|
||||
- navigator
|
||||
- express_editor
|
||||
segmentors:
|
||||
- ascii_segmentor
|
||||
- matcher
|
||||
- abc_segmentor
|
||||
- punct_segmentor
|
||||
- fallback_segmentor
|
||||
translators:
|
||||
- punct_translator
|
||||
- script_translator
|
||||
|
||||
speller:
|
||||
alphabet: "\(alphabet)"
|
||||
initials: "\(alphabet.replacingOccurrences(of: ";", with: ""))"
|
||||
delimiter: " '"
|
||||
algebra:
|
||||
\(algebraYAML)
|
||||
|
||||
translator:
|
||||
dictionary: osg_pinyin
|
||||
prism: \(inputSchema.rawValue)
|
||||
enable_sentence: true
|
||||
enable_completion: true
|
||||
enable_user_dict: true
|
||||
initial_quality: 1.2
|
||||
|
||||
punctuator:
|
||||
half_shape:
|
||||
",": ","
|
||||
".": "。"
|
||||
"?": "?"
|
||||
"!": "!"
|
||||
|
||||
key_binder:
|
||||
import_preset: default
|
||||
|
||||
recognizer:
|
||||
import_preset: default
|
||||
"""
|
||||
}
|
||||
|
||||
/// Rules run against full-pinyin dictionary codes before double-pinyin
|
||||
/// transforms, so fuzzy pairs work consistently in all three schemas.
|
||||
public static func fuzzyRules(_ enabled: Set<PinyinFuzzyPair>) -> [String] {
|
||||
var rules: [String] = []
|
||||
for pair in PinyinFuzzyPair.allCases where enabled.contains(pair) {
|
||||
switch pair {
|
||||
case .zhZ:
|
||||
rules += ["derive/^zh/z/", "derive/^z([^h])/zh$1/"]
|
||||
case .chC:
|
||||
rules += ["derive/^ch/c/", "derive/^c([^h])/ch$1/"]
|
||||
case .shS:
|
||||
rules += ["derive/^sh/s/", "derive/^s([^h])/sh$1/"]
|
||||
case .nL:
|
||||
rules += ["derive/^n/l/", "derive/^l/n/"]
|
||||
case .fH:
|
||||
rules += ["derive/^f/h/", "derive/^h/f/"]
|
||||
case .anAng:
|
||||
rules += ["derive/ang$/an/", "derive/an$/ang/"]
|
||||
case .enEng:
|
||||
rules += ["derive/eng$/en/", "derive/en$/eng/"]
|
||||
case .inIng:
|
||||
rules += ["derive/ing$/in/", "derive/in$/ing/"]
|
||||
}
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
private static func algebraRules(for schema: TypingInputSchema) -> [String] {
|
||||
switch schema {
|
||||
case .fullPinyin:
|
||||
return [
|
||||
"derive/^([jqxy])u$/$1v/",
|
||||
"abbrev/^([a-z]).+$/$1/"
|
||||
]
|
||||
|
||||
case .microsoftDoublePinyin, .sogouDoublePinyin:
|
||||
// Microsoft and Sogou's commonly shipped layouts are key-compatible.
|
||||
// Uppercase markers prevent transforms from matching each other.
|
||||
return [
|
||||
"erase/^xx$/",
|
||||
"derive/^([jqxy])u$/$1v/",
|
||||
"derive/^([aoe].*)$/o$1/",
|
||||
"xform/^([ae])(.*)$/$1$1$2/",
|
||||
"xform/iu$/Q/",
|
||||
"xform/[iu]a$/W/",
|
||||
"xform/er$|[uv]an$/R/",
|
||||
"xform/[uv]e$/T/",
|
||||
"xform/v$|uai$/Y/",
|
||||
"xform/^sh/U/",
|
||||
"xform/^ch/I/",
|
||||
"xform/^zh/V/",
|
||||
"xform/uo$/O/",
|
||||
"xform/[uv]n$/P/",
|
||||
"xform/(.)i?ong$/$1S/",
|
||||
"xform/[iu]ang$/D/",
|
||||
"xform/(.)en$/$1F/",
|
||||
"xform/(.)eng$/$1G/",
|
||||
"xform/(.)ang$/$1H/",
|
||||
"xform/ian$/M/",
|
||||
"xform/(.)an$/$1J/",
|
||||
"xform/iao$/C/",
|
||||
"xform/(.)ao$/$1K/",
|
||||
"xform/(.)ai$/$1L/",
|
||||
"xform/(.)ei$/$1Z/",
|
||||
"xform/ie$/X/",
|
||||
"xform/ui$/V/",
|
||||
"derive/T$/V/",
|
||||
"xform/(.)ou$/$1B/",
|
||||
"xform/in$/N/",
|
||||
"xform/ing$/;/",
|
||||
"xlit/QWRTYUIOPSDFGHMJCKLZXVBN/qwrtyuiopsdfghmjcklzxvbn/"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// TypingLayoutProviding.swift
|
||||
// OSGKeyboard · Shared
|
||||
//
|
||||
// Phase 2 escape hatch: replace the in-repo SwiftUI key shell with a
|
||||
// KeyboardKit-based (or other) layout without changing the engine bridge.
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Page shown by the typing key shell.
|
||||
public enum TypingKeyPage: String, CaseIterable, Sendable {
|
||||
case letters
|
||||
case numbers
|
||||
case symbols
|
||||
}
|
||||
|
||||
/// Abstraction over “which characters the current page shows”.
|
||||
/// The Phase 1 SwiftUI keyboard reads this; a future Kit-backed shell
|
||||
/// can feed the same consumer.
|
||||
public protocol TypingLayoutProviding: Sendable {
|
||||
func rows(for page: TypingKeyPage, shiftActive: Bool) -> [[String]]
|
||||
}
|
||||
|
||||
/// Standard phone QWERTY + 123 + light symbols (NanoMouse / system-like).
|
||||
public struct StandardTypingLayout: TypingLayoutProviding {
|
||||
public init() {}
|
||||
|
||||
public func rows(for page: TypingKeyPage, shiftActive: Bool) -> [[String]] {
|
||||
switch page {
|
||||
case .letters:
|
||||
let upper = shiftActive
|
||||
let map: (String) -> String = { upper ? $0.uppercased() : $0 }
|
||||
return [
|
||||
["q", "w", "e", "r", "t", "y", "u", "i", "o", "p"].map(map),
|
||||
["a", "s", "d", "f", "g", "h", "j", "k", "l"].map(map),
|
||||
["⇧", "z", "x", "c", "v", "b", "n", "m", "⌫"].map { $0.count == 1 ? map($0) : $0 }
|
||||
]
|
||||
case .numbers:
|
||||
return [
|
||||
["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
|
||||
["-", "/", ":", ";", "(", ")", "$", "&", "@", "\""],
|
||||
["#+=", ".", ",", "?", "!", "'", "⌫"]
|
||||
]
|
||||
case .symbols:
|
||||
return [
|
||||
["[", "]", "{", "}", "#", "%", "^", "*", "+", "="],
|
||||
["_", "\\", "|", "~", "<", ">", "€", "£", "¥", "·"],
|
||||
["123", ".", ",", "?", "!", "'", "⌫"],
|
||||
[",", "。", "、", "?", "!", ":", ";"]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
// TypingSessionController.swift
|
||||
// OSGKeyboard · Shared
|
||||
//
|
||||
// Owns the typing-surface engine + layout provider. Injected into the
|
||||
// keyboard extension; torn down when leaving typing mode.
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
@MainActor
|
||||
public final class TypingSessionController: ObservableObject {
|
||||
@Published public private(set) var language: TypingInputLanguage = .chinese
|
||||
@Published public private(set) var page: TypingKeyPage = .letters
|
||||
@Published public private(set) var shiftActive: Bool = false
|
||||
@Published public private(set) var capsLock: Bool = false
|
||||
@Published public private(set) var composition: TypingComposition = .empty
|
||||
@Published public private(set) var engineReady: Bool = false
|
||||
@Published public private(set) var schema: TypingInputSchema
|
||||
@Published public var lastError: String?
|
||||
|
||||
public let layout: TypingLayoutProviding
|
||||
private let engine: RimeEngineBridging
|
||||
private var prepared = false
|
||||
|
||||
public init(
|
||||
engine: RimeEngineBridging = LibrimeEngine(),
|
||||
layout: TypingLayoutProviding = StandardTypingLayout()
|
||||
) {
|
||||
self.engine = engine
|
||||
self.layout = layout
|
||||
schema = engine.schema
|
||||
}
|
||||
|
||||
public var keyRows: [[String]] {
|
||||
var rows = layout.rows(for: page, shiftActive: shiftActive || capsLock)
|
||||
if page == .letters,
|
||||
language == .chinese,
|
||||
schema != .fullPinyin,
|
||||
rows.indices.contains(2),
|
||||
rows[2].first == "⇧" {
|
||||
// Microsoft/Sogou use semicolon for "ing"; Chinese composition
|
||||
// does not need Shift, so keep the standard row width stable.
|
||||
rows[2][0] = ";"
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
public func enterTypingMode() {
|
||||
TypingInputConfiguration.shared.reload()
|
||||
Task { await prepareIfNeeded() }
|
||||
}
|
||||
|
||||
public func leaveTypingMode() {
|
||||
engine.teardown()
|
||||
prepared = false
|
||||
engineReady = false
|
||||
composition = .empty
|
||||
page = .letters
|
||||
shiftActive = false
|
||||
capsLock = false
|
||||
}
|
||||
|
||||
public func toggleLanguage() -> String {
|
||||
let next: TypingInputLanguage = language == .chinese ? .english : .chinese
|
||||
return setLanguage(next)
|
||||
}
|
||||
|
||||
/// Selects a specific language for the shared voice / Chinese / English
|
||||
/// capsule. Any active preedit is returned so callers can commit it
|
||||
/// before switching modes.
|
||||
public func setLanguage(_ newLanguage: TypingInputLanguage) -> String {
|
||||
guard language != newLanguage else { return "" }
|
||||
let raw = composition.preedit.isEmpty ? "" : engine.flushPreedit()
|
||||
language = newLanguage
|
||||
engine.setLanguage(newLanguage)
|
||||
composition = engine.composition
|
||||
page = .letters
|
||||
return raw
|
||||
}
|
||||
|
||||
/// Flushes raw preedit, selects the next built-in scheme, and returns the
|
||||
/// raw text that the caller should insert before switching.
|
||||
public func cycleSchema() -> String {
|
||||
let raw = composition.preedit.isEmpty ? "" : engine.flushPreedit()
|
||||
let schemas = TypingInputSchema.allCases
|
||||
let current = schemas.firstIndex(of: schema) ?? 0
|
||||
let next = schemas[(current + 1) % schemas.count]
|
||||
if engine.setSchema(next) {
|
||||
schema = next
|
||||
TypingInputConfiguration.shared.schema = next
|
||||
}
|
||||
composition = engine.composition
|
||||
return raw
|
||||
}
|
||||
|
||||
public func setPage(_ page: TypingKeyPage) {
|
||||
self.page = page
|
||||
shiftActive = false
|
||||
}
|
||||
|
||||
/// Handle a visible key label. Returns text the proxy should insert now
|
||||
/// (may be empty when composing Chinese).
|
||||
public func handleKey(_ label: String) -> String {
|
||||
switch label {
|
||||
case "⇧":
|
||||
if shiftActive {
|
||||
capsLock = true
|
||||
shiftActive = false
|
||||
} else if capsLock {
|
||||
capsLock = false
|
||||
} else {
|
||||
shiftActive = true
|
||||
}
|
||||
return ""
|
||||
case "⌫":
|
||||
if language == .chinese, !engine.composition.preedit.isEmpty {
|
||||
let committed = engine.processBackspace() ?? ""
|
||||
composition = engine.composition
|
||||
return committed
|
||||
}
|
||||
return "\u{8}" // sentinel: caller deletes backward
|
||||
case "123":
|
||||
setPage(.numbers)
|
||||
return ""
|
||||
case "#+=":
|
||||
setPage(.symbols)
|
||||
return ""
|
||||
case "ABC", "abc":
|
||||
setPage(.letters)
|
||||
return ""
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if page != .letters {
|
||||
// Number/symbol: insert directly
|
||||
let out = label
|
||||
if !capsLock { shiftActive = false }
|
||||
return out
|
||||
}
|
||||
|
||||
guard let ch = label.first else { return "" }
|
||||
|
||||
if language == .english {
|
||||
let out = String(ch)
|
||||
if !capsLock { shiftActive = false }
|
||||
return out
|
||||
}
|
||||
|
||||
// Chinese letters → compose
|
||||
let committed = engine.processCharacter(ch) ?? ""
|
||||
composition = engine.composition
|
||||
if !capsLock { shiftActive = false }
|
||||
return committed
|
||||
}
|
||||
|
||||
public func handleSpace() -> String {
|
||||
if language == .english { return " " }
|
||||
let text = engine.processSpace() ?? " "
|
||||
composition = engine.composition
|
||||
return text
|
||||
}
|
||||
|
||||
public func handleReturn() -> String {
|
||||
if language == .english { return "\n" }
|
||||
let text = engine.processReturn() ?? "\n"
|
||||
composition = engine.composition
|
||||
return text
|
||||
}
|
||||
|
||||
public func selectCandidate(at index: Int) -> String {
|
||||
let text = engine.selectCandidate(at: index)
|
||||
composition = engine.composition
|
||||
return text
|
||||
}
|
||||
|
||||
private func prepareIfNeeded() async {
|
||||
guard !prepared else { return }
|
||||
do {
|
||||
try await engine.prepare()
|
||||
engine.setLanguage(language)
|
||||
prepared = true
|
||||
engineReady = engine.isReady
|
||||
schema = engine.schema
|
||||
lastError = nil
|
||||
} catch {
|
||||
lastError = error.localizedDescription
|
||||
engineReady = false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user