Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ecdae11
vcli-input: register workspace member + add core-graphics deps
blaksmatic Apr 17, 2026
8d42a26
vcli-input: empty crate shell with README
blaksmatic Apr 17, 2026
8756808
vcli-input: InputError (thiserror) + ErrorCode mapping
blaksmatic Apr 17, 2026
ddf2983
vcli-input: KillSwitch primitive (Arc<AtomicBool> + observer)
blaksmatic Apr 17, 2026
caaaaa2
vcli-input: InputSink trait + DragSegment
blaksmatic Apr 17, 2026
d7d870e
vcli-input: MockInputSink with recording log + forced-error hook
blaksmatic Apr 17, 2026
fb54713
vcli-input: integration test — kill switch halts mock mid-sequence
blaksmatic Apr 17, 2026
cd536f1
vcli-input: canonical key parser + macOS virtual keycode table
blaksmatic Apr 17, 2026
652e7b4
vcli-input: PermissionReport + macOS TCC probes + Windows stub scaffo…
blaksmatic Apr 17, 2026
47b4a6b
vcli-input: macOS CGEvent helpers (move/click/drag/key/scroll)
blaksmatic Apr 17, 2026
b1ae90a
vcli-input: macOS Unicode typing helper (CGEventKeyboardSetUnicodeStr…
blaksmatic Apr 17, 2026
c281cfd
vcli-input: CGEventInputSink wires CGEvent helpers into InputSink
blaksmatic Apr 17, 2026
2456440
vcli-input: macOS CGEventTap listener for Cmd+Shift+Esc kill switch c…
blaksmatic Apr 17, 2026
88ccaf5
vcli-input: mock-vs-trait contract tests
blaksmatic Apr 17, 2026
0b80fdb
vcli-input: #[ignore] CGEvent integration smoke tests
blaksmatic Apr 17, 2026
f0b18e1
vcli-input: full-crate verification — fmt + clippy -D warnings + doc …
blaksmatic Apr 17, 2026
88047dc
Merge remote-tracking branch 'origin/master' into implement/plan-2-vc…
blaksmatic Apr 17, 2026
d75c009
Merge remote-tracking branch 'origin/master' into implement/plan-2-vc…
blaksmatic Apr 17, 2026
ae91711
Merge remote-tracking branch 'origin/master' into implement/plan-2-vc…
blaksmatic Apr 17, 2026
a036f1e
ci: rerun after stuck macOS runner
blaksmatic Apr 17, 2026
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
82 changes: 82 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ members = [
"crates/vcli-core",
"crates/vcli-capture",
"crates/vcli-dsl",
"crates/vcli-input",
"crates/vcli-ipc",
"crates/vcli-perception",
"crates/vcli-store",
Expand Down Expand Up @@ -37,8 +38,10 @@ hex = "0.4"

tracing = "0.1"

# Platform-specific, referenced from vcli-capture only. Declaring here so
# downstream crates that later depend on vcli-capture don't re-select versions.
# Platform-specific (used by vcli-input + vcli-capture)
core-graphics = { version = "0.24", features = ["highsierra"] }
core-foundation = "0.10"
libc = "0.2"
screencapturekit = "1"

# Added by vcli-perception lane
Expand Down
24 changes: 24 additions & 0 deletions crates/vcli-input/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "vcli-input"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
authors.workspace = true
description = "Input synthesis trait, macOS CGEvent backend, mock, and HIL kill switch for vcli."

[dependencies]
vcli-core = { path = "../vcli-core" }
thiserror = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }

[target.'cfg(target_os = "macos")'.dependencies]
core-graphics = { workspace = true }
core-foundation = { workspace = true }
libc = { workspace = true }

[dev-dependencies]
tempfile = { workspace = true }
proptest = { workspace = true }
5 changes: 5 additions & 0 deletions crates/vcli-input/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# vcli-input

Input synthesis. Real macOS CGEvent backend, a mock, and a human-in-the-loop kill switch.

See the design spec at `docs/superpowers/specs/2026-04-16-vcli-design.md` §Input synthesis.
86 changes: 86 additions & 0 deletions crates/vcli-input/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//! Errors surfaced by [`crate::sink::InputSink`] implementations.

use thiserror::Error;
use vcli_core::ErrorCode;

/// Error returned by every `InputSink` method. `Halted` wins over all others
/// because the kill switch short-circuits before the backend is even called.
#[derive(Debug, Error)]
pub enum InputError {
/// The kill switch was set; no OS event was posted.
#[error("input halted: kill switch is engaged")]
Halted,

/// macOS TCC (Accessibility / Input Monitoring) has not granted this process
/// permission to synthesize input.
#[error("input permission denied: {detail}")]
PermissionDenied {
/// Short human-readable reason — which TCC bucket is missing.
detail: String,
},

/// Event creation / posting failed at the OS layer.
#[error("backend failure: {detail}")]
Backend {
/// Error message from the OS / FFI call.
detail: String,
},

/// Unknown key name in a key-combo action.
#[error("unknown key: {0}")]
UnknownKey(String),

/// Invalid argument (e.g. empty drag path, `hold_ms > 60_000`).
#[error("invalid input argument: {0}")]
InvalidArgument(String),

/// Platform does not support this method (Windows stub in v0).
#[error("not implemented on this platform")]
Unimplemented,
}

impl InputError {
/// Map to the IPC wire-level [`ErrorCode`]. `Halted` maps to `internal`
/// because it is a local-only condition with no external counterpart.
#[must_use]
pub fn to_error_code(&self) -> ErrorCode {
match self {
Self::Halted | Self::Backend { .. } | Self::Unimplemented => ErrorCode::Internal,
Self::PermissionDenied { .. } => ErrorCode::PermissionDenied,
Self::UnknownKey(_) | Self::InvalidArgument(_) => ErrorCode::InvalidProgram,
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn halted_display_is_stable() {
assert_eq!(
InputError::Halted.to_string(),
"input halted: kill switch is engaged"
);
}

#[test]
fn permission_maps_to_permission_denied() {
let e = InputError::PermissionDenied {
detail: "Accessibility".into(),
};
assert_eq!(e.to_error_code(), ErrorCode::PermissionDenied);
}

#[test]
fn unknown_key_maps_to_invalid_program() {
let e = InputError::UnknownKey("blargh".into());
assert_eq!(e.to_error_code(), ErrorCode::InvalidProgram);
assert!(e.to_string().contains("blargh"));
}

#[test]
fn halted_maps_to_internal() {
assert_eq!(InputError::Halted.to_error_code(), ErrorCode::Internal);
}
}
Loading
Loading