From 0c20900da1903ad58181d68ab77e0598a9655757 Mon Sep 17 00:00:00 2001 From: Stephen B <158498287+StephenDev0@users.noreply.github.com> Date: Thu, 17 Jul 2025 23:17:08 -0400 Subject: [PATCH 1/7] Add block script editor --- README.md | 5 +- StikJIT/JSSupport/BlockScript.swift | 51 ++++++++++++++ StikJIT/JSSupport/BlockScriptEditorView.swift | 70 +++++++++++++++++++ StikJIT/JSSupport/ScriptListView.swift | 38 ++++++++-- StikJIT/Views/HomeView.swift | 12 +++- StikJIT/Views/SettingsView.swift | 9 ++- 6 files changed, 172 insertions(+), 13 deletions(-) create mode 100644 StikJIT/JSSupport/BlockScript.swift create mode 100644 StikJIT/JSSupport/BlockScriptEditorView.swift diff --git a/README.md b/README.md index 0fe321d0..722829f9 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,9 @@ ## Features - On-device debugging/Just-In-Time (JIT) compilation for supported apps via [`idevice`](https://github.com/jkcoxson/idevice). - Seamless integration with our custom built loopback vpn. -- Native UI for managing debugging/JIT-enabling. -- No data collection—ensuring full privacy. +- Native UI for managing debugging/JIT-enabling. +- Optional block-based script editor for building scripts using a Scratch-like interface. +- No data collection—ensuring full privacy. ## License StikDebug is licensed under **AGPL-3.0**. See [`LICENSE`](LICENSE) for details. diff --git a/StikJIT/JSSupport/BlockScript.swift b/StikJIT/JSSupport/BlockScript.swift new file mode 100644 index 00000000..49c3e3c0 --- /dev/null +++ b/StikJIT/JSSupport/BlockScript.swift @@ -0,0 +1,51 @@ +import Foundation + +enum BlockType: String, Codable, CaseIterable, Identifiable { + case sendCommand + case log + + var id: String { rawValue } +} + +struct Block: Codable, Identifiable { + var id = UUID() + var type: BlockType = .sendCommand + var value: String = "" +} + +struct BlockScript: Codable { + var blocks: [Block] = [] + + func generateJS() -> String { + blocks.map { block in + let escaped = block.value + .replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "\"", with: "\\\"") + switch block.type { + case .sendCommand: + return "send_command(\"\(escaped)\")" + case .log: + return "log(\"\(escaped)\")" + } + }.joined(separator: "\n") + } + + static func load(from url: URL) -> BlockScript { + if let data = try? Data(contentsOf: url), + let script = try? JSONDecoder().decode(BlockScript.self, from: data) { + return script + } + return BlockScript() + } + + func save(to url: URL) { + if let data = try? JSONEncoder().encode(self) { + try? data.write(to: url) + } + } + + func saveAsJS(to url: URL) { + let js = generateJS() + try? js.write(to: url, atomically: true, encoding: .utf8) + } +} diff --git a/StikJIT/JSSupport/BlockScriptEditorView.swift b/StikJIT/JSSupport/BlockScriptEditorView.swift new file mode 100644 index 00000000..9d631fb1 --- /dev/null +++ b/StikJIT/JSSupport/BlockScriptEditorView.swift @@ -0,0 +1,70 @@ +import SwiftUI +import Foundation + +struct BlockScriptEditorView: View { + let scriptURL: URL + @State private var script = BlockScript() + @Environment(\.dismiss) private var dismiss + + var body: some View { + VStack { + List { + ForEach($script.blocks) { $block in + BlockRow(block: $block) + } + .onDelete { script.blocks.remove(atOffsets: $0) } + .onMove { script.blocks.move(fromOffsets: $0, toOffset: $1) } + } + .toolbar { EditButton() } + HStack { + Button("Cancel") { dismiss() } + Spacer() + Button("Save") { + saveScript() + dismiss() + } + } + .padding() + } + .navigationTitle(scriptURL.lastPathComponent) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Menu("Add Block") { + ForEach(BlockType.allCases) { type in + Button(type.rawValue) { + script.blocks.append(Block(type: type)) + } + } + } + } + } + .onAppear(perform: loadScript) + } + + private func loadScript() { + script = BlockScript.load(from: scriptURL) + } + + private func saveScript() { + script.save(to: scriptURL) + let jsURL = scriptURL.deletingPathExtension().appendingPathExtension("js") + script.saveAsJS(to: jsURL) + } +} + +struct BlockRow: View { + @Binding var block: Block + + var body: some View { + HStack { + Picker("Type", selection: $block.type) { + ForEach(BlockType.allCases) { type in + Text(type.rawValue).tag(type) + } + } + .pickerStyle(.menu) + TextField("Value", text: $block.value) + } + } +} diff --git a/StikJIT/JSSupport/ScriptListView.swift b/StikJIT/JSSupport/ScriptListView.swift index e9936e14..d73e9f52 100644 --- a/StikJIT/JSSupport/ScriptListView.swift +++ b/StikJIT/JSSupport/ScriptListView.swift @@ -7,6 +7,7 @@ import SwiftUI import UniformTypeIdentifiers +import Foundation struct ScriptListView: View { @State private var scripts: [URL] = [] @@ -14,6 +15,7 @@ struct ScriptListView: View { @State private var newFileName = "" @State private var showImporter = false @AppStorage("DefaultScriptName") private var defaultScriptName = "attachDetach.js" + @AppStorage("useBlockEditor") private var useBlockEditor = false var onSelectScript: ((URL?) -> Void)? = nil @@ -42,7 +44,11 @@ struct ScriptListView: View { } } else { NavigationLink { - ScriptEditorView(scriptURL: script) + if script.pathExtension.lowercased() == "jsb" { + BlockScriptEditorView(scriptURL: script) + } else { + ScriptEditorView(scriptURL: script) + } } label: { HStack { Text(script.lastPathComponent) @@ -97,7 +103,10 @@ struct ScriptListView: View { } .fileImporter( isPresented: $showImporter, - allowedContentTypes: [UTType(filenameExtension: "js") ?? .plainText], + allowedContentTypes: [ + UTType(filenameExtension: "js") ?? .plainText, + UTType(filenameExtension: "jsb") ?? .plainText + ], allowsMultipleSelection: false ) { result in switch result { @@ -158,7 +167,7 @@ struct ScriptListView: View { scripts = (try? FileManager .default .contentsOfDirectory(at: dir, includingPropertiesForKeys: nil))? - .filter { $0.pathExtension.lowercased() == "js" } ?? [] + .filter { ["js", "jsb"].contains($0.pathExtension.lowercased()) } ?? [] } private func saveDefaultScript(_ url: URL) { @@ -168,8 +177,14 @@ struct ScriptListView: View { private func createNewScript() { guard !newFileName.isEmpty else { return } var filename = newFileName - if !filename.hasSuffix(".js") { - filename += ".js" + if useBlockEditor { + if !filename.hasSuffix(".jsb") { + filename += ".jsb" + } + } else { + if !filename.hasSuffix(".js") { + filename += ".js" + } } let newURL = scriptsDirectory().appendingPathComponent(filename) @@ -181,7 +196,14 @@ struct ScriptListView: View { } do { - try "".write(to: newURL, atomically: true, encoding: .utf8) + if useBlockEditor { + let script = BlockScript() + script.save(to: newURL) + let jsURL = newURL.deletingPathExtension().appendingPathExtension("js") + script.saveAsJS(to: jsURL) + } else { + try "".write(to: newURL, atomically: true, encoding: .utf8) + } newFileName = "" loadScripts() } catch { @@ -192,6 +214,10 @@ struct ScriptListView: View { private func deleteScript(_ url: URL) { do { try FileManager.default.removeItem(at: url) + if url.pathExtension.lowercased() == "jsb" { + let jsURL = url.deletingPathExtension().appendingPathExtension("js") + try? FileManager.default.removeItem(at: jsURL) + } if url.lastPathComponent == defaultScriptName { UserDefaults.standard.removeObject(forKey: "DefaultScriptName") } diff --git a/StikJIT/Views/HomeView.swift b/StikJIT/Views/HomeView.swift index 49d2a5b0..628fbf6c 100644 --- a/StikJIT/Views/HomeView.swift +++ b/StikJIT/Views/HomeView.swift @@ -8,6 +8,7 @@ import SwiftUI import UniformTypeIdentifiers import Pipify +import Foundation struct JITEnableConfiguration { var bundleID: String? = nil @@ -454,14 +455,19 @@ struct HomeView: View { if scriptData == nil, let scriptName { let selectedScriptURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] .appendingPathComponent("scripts").appendingPathComponent(scriptName) - + if FileManager.default.fileExists(atPath: selectedScriptURL.path) { do { - scriptData = try Data(contentsOf: selectedScriptURL) + if selectedScriptURL.pathExtension.lowercased() == "jsb" { + let block = BlockScript.load(from: selectedScriptURL) + scriptData = block.generateJS().data(using: .utf8) + } else { + scriptData = try Data(contentsOf: selectedScriptURL) + } } catch { print("failed to load data from script \(error)") } - + } } } else { diff --git a/StikJIT/Views/SettingsView.swift b/StikJIT/Views/SettingsView.swift index fe32932a..0f079144 100644 --- a/StikJIT/Views/SettingsView.swift +++ b/StikJIT/Views/SettingsView.swift @@ -14,6 +14,7 @@ struct SettingsView: View { @AppStorage("useDefaultScript") private var useDefaultScript = false @AppStorage("enableAdvancedOptions") private var enableAdvancedOptions = false @AppStorage("enablePiP") private var enablePiP = false + @AppStorage("useBlockEditor") private var useBlockEditor = false @State private var isShowingPairingFilePicker = false @Environment(\.colorScheme) private var colorScheme @@ -284,6 +285,9 @@ struct SettingsView: View { Toggle("Picture in Picture", isOn: $enablePiP) .foregroundColor(.primary) .padding(.vertical, 6) + Toggle("Use Block Script Editor", isOn: $useBlockEditor) + .foregroundColor(.primary) + .padding(.vertical, 6) } } .padding(.vertical, 20) @@ -292,9 +296,10 @@ struct SettingsView: View { if !newValue { useDefaultScript = false enablePiP = false + useBlockEditor = false } - } - } + } + } // About section SettingsCard { From 7d6f3e458bea69e9fb26e125d20d61d953c90607 Mon Sep 17 00:00:00 2001 From: Stephen B <158498287+StephenDev0@users.noreply.github.com> Date: Thu, 17 Jul 2025 23:46:44 -0400 Subject: [PATCH 2/7] Improve block editor and extend commands --- StikJIT/JSSupport/BlockScript.swift | 22 +++++++++++++++++++ StikJIT/JSSupport/BlockScriptEditorView.swift | 16 ++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/StikJIT/JSSupport/BlockScript.swift b/StikJIT/JSSupport/BlockScript.swift index 49c3e3c0..84efe98d 100644 --- a/StikJIT/JSSupport/BlockScript.swift +++ b/StikJIT/JSSupport/BlockScript.swift @@ -3,8 +3,24 @@ import Foundation enum BlockType: String, Codable, CaseIterable, Identifiable { case sendCommand case log + case getPid + case hasTXM + case prepareMemoryRegion var id: String { rawValue } + + var placeholder: String { + switch self { + case .sendCommand: + return "Command" + case .log: + return "Message" + case .prepareMemoryRegion: + return "startAddr,size" + case .getPid, .hasTXM: + return "" + } + } } struct Block: Codable, Identifiable { @@ -26,6 +42,12 @@ struct BlockScript: Codable { return "send_command(\"\(escaped)\")" case .log: return "log(\"\(escaped)\")" + case .getPid: + return "get_pid()" + case .hasTXM: + return "hasTXM()" + case .prepareMemoryRegion: + return "prepare_memory_region(\(block.value))" } }.joined(separator: "\n") } diff --git a/StikJIT/JSSupport/BlockScriptEditorView.swift b/StikJIT/JSSupport/BlockScriptEditorView.swift index 9d631fb1..9c208a16 100644 --- a/StikJIT/JSSupport/BlockScriptEditorView.swift +++ b/StikJIT/JSSupport/BlockScriptEditorView.swift @@ -7,7 +7,7 @@ struct BlockScriptEditorView: View { @Environment(\.dismiss) private var dismiss var body: some View { - VStack { + VStack(spacing: 0) { List { ForEach($script.blocks) { $block in BlockRow(block: $block) @@ -15,14 +15,21 @@ struct BlockScriptEditorView: View { .onDelete { script.blocks.remove(atOffsets: $0) } .onMove { script.blocks.move(fromOffsets: $0, toOffset: $1) } } + .listStyle(.plain) .toolbar { EditButton() } + .frame(maxWidth: .infinity, maxHeight: .infinity) + + Divider() + HStack { Button("Cancel") { dismiss() } + .buttonStyle(.bordered) Spacer() Button("Save") { saveScript() dismiss() } + .buttonStyle(.borderedProminent) } .padding() } @@ -64,7 +71,12 @@ struct BlockRow: View { } } .pickerStyle(.menu) - TextField("Value", text: $block.value) + if !block.type.placeholder.isEmpty { + TextField(block.type.placeholder, text: $block.value) + } else { + Text(block.type.rawValue) + .foregroundStyle(.secondary) + } } } } From bcfd65150afba7aaee1865f86399bd3be84f74e8 Mon Sep 17 00:00:00 2001 From: Stephen B <158498287+StephenDev0@users.noreply.github.com> Date: Thu, 17 Jul 2025 23:46:49 -0400 Subject: [PATCH 3/7] Add quick value options to block editor --- README.md | 1 + StikJIT/JSSupport/BlockScript.swift | 15 +++++++++++++++ StikJIT/JSSupport/BlockScriptEditorView.swift | 9 +++++++++ 3 files changed, 25 insertions(+) diff --git a/README.md b/README.md index 722829f9..6009ee9a 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ - Seamless integration with our custom built loopback vpn. - Native UI for managing debugging/JIT-enabling. - Optional block-based script editor for building scripts using a Scratch-like interface. +- Block editor provides quick-insert options for common values. - No data collection—ensuring full privacy. ## License diff --git a/StikJIT/JSSupport/BlockScript.swift b/StikJIT/JSSupport/BlockScript.swift index 84efe98d..4de565f1 100644 --- a/StikJIT/JSSupport/BlockScript.swift +++ b/StikJIT/JSSupport/BlockScript.swift @@ -21,6 +21,21 @@ enum BlockType: String, Codable, CaseIterable, Identifiable { return "" } } + + /// Suggested values that can be inserted when editing a block of this type. + /// These are just examples to help users build scripts quickly. + var options: [String] { + switch self { + case .sendCommand: + return ["vAttach;${pid}", "D", "c"] + case .log: + return ["Hello", "Done"] + case .prepareMemoryRegion: + return ["0x0,0x1000", "0x100000000,0x2000"] + case .getPid, .hasTXM: + return [] + } + } } struct Block: Codable, Identifiable { diff --git a/StikJIT/JSSupport/BlockScriptEditorView.swift b/StikJIT/JSSupport/BlockScriptEditorView.swift index 9c208a16..27e5814d 100644 --- a/StikJIT/JSSupport/BlockScriptEditorView.swift +++ b/StikJIT/JSSupport/BlockScriptEditorView.swift @@ -73,6 +73,15 @@ struct BlockRow: View { .pickerStyle(.menu) if !block.type.placeholder.isEmpty { TextField(block.type.placeholder, text: $block.value) + if !block.type.options.isEmpty { + Menu { + ForEach(block.type.options, id: \\.self) { option in + Button(option) { block.value = option } + } + } label: { + Image(systemName: "ellipsis.circle") + } + } } else { Text(block.type.rawValue) .foregroundStyle(.secondary) From eaa666b8c69bc8c95929db93261c5556a1277954 Mon Sep 17 00:00:00 2001 From: Stephen B <158498287+StephenDev0@users.noreply.github.com> Date: Thu, 17 Jul 2025 23:46:54 -0400 Subject: [PATCH 4/7] Label block editor toggle as beta --- StikJIT/Views/SettingsView.swift | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/StikJIT/Views/SettingsView.swift b/StikJIT/Views/SettingsView.swift index 0f079144..1f0c4968 100644 --- a/StikJIT/Views/SettingsView.swift +++ b/StikJIT/Views/SettingsView.swift @@ -285,9 +285,21 @@ struct SettingsView: View { Toggle("Picture in Picture", isOn: $enablePiP) .foregroundColor(.primary) .padding(.vertical, 6) - Toggle("Use Block Script Editor", isOn: $useBlockEditor) - .foregroundColor(.primary) - .padding(.vertical, 6) + Toggle(isOn: $useBlockEditor) { + HStack(spacing: 6) { + Text("Use Block Script Editor") + Text("Beta") + .font(.caption2) + .fontWeight(.semibold) + .padding(.horizontal, 4) + .padding(.vertical, 2) + .foregroundColor(.orange) + .background(Color.orange.opacity(0.2)) + .cornerRadius(4) + } + } + .foregroundColor(.primary) + .padding(.vertical, 6) } } .padding(.vertical, 20) From 7478922eff449bbac2036c0b9f49af31225d8fd3 Mon Sep 17 00:00:00 2001 From: Stephen B <158498287+StephenDev0@users.noreply.github.com> Date: Thu, 17 Jul 2025 23:53:26 -0400 Subject: [PATCH 5/7] Fix block editor menu --- StikJIT/JSSupport/BlockScriptEditorView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/StikJIT/JSSupport/BlockScriptEditorView.swift b/StikJIT/JSSupport/BlockScriptEditorView.swift index 27e5814d..16ae19a2 100644 --- a/StikJIT/JSSupport/BlockScriptEditorView.swift +++ b/StikJIT/JSSupport/BlockScriptEditorView.swift @@ -75,7 +75,7 @@ struct BlockRow: View { TextField(block.type.placeholder, text: $block.value) if !block.type.options.isEmpty { Menu { - ForEach(block.type.options, id: \\.self) { option in + ForEach(block.type.options, id: \.self) { option in Button(option) { block.value = option } } } label: { From 3bd85a4b31c8d6eb27e80551d0d29a8017886c0b Mon Sep 17 00:00:00 2001 From: Stephen B <158498287+StephenDev0@users.noreply.github.com> Date: Fri, 18 Jul 2025 19:57:10 -0400 Subject: [PATCH 6/7] Default to jsb when block editor enabled --- StikJIT/JSSupport/ScriptListView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/StikJIT/JSSupport/ScriptListView.swift b/StikJIT/JSSupport/ScriptListView.swift index d73e9f52..09ff5141 100644 --- a/StikJIT/JSSupport/ScriptListView.swift +++ b/StikJIT/JSSupport/ScriptListView.swift @@ -82,6 +82,7 @@ struct ScriptListView: View { .toolbar { ToolbarItem(placement: .primaryAction) { Button { + newFileName = useBlockEditor ? "script.jsb" : "script.js" showNewFileAlert = true } label: { Label("New Script", systemImage: "plus") @@ -97,7 +98,7 @@ struct ScriptListView: View { } .onAppear(perform: loadScripts) .alert("New Script", isPresented: $showNewFileAlert) { - TextField("Filename", text: $newFileName) + TextField(useBlockEditor ? "Filename.jsb" : "Filename.js", text: $newFileName) Button("Create", action: createNewScript) Button("Cancel", role: .cancel) { } } From 08695ca1f7b0b7cc14f1cdf38e89b6734a9944fd Mon Sep 17 00:00:00 2001 From: Stephen B <158498287+StephenDev0@users.noreply.github.com> Date: Fri, 18 Jul 2025 19:57:15 -0400 Subject: [PATCH 7/7] Style block editor like Scratch --- README.md | 2 +- StikJIT/JSSupport/BlockScript.swift | 16 ++++++++++++++++ StikJIT/JSSupport/BlockScriptEditorView.swift | 11 +++++++++-- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6009ee9a..066fbe3f 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ - Seamless integration with our custom built loopback vpn. - Native UI for managing debugging/JIT-enabling. - Optional block-based script editor for building scripts using a Scratch-like interface. -- Block editor provides quick-insert options for common values. +- Block editor provides quick-insert options for common values and color-coded blocks for a Scratch-like feel. - No data collection—ensuring full privacy. ## License diff --git a/StikJIT/JSSupport/BlockScript.swift b/StikJIT/JSSupport/BlockScript.swift index 4de565f1..96206d2b 100644 --- a/StikJIT/JSSupport/BlockScript.swift +++ b/StikJIT/JSSupport/BlockScript.swift @@ -1,4 +1,5 @@ import Foundation +import SwiftUI enum BlockType: String, Codable, CaseIterable, Identifiable { case sendCommand @@ -36,6 +37,21 @@ enum BlockType: String, Codable, CaseIterable, Identifiable { return [] } } + + /// Color used for displaying the block in the editor so it looks + /// similar to a Scratch block category. + var color: Color { + switch self { + case .sendCommand: + return .blue + case .log: + return .green + case .getPid, .hasTXM: + return .orange + case .prepareMemoryRegion: + return .purple + } + } } struct Block: Codable, Identifiable { diff --git a/StikJIT/JSSupport/BlockScriptEditorView.swift b/StikJIT/JSSupport/BlockScriptEditorView.swift index 16ae19a2..3b019616 100644 --- a/StikJIT/JSSupport/BlockScriptEditorView.swift +++ b/StikJIT/JSSupport/BlockScriptEditorView.swift @@ -16,6 +16,7 @@ struct BlockScriptEditorView: View { .onMove { script.blocks.move(fromOffsets: $0, toOffset: $1) } } .listStyle(.plain) + .scrollContentBackground(.hidden) .toolbar { EditButton() } .frame(maxWidth: .infinity, maxHeight: .infinity) @@ -64,22 +65,25 @@ struct BlockRow: View { @Binding var block: Block var body: some View { - HStack { + HStack(spacing: 8) { Picker("Type", selection: $block.type) { ForEach(BlockType.allCases) { type in Text(type.rawValue).tag(type) } } .pickerStyle(.menu) + if !block.type.placeholder.isEmpty { TextField(block.type.placeholder, text: $block.value) + .textFieldStyle(.roundedBorder) + if !block.type.options.isEmpty { Menu { ForEach(block.type.options, id: \.self) { option in Button(option) { block.value = option } } } label: { - Image(systemName: "ellipsis.circle") + Image(systemName: "plus.circle") } } } else { @@ -87,5 +91,8 @@ struct BlockRow: View { .foregroundStyle(.secondary) } } + .padding(8) + .background(block.type.color.opacity(0.2)) + .clipShape(RoundedRectangle(cornerRadius: 8)) } }