feat: add searchable emoji picker
This commit is contained in:
@@ -3,6 +3,7 @@ import Foundation
|
||||
public struct Application: Identifiable, Sendable {
|
||||
public enum Command: String, CaseIterable, Sendable {
|
||||
case translate
|
||||
case emoji
|
||||
}
|
||||
public static let commands = Command.allCases.map { Application(command: $0) }
|
||||
public let command: Command?
|
||||
@@ -20,6 +21,7 @@ public struct Application: Identifiable, Sendable {
|
||||
public init(command: Command) {
|
||||
let name = switch command {
|
||||
case .translate: "Conjure Translate"
|
||||
case .emoji: "Conjure Emoji"
|
||||
}
|
||||
self.init(canonicalURL: URL(string: "conjure-command://\(command.rawValue)")!, name: name, command: command)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import Foundation
|
||||
|
||||
public struct Emoji: Sendable {
|
||||
public let value: String
|
||||
public let name: String
|
||||
public let category: String
|
||||
}
|
||||
|
||||
public enum EmojiCatalog {
|
||||
private struct Entry: Sendable {
|
||||
let emoji: Emoji
|
||||
let searchText: String
|
||||
}
|
||||
|
||||
private static let entries: [Entry] = {
|
||||
// SwiftPM's generated accessor does not search Contents/Resources in a packaged app.
|
||||
let packagedBundle = Bundle.main.resourceURL.flatMap {
|
||||
Bundle(url: $0.appendingPathComponent("Conjure_ConjureKit.bundle", isDirectory: true))
|
||||
}
|
||||
let bundle = packagedBundle ?? Bundle.module
|
||||
guard let url = bundle.url(forResource: "emoji", withExtension: "tsv"),
|
||||
let contents = try? String(contentsOf: url, encoding: .utf8) else {
|
||||
preconditionFailure("The bundled emoji catalog is missing or unreadable")
|
||||
}
|
||||
|
||||
// Pinned github/gemoji v4.1.0; attribution and license accompany the TSV.
|
||||
return contents.split(whereSeparator: \.isNewline).map { line in
|
||||
let fields = line.split(separator: "\t", omittingEmptySubsequences: false)
|
||||
precondition(fields.count == 5, "Invalid bundled emoji catalog row")
|
||||
return Entry(
|
||||
emoji: Emoji(value: String(fields[0]), name: String(fields[1]), category: String(fields[2])),
|
||||
searchText: normalized([fields[0], fields[1], fields[3], fields[4]].joined(separator: " "))
|
||||
)
|
||||
}
|
||||
}()
|
||||
|
||||
public static let all: [Emoji] = entries.map(\.emoji)
|
||||
|
||||
public static let categories: [String] = {
|
||||
var seen = Set<String>()
|
||||
return all.compactMap { seen.insert($0.category).inserted ? $0.category : nil }
|
||||
}()
|
||||
|
||||
/// Matches every query word against names, GitHub aliases, tags, or literal emoji.
|
||||
/// Category names use the exact values supplied by `categories`; results retain catalog order.
|
||||
public static func search(_ query: String, category: String? = nil) -> [Emoji] {
|
||||
let words = normalized(query).split(whereSeparator: \.isWhitespace)
|
||||
if words.isEmpty && category == nil { return all }
|
||||
return entries.compactMap { entry in
|
||||
guard category == nil || entry.emoji.category == category,
|
||||
words.allSatisfy({ entry.searchText.contains($0) }) else { return nil }
|
||||
return entry.emoji
|
||||
}
|
||||
}
|
||||
|
||||
private static func normalized(_ text: String) -> String {
|
||||
text.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: Locale(identifier: "en_US_POSIX"))
|
||||
.replacingOccurrences(of: "_", with: " ")
|
||||
.replacingOccurrences(of: ":", with: " ")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
Emoji catalog derived from github/gemoji v4.1.0.
|
||||
Source: https://raw.githubusercontent.com/github/gemoji/v4.1.0/db/emoji.json
|
||||
License: https://raw.githubusercontent.com/github/gemoji/v4.1.0/LICENSE
|
||||
|
||||
TSV columns: emoji, description, category, space-separated aliases, space-separated tags.
|
||||
|
||||
Copyright (c) 2019 GitHub, Inc.
|
||||
|
||||
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.
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user