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
59 changes: 59 additions & 0 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Claude Code Review

on:
pull_request:
types: [opened, synchronize]

jobs:
claude-review:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
id-token: write

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Detect workflow changes in PR
id: workflow-changes
run: |
set -euo pipefail
git fetch origin "${{ github.base_ref }}" --depth=1
if git diff --quiet "origin/${{ github.base_ref }}" -- .github/workflows/claude-code-review.yml; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
fi

- name: Run Claude Code Review
id: claude-review
if: steps.workflow-changes.outputs.changed != 'true'
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_args: |
--allowed-tools Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*),Bash(gh issue view:*),Bash(gh issue list:*),Bash(gh search:*),Read,Glob,Grep
prompt: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}

Please review this pull request and provide feedback on:
- Code quality and Rust best practices
- Potential bugs or issues
- Performance considerations (this is a hot-path keyboard remapping engine)
- API design and backward compatibility
- Test coverage

Be constructive and helpful in your feedback.

Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.

- name: Skip Claude review for workflow-file changes
if: steps.workflow-changes.outputs.changed == 'true'
run: |
echo "Skipping Claude review because .github/workflows/claude-code-review.yml changed in this PR."
120 changes: 120 additions & 0 deletions keyberon/src/action/switch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const INPUT_VAL: u16 = 851;
const HISTORICAL_INPUT_VAL: u16 = 852;
const LAYER_VAL: u16 = 853;
const BASE_LAYER_VAL: u16 = 854;
const DEVICE_VAL: u16 = 855;

// Binary values:
// 0b0100 ...
Expand Down Expand Up @@ -87,6 +88,7 @@ enum OpCodeType {
TicksSinceGreaterThan(TicksSinceNthKey),
Layer(u16),
BaseLayer(u16),
Device(u16),
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -131,6 +133,7 @@ impl<'a, T> Switch<'a, T> {
/// the currently active keys, and historically pressed keys.
///
/// The `historical_keys` parameter should iterate in the order of most-recent-first.
#[allow(clippy::too_many_arguments)]
pub fn actions<A1, A2, H1, H2, L>(
&self,
active_keys: A1,
Expand All @@ -139,6 +142,7 @@ impl<'a, T> Switch<'a, T> {
historical_positions: H2,
layers: L,
default_layer: u16,
current_device: Option<u8>,
) -> SwitchActions<'a, T, A1, A2, H1, H2, L>
where
A1: Iterator<Item = KeyCode> + Clone,
Expand All @@ -155,6 +159,7 @@ impl<'a, T> Switch<'a, T> {
historical_positions,
layers,
default_layer,
current_device,
case_index: 0,
}
}
Expand All @@ -177,6 +182,7 @@ where
historical_positions: H2,
layers: L,
default_layer: u16,
current_device: Option<u8>,
case_index: usize,
}

Expand All @@ -201,6 +207,7 @@ where
self.historical_positions.clone(),
self.layers.clone(),
self.default_layer,
self.current_device,
) {
let ret_ac = case.1;
match case.2 {
Expand Down Expand Up @@ -315,6 +322,12 @@ impl OpCode {
(Self(BASE_LAYER_VAL), Self(base_layer))
}

/// Return OpCodes specifying a device index check.
pub fn new_device(device_idx: u16) -> (Self, Self) {
assert!(device_idx <= u8::MAX as u16);
(Self(DEVICE_VAL), Self(device_idx))
}

/// Return the interpretation of this `OpCode`.
fn opcode_type(self, next: Option<OpCode>) -> OpCodeType {
if self.0 < KEY_MAX {
Expand All @@ -329,6 +342,7 @@ impl OpCode {
}),
LAYER_VAL => OpCodeType::Layer(op2.0),
BASE_LAYER_VAL => OpCodeType::BaseLayer(op2.0),
DEVICE_VAL => OpCodeType::Device(op2.0),
_ => unreachable!("unexpected opcode {self:?}"),
}
} else {
Expand Down Expand Up @@ -366,6 +380,7 @@ impl From<u16> for OperatorAndEndIndex {
}

/// Evaluate the return value of an expression evaluated on the given key codes.
#[allow(clippy::too_many_arguments)]
fn evaluate_boolean(
bool_expr: &[OpCode],
key_codes: impl Iterator<Item = KeyCode> + Clone,
Expand All @@ -374,6 +389,7 @@ fn evaluate_boolean(
historical_inputs: impl Iterator<Item = HistoricalEvent<KCoord>> + Clone,
layers: impl Iterator<Item = u16> + Clone,
default_layer: u16,
current_device: Option<u8>,
) -> bool {
let mut ret = true;
let mut current_index = 0;
Expand Down Expand Up @@ -465,6 +481,11 @@ fn evaluate_boolean(
current_index += 1;
ret = default_layer == base_layer;
}
OpCodeType::Device(device_idx) => {
// opcode has size 2
current_index += 1;
ret = current_device == Some(device_idx as u8);
}
};
if current_op == Not {
ret = !ret;
Expand Down Expand Up @@ -493,6 +514,7 @@ fn evaluate_bool_test(opcodes: &[OpCode], keycodes: impl Iterator<Item = KeyCode
[].iter().copied(),
[].iter().copied(),
0,
None,
)
}

Expand Down Expand Up @@ -752,6 +774,7 @@ fn switch_fallthrough() {
[].iter().copied(),
[].iter().copied(),
0,
None,
);
assert_eq!(actions.next(), Some(&Action::<()>::KeyCode(KeyCode::A)));
assert_eq!(actions.next(), Some(&Action::<()>::KeyCode(KeyCode::B)));
Expand All @@ -773,6 +796,7 @@ fn switch_break() {
[].iter().copied(),
[].iter().copied(),
0,
None,
);
assert_eq!(actions.next(), Some(&Action::<()>::KeyCode(KeyCode::A)));
assert_eq!(actions.next(), None);
Expand Down Expand Up @@ -801,6 +825,7 @@ fn switch_no_actions() {
[].iter().copied(),
[].iter().copied(),
0,
None,
);
assert_eq!(actions.next(), None);
}
Expand Down Expand Up @@ -861,6 +886,7 @@ fn switch_historical_1() {
[].iter().copied(),
[].iter().copied(),
0,
None,
));
assert!(evaluate_boolean(
opcode_true2.as_slice(),
Expand All @@ -870,6 +896,7 @@ fn switch_historical_1() {
[].iter().copied(),
[].iter().copied(),
0,
None,
));
assert!(!evaluate_boolean(
opcode_false.as_slice(),
Expand All @@ -879,6 +906,7 @@ fn switch_historical_1() {
[].iter().copied(),
[].iter().copied(),
0,
None,
));
assert!(!evaluate_boolean(
opcode_false2.as_slice(),
Expand All @@ -888,6 +916,7 @@ fn switch_historical_1() {
[].iter().copied(),
[].iter().copied(),
0,
None,
));
}

Expand Down Expand Up @@ -973,6 +1002,7 @@ fn switch_historical_bools() {
[].iter().copied(),
[].iter().copied(),
0,
None,
),
expectation
);
Expand Down Expand Up @@ -1089,6 +1119,7 @@ fn switch_historical_ticks_since() {
[].iter().copied(),
[].iter().copied(),
0,
None,
),
expectation
);
Expand Down Expand Up @@ -1323,6 +1354,7 @@ fn switch_inputs() {
[].iter().copied(),
[].iter().copied(),
0,
None,
),
expectation
);
Expand Down Expand Up @@ -1391,6 +1423,7 @@ fn switch_historical_inputs() {
historical_inputs.iter().copied(),
[].iter().copied(),
0,
None,
),
expectation
);
Expand All @@ -1402,3 +1435,90 @@ fn switch_historical_inputs() {
test(&opcodes_true_or1, true);
test(&opcodes_true_or2, true);
}

#[test]
fn device_opcode_encoding_roundtrip() {
let (op1, op2) = OpCode::new_device(0);
assert_eq!(op1.opcode_type(Some(op2)), OpCodeType::Device(0));
let (op1, op2) = OpCode::new_device(255);
assert_eq!(op1.opcode_type(Some(op2)), OpCodeType::Device(255));
let (op1, op2) = OpCode::new_device(42);
assert_eq!(op1.opcode_type(Some(op2)), OpCodeType::Device(42));
}

#[test]
fn device_evaluate_boolean_matches() {
let (op1, op2) = OpCode::new_device(0);
let opcodes = [op1, op2];
assert!(evaluate_boolean(
&opcodes,
[].iter().copied(),
[].iter().copied(),
[].iter().copied(),
[].iter().copied(),
[].iter().copied(),
0,
Some(0),
));
assert!(!evaluate_boolean(
&opcodes,
[].iter().copied(),
[].iter().copied(),
[].iter().copied(),
[].iter().copied(),
[].iter().copied(),
0,
Some(1),
));
assert!(!evaluate_boolean(
&opcodes,
[].iter().copied(),
[].iter().copied(),
[].iter().copied(),
[].iter().copied(),
[].iter().copied(),
0,
None,
));
}

#[test]
fn device_evaluate_boolean_with_and() {
let (d1, d2) = OpCode::new_device(1);
let opcodes = [
OpCode::new_bool(And, 4),
OpCode::new_key(KeyCode::A),
d1,
d2,
];
assert!(evaluate_boolean(
&opcodes,
[KeyCode::A].iter().copied(),
[].iter().copied(),
[].iter().copied(),
[].iter().copied(),
[].iter().copied(),
0,
Some(1),
));
assert!(!evaluate_boolean(
&opcodes,
[KeyCode::A].iter().copied(),
[].iter().copied(),
[].iter().copied(),
[].iter().copied(),
[].iter().copied(),
0,
Some(0),
));
assert!(!evaluate_boolean(
&opcodes,
[KeyCode::B].iter().copied(),
[].iter().copied(),
[].iter().copied(),
[].iter().copied(),
[].iter().copied(),
0,
Some(1),
));
}
5 changes: 5 additions & 0 deletions keyberon/src/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ where
/// Only stores data when the `tap_hold_tracker` feature is enabled;
/// otherwise this is a zero-sized no-op.
pub tap_hold_tracker: crate::tap_hold_tracker::TapHoldTracker,
/// The device index of the most recently processed input event.
/// Used by switch `(device N)` conditions.
pub current_device: Option<u8>,
}

pub use crate::tap_hold_tracker::{HoldActivatedInfo, TapActivatedInfo};
Expand Down Expand Up @@ -1183,6 +1186,7 @@ impl<'a, const C: usize, const R: usize, T: 'a + Copy + std::fmt::Debug> Layout<
chords_v2: None,
contextual_execution: ContextualExecution::new(),
tap_hold_tracker: Default::default(),
current_device: None,
}
}
pub fn new_with_trans_action_settings(
Expand Down Expand Up @@ -2218,6 +2222,7 @@ impl<'a, const C: usize, const R: usize, T: 'a + Copy + std::fmt::Debug> Layout<
// Note on truncating cast: I expect default layer to be in range by other
// assertions.
self.default_layer as u16,
self.current_device,
) {
action_queue.push_back(Some((coord, 0, ac, layer_stack.collect())));
}
Expand Down
Loading
Loading