├── .gitignore
├── Screenshot.png
├── Assets.xcassets
├── Contents.json
├── AccentColor.colorset
│ └── Contents.json
└── AppIcon.appiconset
│ └── Contents.json
├── README.md
├── StringExplorer.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcshareddata
│ └── xcschemes
│ │ └── StringExplorer.xcscheme
└── project.pbxproj
├── StringExplorer.entitlements
├── LICENSE
└── StringExplorer.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
3 | xcuserdata/
4 |
--------------------------------------------------------------------------------
/Screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unixzii/StringExplorer/HEAD/Screenshot.png
--------------------------------------------------------------------------------
/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # StringExplorer
2 |
3 | A handy tool to explore various string encoding.
4 |
5 | 
6 |
7 | ## License
8 |
9 | MIT
10 |
--------------------------------------------------------------------------------
/StringExplorer.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/StringExplorer.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "platform" : "universal",
6 | "reference" : "systemIndigoColor"
7 | },
8 | "idiom" : "universal"
9 | }
10 | ],
11 | "info" : {
12 | "author" : "xcode",
13 | "version" : 1
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/StringExplorer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Cyandev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/StringExplorer.xcodeproj/xcshareddata/xcschemes/StringExplorer.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
42 |
44 |
50 |
51 |
52 |
53 |
59 |
61 |
67 |
68 |
69 |
70 |
72 |
73 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/StringExplorer.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 2813E9E92B8877590082274A /* StringExplorer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2813E9E82B8877590082274A /* StringExplorer.swift */; };
11 | 2813E9ED2B88775A0082274A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2813E9EC2B88775A0082274A /* Assets.xcassets */; };
12 | /* End PBXBuildFile section */
13 |
14 | /* Begin PBXFileReference section */
15 | 2813E9E52B8877590082274A /* String Explorer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "String Explorer.app"; sourceTree = BUILT_PRODUCTS_DIR; };
16 | 2813E9E82B8877590082274A /* StringExplorer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExplorer.swift; sourceTree = SOURCE_ROOT; };
17 | 2813E9EC2B88775A0082274A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = SOURCE_ROOT; };
18 | 2813E9F12B88775A0082274A /* StringExplorer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = StringExplorer.entitlements; sourceTree = SOURCE_ROOT; };
19 | 2813EA062B89C0B90082274A /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
20 | 2813EA072B89C1360082274A /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; };
21 | /* End PBXFileReference section */
22 |
23 | /* Begin PBXFrameworksBuildPhase section */
24 | 2813E9E22B8877590082274A /* Frameworks */ = {
25 | isa = PBXFrameworksBuildPhase;
26 | buildActionMask = 2147483647;
27 | files = (
28 | );
29 | runOnlyForDeploymentPostprocessing = 0;
30 | };
31 | /* End PBXFrameworksBuildPhase section */
32 |
33 | /* Begin PBXGroup section */
34 | 2813E9DC2B8877590082274A = {
35 | isa = PBXGroup;
36 | children = (
37 | 2813EA072B89C1360082274A /* LICENSE */,
38 | 2813EA062B89C0B90082274A /* README.md */,
39 | 2813E9E82B8877590082274A /* StringExplorer.swift */,
40 | 2813E9EC2B88775A0082274A /* Assets.xcassets */,
41 | 2813E9F12B88775A0082274A /* StringExplorer.entitlements */,
42 | 2813E9E62B8877590082274A /* Products */,
43 | );
44 | sourceTree = "";
45 | };
46 | 2813E9E62B8877590082274A /* Products */ = {
47 | isa = PBXGroup;
48 | children = (
49 | 2813E9E52B8877590082274A /* String Explorer.app */,
50 | );
51 | name = Products;
52 | sourceTree = "";
53 | };
54 | /* End PBXGroup section */
55 |
56 | /* Begin PBXNativeTarget section */
57 | 2813E9E42B8877590082274A /* StringExplorer */ = {
58 | isa = PBXNativeTarget;
59 | buildConfigurationList = 2813E9F42B88775A0082274A /* Build configuration list for PBXNativeTarget "StringExplorer" */;
60 | buildPhases = (
61 | 2813E9E12B8877590082274A /* Sources */,
62 | 2813E9E22B8877590082274A /* Frameworks */,
63 | 2813E9E32B8877590082274A /* Resources */,
64 | );
65 | buildRules = (
66 | );
67 | dependencies = (
68 | );
69 | name = StringExplorer;
70 | productName = StringExplorer;
71 | productReference = 2813E9E52B8877590082274A /* String Explorer.app */;
72 | productType = "com.apple.product-type.application";
73 | };
74 | /* End PBXNativeTarget section */
75 |
76 | /* Begin PBXProject section */
77 | 2813E9DD2B8877590082274A /* Project object */ = {
78 | isa = PBXProject;
79 | attributes = {
80 | BuildIndependentTargetsInParallel = 1;
81 | LastSwiftUpdateCheck = 1520;
82 | LastUpgradeCheck = 1520;
83 | TargetAttributes = {
84 | 2813E9E42B8877590082274A = {
85 | CreatedOnToolsVersion = 15.2;
86 | };
87 | };
88 | };
89 | buildConfigurationList = 2813E9E02B8877590082274A /* Build configuration list for PBXProject "StringExplorer" */;
90 | compatibilityVersion = "Xcode 14.0";
91 | developmentRegion = en;
92 | hasScannedForEncodings = 0;
93 | knownRegions = (
94 | en,
95 | Base,
96 | );
97 | mainGroup = 2813E9DC2B8877590082274A;
98 | productRefGroup = 2813E9E62B8877590082274A /* Products */;
99 | projectDirPath = "";
100 | projectRoot = "";
101 | targets = (
102 | 2813E9E42B8877590082274A /* StringExplorer */,
103 | );
104 | };
105 | /* End PBXProject section */
106 |
107 | /* Begin PBXResourcesBuildPhase section */
108 | 2813E9E32B8877590082274A /* Resources */ = {
109 | isa = PBXResourcesBuildPhase;
110 | buildActionMask = 2147483647;
111 | files = (
112 | 2813E9ED2B88775A0082274A /* Assets.xcassets in Resources */,
113 | );
114 | runOnlyForDeploymentPostprocessing = 0;
115 | };
116 | /* End PBXResourcesBuildPhase section */
117 |
118 | /* Begin PBXSourcesBuildPhase section */
119 | 2813E9E12B8877590082274A /* Sources */ = {
120 | isa = PBXSourcesBuildPhase;
121 | buildActionMask = 2147483647;
122 | files = (
123 | 2813E9E92B8877590082274A /* StringExplorer.swift in Sources */,
124 | );
125 | runOnlyForDeploymentPostprocessing = 0;
126 | };
127 | /* End PBXSourcesBuildPhase section */
128 |
129 | /* Begin XCBuildConfiguration section */
130 | 2813E9F22B88775A0082274A /* Debug */ = {
131 | isa = XCBuildConfiguration;
132 | buildSettings = {
133 | ALWAYS_SEARCH_USER_PATHS = NO;
134 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
135 | CLANG_ANALYZER_NONNULL = YES;
136 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
137 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
138 | CLANG_ENABLE_MODULES = YES;
139 | CLANG_ENABLE_OBJC_ARC = YES;
140 | CLANG_ENABLE_OBJC_WEAK = YES;
141 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
142 | CLANG_WARN_BOOL_CONVERSION = YES;
143 | CLANG_WARN_COMMA = YES;
144 | CLANG_WARN_CONSTANT_CONVERSION = YES;
145 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
146 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
147 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
148 | CLANG_WARN_EMPTY_BODY = YES;
149 | CLANG_WARN_ENUM_CONVERSION = YES;
150 | CLANG_WARN_INFINITE_RECURSION = YES;
151 | CLANG_WARN_INT_CONVERSION = YES;
152 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
153 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
154 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
155 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
156 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
157 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
158 | CLANG_WARN_STRICT_PROTOTYPES = YES;
159 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
160 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
161 | CLANG_WARN_UNREACHABLE_CODE = YES;
162 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
163 | COPY_PHASE_STRIP = NO;
164 | DEBUG_INFORMATION_FORMAT = dwarf;
165 | ENABLE_STRICT_OBJC_MSGSEND = YES;
166 | ENABLE_TESTABILITY = YES;
167 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
168 | GCC_C_LANGUAGE_STANDARD = gnu17;
169 | GCC_DYNAMIC_NO_PIC = NO;
170 | GCC_NO_COMMON_BLOCKS = YES;
171 | GCC_OPTIMIZATION_LEVEL = 0;
172 | GCC_PREPROCESSOR_DEFINITIONS = (
173 | "DEBUG=1",
174 | "$(inherited)",
175 | );
176 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
177 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
178 | GCC_WARN_UNDECLARED_SELECTOR = YES;
179 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
180 | GCC_WARN_UNUSED_FUNCTION = YES;
181 | GCC_WARN_UNUSED_VARIABLE = YES;
182 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
183 | MACOSX_DEPLOYMENT_TARGET = 14.2;
184 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
185 | MTL_FAST_MATH = YES;
186 | ONLY_ACTIVE_ARCH = YES;
187 | SDKROOT = macosx;
188 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
189 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
190 | };
191 | name = Debug;
192 | };
193 | 2813E9F32B88775A0082274A /* Release */ = {
194 | isa = XCBuildConfiguration;
195 | buildSettings = {
196 | ALWAYS_SEARCH_USER_PATHS = NO;
197 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
198 | CLANG_ANALYZER_NONNULL = YES;
199 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
200 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
201 | CLANG_ENABLE_MODULES = YES;
202 | CLANG_ENABLE_OBJC_ARC = YES;
203 | CLANG_ENABLE_OBJC_WEAK = YES;
204 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
205 | CLANG_WARN_BOOL_CONVERSION = YES;
206 | CLANG_WARN_COMMA = YES;
207 | CLANG_WARN_CONSTANT_CONVERSION = YES;
208 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
209 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
210 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
211 | CLANG_WARN_EMPTY_BODY = YES;
212 | CLANG_WARN_ENUM_CONVERSION = YES;
213 | CLANG_WARN_INFINITE_RECURSION = YES;
214 | CLANG_WARN_INT_CONVERSION = YES;
215 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
216 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
217 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
218 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
219 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
220 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
221 | CLANG_WARN_STRICT_PROTOTYPES = YES;
222 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
223 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
224 | CLANG_WARN_UNREACHABLE_CODE = YES;
225 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
226 | COPY_PHASE_STRIP = NO;
227 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
228 | ENABLE_NS_ASSERTIONS = NO;
229 | ENABLE_STRICT_OBJC_MSGSEND = YES;
230 | ENABLE_USER_SCRIPT_SANDBOXING = YES;
231 | GCC_C_LANGUAGE_STANDARD = gnu17;
232 | GCC_NO_COMMON_BLOCKS = YES;
233 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
234 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
235 | GCC_WARN_UNDECLARED_SELECTOR = YES;
236 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
237 | GCC_WARN_UNUSED_FUNCTION = YES;
238 | GCC_WARN_UNUSED_VARIABLE = YES;
239 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
240 | MACOSX_DEPLOYMENT_TARGET = 14.2;
241 | MTL_ENABLE_DEBUG_INFO = NO;
242 | MTL_FAST_MATH = YES;
243 | SDKROOT = macosx;
244 | SWIFT_COMPILATION_MODE = wholemodule;
245 | };
246 | name = Release;
247 | };
248 | 2813E9F52B88775A0082274A /* Debug */ = {
249 | isa = XCBuildConfiguration;
250 | buildSettings = {
251 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
252 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
253 | CODE_SIGN_STYLE = Automatic;
254 | COMBINE_HIDPI_IMAGES = YES;
255 | CURRENT_PROJECT_VERSION = 1;
256 | ENABLE_PREVIEWS = YES;
257 | GENERATE_INFOPLIST_FILE = YES;
258 | INFOPLIST_KEY_CFBundleDisplayName = "String Explorer";
259 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
260 | LD_RUNPATH_SEARCH_PATHS = (
261 | "$(inherited)",
262 | "@executable_path/../Frameworks",
263 | );
264 | MARKETING_VERSION = 1.0;
265 | PRODUCT_BUNDLE_IDENTIFIER = me.cyandev.StringExplorer;
266 | PRODUCT_NAME = "String Explorer";
267 | SWIFT_EMIT_LOC_STRINGS = YES;
268 | SWIFT_VERSION = 5.0;
269 | };
270 | name = Debug;
271 | };
272 | 2813E9F62B88775A0082274A /* Release */ = {
273 | isa = XCBuildConfiguration;
274 | buildSettings = {
275 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
276 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
277 | CODE_SIGN_STYLE = Automatic;
278 | COMBINE_HIDPI_IMAGES = YES;
279 | CURRENT_PROJECT_VERSION = 1;
280 | ENABLE_PREVIEWS = YES;
281 | GENERATE_INFOPLIST_FILE = YES;
282 | INFOPLIST_KEY_CFBundleDisplayName = "String Explorer";
283 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
284 | LD_RUNPATH_SEARCH_PATHS = (
285 | "$(inherited)",
286 | "@executable_path/../Frameworks",
287 | );
288 | MARKETING_VERSION = 1.0;
289 | PRODUCT_BUNDLE_IDENTIFIER = me.cyandev.StringExplorer;
290 | PRODUCT_NAME = "String Explorer";
291 | SWIFT_EMIT_LOC_STRINGS = YES;
292 | SWIFT_VERSION = 5.0;
293 | };
294 | name = Release;
295 | };
296 | /* End XCBuildConfiguration section */
297 |
298 | /* Begin XCConfigurationList section */
299 | 2813E9E02B8877590082274A /* Build configuration list for PBXProject "StringExplorer" */ = {
300 | isa = XCConfigurationList;
301 | buildConfigurations = (
302 | 2813E9F22B88775A0082274A /* Debug */,
303 | 2813E9F32B88775A0082274A /* Release */,
304 | );
305 | defaultConfigurationIsVisible = 0;
306 | defaultConfigurationName = Release;
307 | };
308 | 2813E9F42B88775A0082274A /* Build configuration list for PBXNativeTarget "StringExplorer" */ = {
309 | isa = XCConfigurationList;
310 | buildConfigurations = (
311 | 2813E9F52B88775A0082274A /* Debug */,
312 | 2813E9F62B88775A0082274A /* Release */,
313 | );
314 | defaultConfigurationIsVisible = 0;
315 | defaultConfigurationName = Release;
316 | };
317 | /* End XCConfigurationList section */
318 | };
319 | rootObject = 2813E9DD2B8877590082274A /* Project object */;
320 | }
321 |
--------------------------------------------------------------------------------
/StringExplorer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StringExplorer.swift
3 | // StringExplorer
4 | //
5 | // Created by Cyandev on 2024/2/23.
6 | //
7 |
8 | import SwiftUI
9 | import Observation
10 |
11 | @Observable
12 | class ViewContext {
13 |
14 | var showsUnicodeScalar = true
15 | var showsUTF16CodeUnit = true
16 | var showsUTF8CodeUnit = true
17 |
18 | var hexMode = true
19 |
20 | var highlightedGroupID: Int?
21 |
22 | init() { }
23 | }
24 |
25 | fileprivate struct AppView: View {
26 |
27 | private enum Field: Hashable {
28 | case input
29 | case hexMode
30 | }
31 |
32 | @State private var input: String = ""
33 | @State private var viewContext = ViewContext()
34 | @FocusState private var focusedField: Field?
35 |
36 | var body: some View {
37 | VStack(spacing: 0) {
38 | VStack {
39 | TextField("Input", text: $input)
40 | .focused($focusedField, equals: .input)
41 |
42 | HStack {
43 | FilterToggle(title: "Unicode Scalar", keyPath: \.showsUnicodeScalar)
44 | FilterToggle(title: "UTF-16", keyPath: \.showsUTF16CodeUnit)
45 | .foregroundStyle(Color(nsColor: .systemOrange))
46 | FilterToggle(title: "UTF-8", keyPath: \.showsUTF8CodeUnit)
47 | .foregroundStyle(Color(nsColor: .systemBlue))
48 |
49 | Spacer()
50 |
51 | Toggle("Hex", isOn: .init(get: {
52 | return viewContext.hexMode
53 | }, set: { newValue in
54 | viewContext.hexMode = newValue
55 | }))
56 | .toggleStyle(SwitchToggleStyle())
57 | .controlSize(.mini)
58 | .bold()
59 | .focused($focusedField, equals: .hexMode)
60 | .help("Show values in hexadecimal")
61 | }
62 | }
63 | .padding()
64 |
65 | Divider()
66 |
67 | CharacterCellProvider(string: input) { cells in
68 | CharacterCellGridView(cells: cells)
69 | }
70 | .equatable()
71 | }
72 | .environment(viewContext)
73 | .onAppear {
74 | focusedField = .input
75 | }
76 | }
77 | }
78 |
79 | fileprivate struct FilterToggle: View {
80 |
81 | let title: String
82 | let keyPath: WritableKeyPath
83 |
84 | @Environment(ViewContext.self) private var viewContext
85 |
86 | private var checkedFilters: Int {
87 | let filterValues = [
88 | viewContext.showsUnicodeScalar,
89 | viewContext.showsUTF16CodeUnit,
90 | viewContext.showsUTF8CodeUnit
91 | ]
92 | return filterValues.filter(Bool.init(_:)).count
93 | }
94 |
95 | var body: some View {
96 | Toggle(title, isOn: .init(get: {
97 | return viewContext[keyPath: keyPath]
98 | }, set: { newValue in
99 | var viewContext = self.viewContext
100 | viewContext[keyPath: keyPath] = newValue
101 | }))
102 | .font(.system(size: 12, weight: .bold).monospaced())
103 | .disabled(checkedFilters <= 1 && viewContext[keyPath: keyPath])
104 | }
105 | }
106 |
107 | fileprivate struct CharacterCell: Hashable {
108 |
109 | struct IndexedValue: Hashable {
110 |
111 | let index: Int
112 | let value: T
113 | }
114 |
115 | let groupID: Int
116 |
117 | let character: IndexedValue?
118 | let unicodeScalar: IndexedValue?
119 | let utf16CodeUnit: IndexedValue?
120 | let utf8CodeUnit: IndexedValue?
121 | }
122 |
123 | extension CharacterCell: Identifiable {
124 |
125 | var id: Int {
126 | return hashValue
127 | }
128 | }
129 |
130 | fileprivate struct WalkState {
131 |
132 | let view: V
133 | private var index: V.Index
134 | private(set) var onHold = false
135 | private var holdAfterSteps: Int? = nil
136 |
137 | init(_ view: V) {
138 | self.view = view
139 | index = view.startIndex
140 | }
141 |
142 | mutating func next() -> V.Element? {
143 | guard !onHold && index != view.endIndex else {
144 | return nil
145 | }
146 |
147 | defer {
148 | index = view.index(after: index)
149 | }
150 |
151 | if let holdAfterSteps {
152 | self.holdAfterSteps = holdAfterSteps - 1
153 | onHold = holdAfterSteps == 1
154 | }
155 |
156 | return view[index]
157 | }
158 |
159 | mutating func hold() {
160 | onHold = true
161 | holdAfterSteps = nil
162 | }
163 |
164 | mutating func hold(after steps: Int) {
165 | holdAfterSteps = steps
166 | }
167 |
168 | mutating func resume() {
169 | onHold = false
170 | holdAfterSteps = nil
171 | }
172 | }
173 |
174 | fileprivate struct CharacterCellProvider: View, Equatable where Content: View {
175 |
176 | let string: String
177 | let content: ([CharacterCell]) -> Content
178 |
179 | static func == (lhs: CharacterCellProvider, rhs: CharacterCellProvider) -> Bool {
180 | lhs.string == rhs.string
181 | }
182 |
183 | init(string: String, @ViewBuilder content: @escaping ([CharacterCell]) -> Content) {
184 | self.string = string
185 | self.content = content
186 | }
187 |
188 | var body: Content {
189 | var cells = [CharacterCell]()
190 |
191 | var characterRawIndex = 0
192 | var unicodeScalarRawIndex = 0
193 | var utf16RawIndex = 0
194 | var utf8RawIndex = 0
195 |
196 | for character in string {
197 | var unicodeWalkState = WalkState(character.unicodeScalars)
198 | var utf16WalkState = WalkState(character.utf16)
199 | var utf8WalkState = WalkState(character.utf8)
200 |
201 | var firstCellOfCharacter = true
202 | while true {
203 | var unicodeScalar: CharacterCell.IndexedValue? = nil
204 | if let value = unicodeWalkState.next() {
205 | unicodeScalar = .init(index: unicodeScalarRawIndex, value: value)
206 | unicodeScalarRawIndex += 1
207 | unicodeWalkState.hold()
208 | }
209 |
210 | var utf16CodeUnit: CharacterCell.IndexedValue? = nil
211 | if let value = utf16WalkState.next() {
212 | utf16CodeUnit = .init(index: utf16RawIndex, value: value)
213 | utf16RawIndex += 1
214 | if (value >> 10) != 0x36 {
215 | utf16WalkState.hold()
216 | }
217 | }
218 |
219 | var utf8CodeUnit: CharacterCell.IndexedValue? = nil
220 | if let value = utf8WalkState.next() {
221 | utf8CodeUnit = .init(index: utf8RawIndex, value: value)
222 | utf8RawIndex += 1
223 | if value & 0xc0 == 0x80 {
224 | // Don't update walking state at the continuation byte.
225 | } else if value & 0x80 == 0 {
226 | utf8WalkState.hold()
227 | } else if value & 0xe0 == 0xc0 {
228 | utf8WalkState.hold(after: 1)
229 | } else if value & 0xf0 == 0xe0 {
230 | utf8WalkState.hold(after: 2)
231 | } else {
232 | utf8WalkState.hold(after: 3)
233 | }
234 | }
235 |
236 | // UTF8 sequence should always be the longest one, resume walking
237 | // all the sequences when we meet the Unicode scalar boundary.
238 | if utf8WalkState.onHold {
239 | unicodeWalkState.resume()
240 | utf16WalkState.resume()
241 | utf8WalkState.resume()
242 | }
243 |
244 | if unicodeScalar == nil && utf16CodeUnit == nil && utf8CodeUnit == nil {
245 | break
246 | }
247 |
248 | let characterValue: CharacterCell.IndexedValue? = if firstCellOfCharacter {
249 | .init(index: characterRawIndex, value: character)
250 | } else {
251 | nil
252 | }
253 |
254 | let cell = CharacterCell(
255 | groupID: characterRawIndex,
256 | character: characterValue,
257 | unicodeScalar: unicodeScalar,
258 | utf16CodeUnit: utf16CodeUnit,
259 | utf8CodeUnit: utf8CodeUnit
260 | )
261 | cells.append(cell)
262 |
263 | firstCellOfCharacter = false
264 | }
265 |
266 | characterRawIndex += 1
267 | }
268 |
269 | return content(cells)
270 | }
271 | }
272 |
273 | fileprivate struct CharacterCellGridView: View {
274 |
275 | let cells: [CharacterCell]
276 |
277 | @State private var gridColumns = [GridItem]()
278 | @State private var filteredCells = [CharacterCell]()
279 | @Environment(ViewContext.self) private var viewContext
280 |
281 | var body: some View {
282 | ScrollView(.vertical) {
283 | LazyVGrid(columns: gridColumns, alignment: .leading, spacing: 4) {
284 | ForEach(filteredCells) { cell in
285 | RubyView(cell: cell)
286 | }
287 | }
288 | .padding(4)
289 | }
290 | .frame(minWidth: 400)
291 | .background {
292 | GeometryReader { geometry in
293 | Color.clear
294 | .onChange(of: geometry.size) { _, newValue in
295 | updateGridColumns(with: newValue)
296 | }
297 | .onAppear {
298 | updateGridColumns(with: geometry.size)
299 | }
300 | }
301 | }
302 | .background(Color(nsColor: .controlBackgroundColor))
303 | .onChange(of: [
304 | viewContext.showsUnicodeScalar,
305 | viewContext.showsUTF16CodeUnit,
306 | viewContext.showsUTF8CodeUnit
307 | ]) { _, _ in
308 | reloadFilter()
309 | }
310 | .onChange(of: cells, { _, _ in
311 | reloadFilter()
312 | })
313 | .onAppear {
314 | reloadFilter()
315 | }
316 | }
317 |
318 | func updateGridColumns(with size: CGSize) {
319 | let gridWidth: CGFloat = 80
320 | let spacing: CGFloat = 4
321 | let numberOfColumns = Int(floor((size.width - 8) / (gridWidth + spacing)))
322 | let templateItem = GridItem(.fixed(gridWidth), spacing: spacing)
323 | gridColumns = .init(repeating: templateItem, count: numberOfColumns)
324 | }
325 |
326 | func reloadFilter() {
327 | filteredCells = cells.filter { cell in
328 | let unicodeScalarVisible = cell.unicodeScalar != nil && viewContext.showsUnicodeScalar
329 | let utf16CodeUnitVisible = cell.utf16CodeUnit != nil && viewContext.showsUTF16CodeUnit
330 | let utf8CodeUnitVisible = cell.utf8CodeUnit != nil && viewContext.showsUTF8CodeUnit
331 | return unicodeScalarVisible || utf16CodeUnitVisible || utf8CodeUnitVisible
332 | }
333 | }
334 | }
335 |
336 | fileprivate struct RubyView: View {
337 |
338 | @Environment(ViewContext.self) private var viewContext
339 |
340 | let cell: CharacterCell
341 |
342 | var isHighlighted: Bool {
343 | return viewContext.highlightedGroupID == cell.groupID
344 | }
345 |
346 | var body: some View {
347 | VStack(alignment: .leading, spacing: 0) {
348 | if let character = cell.character {
349 | makeText(String(character.value), index: character.index)
350 | } else {
351 | makeText(" ", index: nil)
352 | }
353 |
354 | if viewContext.showsUnicodeScalar {
355 | makeDivider()
356 | if let unicodeScalar = cell.unicodeScalar {
357 | makeText(String(unicodeScalar.value), index: unicodeScalar.index)
358 | } else {
359 | makeText(" ", index: nil)
360 | }
361 | }
362 |
363 | if viewContext.showsUTF16CodeUnit {
364 | makeDivider()
365 | if let utf16CodeUnit = cell.utf16CodeUnit {
366 | makeText(formatCodeUnit(utf16CodeUnit.value, hexMinimumLength: 4),
367 | index: utf16CodeUnit.index,
368 | monospaced: true,
369 | color: .systemOrange)
370 | } else {
371 | makeText(" ", index: nil)
372 | }
373 | }
374 |
375 | if viewContext.showsUTF8CodeUnit {
376 | makeDivider()
377 | if let utf8CodeUnit = cell.utf8CodeUnit {
378 | makeText(formatCodeUnit(utf8CodeUnit.value, hexMinimumLength: 2),
379 | index: utf8CodeUnit.index,
380 | monospaced: true,
381 | color: .systemBlue)
382 | } else {
383 | makeText(" ", index: nil)
384 | }
385 | }
386 | }
387 | .padding(6)
388 | .background {
389 | Color(nsColor: .textColor)
390 | .opacity(isHighlighted ? 0.08 : 0.05)
391 | .animation(.linear(duration: 0.1), value: isHighlighted)
392 |
393 | }
394 | .clipShape(RoundedRectangle(cornerSize: .init(width: 6, height: 6)))
395 | .onHover { hovering in
396 | if hovering {
397 | viewContext.highlightedGroupID = cell.groupID
398 | } else {
399 | viewContext.highlightedGroupID = nil
400 | }
401 | }
402 | }
403 |
404 | private func makeDivider() -> some View {
405 | return Color(nsColor: .separatorColor)
406 | .frame(height: 1)
407 | .padding(.vertical, 1)
408 | }
409 |
410 | private func makeText(_ text: String,
411 | index: Int?,
412 | monospaced: Bool = false,
413 | color: NSColor? = nil) -> some View {
414 | VStack(alignment: .leading, spacing: 2) {
415 | Text(index.map(String.init) ?? " ")
416 | .font(.system(size: 10).monospaced())
417 | .opacity(0.5)
418 | Text(text)
419 | .foregroundStyle(Color(nsColor: color ?? .textColor))
420 | .font(monospaced ? .system(size: 12).monospaced() : .system(size: 12))
421 | }
422 | .padding(.init(top: 2, leading: 4, bottom: 2, trailing: 4))
423 | }
424 |
425 | private func formatCodeUnit(_ value: T,
426 | hexMinimumLength: Int) -> String {
427 | if viewContext.hexMode {
428 | return value.hexString(minimumLength: hexMinimumLength)
429 | } else {
430 | return String(value)
431 | }
432 | }
433 | }
434 |
435 | fileprivate extension BinaryInteger {
436 |
437 | func hexString(minimumLength: Int) -> String {
438 | var string = String(self, radix: 16)
439 | if string.count < minimumLength {
440 | let padding = minimumLength - string.count
441 | string = String(repeating: "0", count: padding) + string
442 | }
443 | return "0x\(string)"
444 | }
445 | }
446 |
447 | @main
448 | struct App: SwiftUI.App {
449 |
450 | var body: some Scene {
451 | WindowGroup {
452 | AppView()
453 | }
454 | }
455 | }
456 |
--------------------------------------------------------------------------------