Skip to content
Merged
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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ files should use the SPDX identifier `GPL-3.0-or-later` where practical.
file format and OpenMacro Workspace is the user's versioned workspace.
- Macros must be human-readable, AI-editable, versionable, portable,
explainable, and locally executable.
- Keep the primary editor MacroDroid-simple: Triggers, Conditions, and Actions,
with an always-available code view backed by the same validated model.
- The trust model is: **AI proposes. Schema validates. App explains. User
approves. Engine runs. Logs prove.**
- Runtime behavior must remain deterministic. Do not place an AI black box in
Expand Down
24 changes: 24 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Third-party notices

ZeroBit uses the following third-party software:

## SnakeYAML Engine

- Project: <https://bitbucket.org/snakeyaml/snakeyaml-engine>
- Copyright: SnakeYAML contributors
- License: Apache License 2.0
- License text: <https://www.apache.org/licenses/LICENSE-2.0>

SnakeYAML Engine is used to parse the restricted YAML 1.2 syntax accepted by
OpenMacro. ZeroBit adds its own validation and rejects YAML features outside
that subset.

## kotlinx.coroutines

- Project: <https://github.com/Kotlin/kotlinx.coroutines>
- Copyright: JetBrains and Kotlin contributors
- License: Apache License 2.0
- License text: <https://www.apache.org/licenses/LICENSE-2.0>

kotlinx.coroutines provides cancellable background parsing for the OpenMacro
source editor without blocking Android's main UI thread.
5 changes: 4 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ dependencies {
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.snakeyaml.engine)

testImplementation(libs.junit)

debugImplementation(libs.androidx.compose.ui.tooling)
}

3 changes: 2 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
Expand All @@ -20,4 +22,3 @@
</application>

</manifest>

135 changes: 42 additions & 93 deletions app/src/main/java/com/vibhor1102/zerobit/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,111 +3,60 @@ package com.vibhor1102.zerobit
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import com.vibhor1102.zerobit.openmacro.capability.CapabilityRegistry
import com.vibhor1102.zerobit.openmacro.proposal.OpenMacroProposalPipeline
import com.vibhor1102.zerobit.ui.editor.MacroEditorScreen
import com.vibhor1102.zerobit.ui.editor.MacroEditorSession
import com.vibhor1102.zerobit.ui.editor.SampleMacro
import com.vibhor1102.zerobit.ui.theme.ZeroBitTheme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ZeroBitTheme {
ZeroBitHome()
}
}
}
}

@Composable
private fun ZeroBitHome() {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background,
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
contentAlignment = Alignment.Center,
) {
Card(
modifier = Modifier.widthIn(max = 520.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
),
shape = RoundedCornerShape(28.dp),
) {
Column(
modifier = Modifier.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Image(
painter = painterResource(R.drawable.zerobit_mark),
contentDescription = null,
modifier = Modifier.size(128.dp),
contentScale = ContentScale.Fit,
)

Spacer(Modifier.size(24.dp))

Text(
text = "ZeroBit",
style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold,
)

Spacer(Modifier.size(12.dp))

Text(
text = "Transparent automation for Android,\nbuilt for humans and AI.",
style = MaterialTheme.typography.titleMedium,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)

Spacer(Modifier.size(24.dp))

Text(
text = "AI proposes. You approve. ZeroBit runs it locally.",
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Medium,
val pipeline = remember {
OpenMacroProposalPipeline(CapabilityRegistry.builtIn())
}
val editor = remember {
MacroEditorSession.withInitialSourceApproved(
pipeline = pipeline,
initialSource = SampleMacro.source,
)
}
val session = editor.first
var state by remember { mutableStateOf(editor.second) }

LaunchedEffect(state.sourceText) {
val baseState = state
val sourceToParse = state.sourceText
delay(SOURCE_PARSE_DEBOUNCE_MILLIS)
val parsed = withContext(Dispatchers.Default) {
session.updateSource(baseState, sourceToParse)
}
if (state.sourceText == sourceToParse) {
state = parsed.copy(mode = state.mode)
}
}

MacroEditorScreen(
state = state,
onModeSelected = { state = session.selectMode(state, it) },
onSourceChanged = { state = state.copy(sourceText = it) },
)
}
}
}
}

@Preview(showBackground = true)
@Composable
private fun ZeroBitHomePreview() {
ZeroBitTheme {
ZeroBitHome()
private companion object {
const val SOURCE_PARSE_DEBOUNCE_MILLIS = 250L
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
*/

package com.vibhor1102.zerobit.openmacro.capability

import com.vibhor1102.zerobit.openmacro.model.MacroBlock
import com.vibhor1102.zerobit.openmacro.runtime.RuntimeStep
import com.vibhor1102.zerobit.openmacro.validation.ValidationIssue

/**
* One source of truth for a block's code shape, generated form, explanation,
* permission discovery, and runtime instruction.
*/
interface CapabilityDefinition {
val type: String
val lane: CapabilityLane
val displayName: String
val description: String
val fields: List<CapabilityField>

fun validate(block: MacroBlock, path: String): List<ValidationIssue>

fun explain(block: MacroBlock): String

fun requiredPermissions(block: MacroBlock): Set<AndroidPermission>

fun compile(block: MacroBlock): RuntimeStep
}

enum class CapabilityLane {
TRIGGER,
CONDITION,
ACTION,
}

data class CapabilityField(
val key: String,
val label: String,
val kind: CapabilityFieldKind,
val required: Boolean,
val help: String,
val advanced: Boolean = false,
)

enum class CapabilityFieldKind {
TEXT,
MULTILINE_TEXT,
NUMBER,
BOOLEAN,
}

enum class AndroidPermission(
val manifestName: String,
) {
POST_NOTIFICATIONS("android.permission.POST_NOTIFICATIONS"),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
*/

package com.vibhor1102.zerobit.openmacro.capability

import com.vibhor1102.zerobit.openmacro.capability.builtin.DeviceUnlockedCondition
import com.vibhor1102.zerobit.openmacro.capability.builtin.NotificationShowAction
import com.vibhor1102.zerobit.openmacro.capability.builtin.PowerConnectedTrigger

class CapabilityRegistry private constructor(
definitions: List<CapabilityDefinition>,
) {
private val definitionsByType = definitions.associateBy { it.type }

init {
require(definitionsByType.size == definitions.size) {
"Capability types must be unique."
}
}

fun find(type: String): CapabilityDefinition? = definitionsByType[type]

fun list(lane: CapabilityLane): List<CapabilityDefinition> =
definitionsByType.values
.filter { it.lane == lane }
.sortedBy { it.displayName }

companion object {
fun builtIn(): CapabilityRegistry = CapabilityRegistry(
definitions = listOf(
PowerConnectedTrigger,
DeviceUnlockedCondition,
NotificationShowAction,
),
)

fun of(vararg definitions: CapabilityDefinition): CapabilityRegistry =
CapabilityRegistry(definitions.toList())
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
*/

package com.vibhor1102.zerobit.openmacro.capability

import com.vibhor1102.zerobit.openmacro.model.MacroBlock
import com.vibhor1102.zerobit.openmacro.model.MacroValue
import com.vibhor1102.zerobit.openmacro.validation.ValidationIssue

internal fun MacroBlock.rejectUnknownConfig(
allowedKeys: Set<String>,
path: String,
): List<ValidationIssue> = config.keys
.filterNot(allowedKeys::contains)
.sorted()
.map { key ->
ValidationIssue(
path = "$path.config.$key",
code = "unknown_config",
message = "Configuration '$key' is not supported by '$type'.",
)
}

internal fun MacroBlock.requireText(
key: String,
path: String,
maxLength: Int,
): List<ValidationIssue> {
val value = config[key]
return when {
value == null -> listOf(
ValidationIssue(
path = "$path.config.$key",
code = "missing_config",
message = "Configuration '$key' is required.",
),
)

value !is MacroValue.Text -> listOf(
ValidationIssue(
path = "$path.config.$key",
code = "wrong_config_type",
message = "Configuration '$key' must be text.",
),
)

value.value.isBlank() -> listOf(
ValidationIssue(
path = "$path.config.$key",
code = "blank_config",
message = "Configuration '$key' must not be blank.",
),
)

value.value.length > maxLength -> listOf(
ValidationIssue(
path = "$path.config.$key",
code = "config_too_long",
message = "Configuration '$key' must be $maxLength characters or fewer.",
),
)

else -> emptyList()
}
}

internal fun MacroBlock.text(key: String): String =
(config.getValue(key) as MacroValue.Text).value
Loading