Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@
## 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.
- 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
StikDebug is licensed under **AGPL-3.0**. See [`LICENSE`](LICENSE) for details.
Expand Down
104 changes: 104 additions & 0 deletions StikJIT/JSSupport/BlockScript.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import Foundation
import SwiftUI

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 ""
}
}

/// 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 []
}
}

/// 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 {
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)\")"
case .getPid:
return "get_pid()"
case .hasTXM:
return "hasTXM()"
case .prepareMemoryRegion:
return "prepare_memory_region(\(block.value))"
}
}.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)
}
}
98 changes: 98 additions & 0 deletions StikJIT/JSSupport/BlockScriptEditorView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
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(spacing: 0) {
List {
ForEach($script.blocks) { $block in
BlockRow(block: $block)
}
.onDelete { script.blocks.remove(atOffsets: $0) }
.onMove { script.blocks.move(fromOffsets: $0, toOffset: $1) }
}
.listStyle(.plain)
.scrollContentBackground(.hidden)
.toolbar { EditButton() }
.frame(maxWidth: .infinity, maxHeight: .infinity)

Divider()

HStack {
Button("Cancel") { dismiss() }
.buttonStyle(.bordered)
Spacer()
Button("Save") {
saveScript()
dismiss()
}
.buttonStyle(.borderedProminent)
}
.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(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: "plus.circle")
}
}
} else {
Text(block.type.rawValue)
.foregroundStyle(.secondary)
}
}
.padding(8)
.background(block.type.color.opacity(0.2))
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
41 changes: 34 additions & 7 deletions StikJIT/JSSupport/ScriptListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@

import SwiftUI
import UniformTypeIdentifiers
import Foundation

struct ScriptListView: View {
@State private var scripts: [URL] = []
@State private var showNewFileAlert = false
@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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -76,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")
Expand All @@ -91,13 +98,16 @@ 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) { }
}
.fileImporter(
isPresented: $showImporter,
allowedContentTypes: [UTType(filenameExtension: "js") ?? .plainText],
allowedContentTypes: [
UTType(filenameExtension: "js") ?? .plainText,
UTType(filenameExtension: "jsb") ?? .plainText
],
allowsMultipleSelection: false
) { result in
switch result {
Expand Down Expand Up @@ -158,7 +168,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) {
Expand All @@ -168,8 +178,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)

Expand All @@ -181,7 +197,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 {
Expand All @@ -192,6 +215,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")
}
Expand Down
12 changes: 9 additions & 3 deletions StikJIT/Views/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import SwiftUI
import UniformTypeIdentifiers
import Pipify
import Foundation

struct JITEnableConfiguration {
var bundleID: String? = nil
Expand Down Expand Up @@ -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 {
Expand Down
21 changes: 19 additions & 2 deletions StikJIT/Views/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -284,6 +285,21 @@ struct SettingsView: View {
Toggle("Picture in Picture", isOn: $enablePiP)
.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)
Expand All @@ -292,9 +308,10 @@ struct SettingsView: View {
if !newValue {
useDefaultScript = false
enablePiP = false
useBlockEditor = false
}
}
}
}
}

// About section
SettingsCard {
Expand Down