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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ scribe-tap
*.gcno
*.gcov
.DS_Store

AGENTS.override.md

AGENTS.md
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<!-- Generated by scripts/render-agents. Edit /realm/project/scribe-tap/CLAUDE.md and included files instead. -->

# Repository Guidelines

## Project Structure & Module Organization
Expand All @@ -20,4 +22,4 @@ Extend `tests/test_basic.py` when adding behaviours; each scenario should isolat
Recent history mixes plain imperative subjects (`Add data-dir flag`) with optional Conventional Commit prefixes (`feat:`). Whichever you choose, keep the subject under 72 characters and describe the user-visible impact in the body when needed. Pull requests should link issues, outline configuration changes, and include `make`/`make check` results (terminal snippets beat screenshots). Mention any flags or environment requirements reviewers must set.

## Environment & Tooling Notes
Hyprland context detection depends on libxkbcommon plus clipboard helpers; verify they are on PATH or rely on `nix develop`. Default log destinations point at `/realm/data/keylog`, so override with `--data-dir` or `--log-dir` during local testing. When embedding inside interception pipelines, keep stdin/stdout unbuffered and use `--context none` for headless runs to avoid compositor calls.
Hyprland context detection depends on libxkbcommon plus clipboard helpers; verify they are on PATH or rely on `nix develop`. Default log destinations point at `/realm/data/captures/keylog`, so override with `--data-dir` or `--log-dir` during local testing. When embedding inside interception pipelines, keep stdin/stdout unbuffered and use `--context none` for headless runs to avoid compositor calls.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ scribe-tap [--data-dir DIR] [--log-dir DIR] [--snapshot-dir DIR] [--snapshot-int
[--hypr-signature PATH] [--hypr-user USER]
```

- `--data-dir` – root directory for artefacts (defaults to `/realm/data/keylog`, creating `logs/` and `snapshots/` automatically).
- `--data-dir` – root directory for artefacts (defaults to `/realm/data/captures/keylog`, creating `logs/` and `snapshots/` automatically).
- `--log-dir` – directory for JSONL log files (`$data_dir/logs` by default).
- `--snapshot-dir` – directory for live snapshots (`$data_dir/snapshots`).
- `--snapshot-interval` – write snapshot at most once per window per interval (seconds).
Expand All @@ -109,10 +109,10 @@ Use the included replay helper to inspect logs (`scribe-tap-replay` when install

```sh
# latest snapshots and tail events
python3 tools/replay.py --log-dir /realm/data/keylog/logs --snapshot-dir /realm/data/keylog/snapshots --mode both --window messenger --events-tail 10 --show-clipboard
python3 tools/replay.py --log-dir /realm/data/captures/keylog/logs --snapshot-dir /realm/data/captures/keylog/snapshots --mode both --window messenger --events-tail 10 --show-clipboard

# interactive picker
python3 tools/replay.py --snapshot-dir /realm/data/keylog/snapshots --interactive --session 20251003T001711
python3 tools/replay.py --snapshot-dir /realm/data/captures/keylog/snapshots --interactive --session 20251003T001711
```

## License
Expand Down
12 changes: 10 additions & 2 deletions src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -248,9 +248,12 @@ static void print_usage(const char *prog) {
}

int main(int argc, char **argv) {
const char *data_dir = "/realm/data/keylog";
const char *data_dir = "/realm/data/captures/keylog";
const char *log_dir = NULL;
const char *snapshot_dir = NULL;
bool data_dir_explicit = false;
bool log_dir_explicit = false;
bool snapshot_dir_explicit = false;
const char *hyprctl_cmd = "hyprctl";
double snapshot_interval = 5.0;
double context_refresh = 0.4;
Expand All @@ -269,10 +272,13 @@ int main(int argc, char **argv) {
for (int i = 1; i < argc; ++i) {
if (strcmp(argv[i], "--log-dir") == 0 && i + 1 < argc) {
log_dir = argv[++i];
log_dir_explicit = true;
} else if (strcmp(argv[i], "--snapshot-dir") == 0 && i + 1 < argc) {
snapshot_dir = argv[++i];
snapshot_dir_explicit = true;
} else if (strcmp(argv[i], "--data-dir") == 0 && i + 1 < argc) {
data_dir = argv[++i];
data_dir_explicit = true;
} else if (strcmp(argv[i], "--snapshot-interval") == 0 && i + 1 < argc) {
snapshot_interval = atof(argv[++i]);
} else if (strcmp(argv[i], "--context-refresh") == 0 && i + 1 < argc) {
Expand Down Expand Up @@ -356,7 +362,9 @@ int main(int argc, char **argv) {
snapshot_dir = snapshot_dir_buf;
}

util_ensure_dir_tree(data_dir);
if (data_dir_explicit || !log_dir_explicit || !snapshot_dir_explicit) {
util_ensure_dir_tree(data_dir);
}
util_ensure_dir_tree(log_dir);
util_ensure_dir_tree(snapshot_dir);

Expand Down
19 changes: 16 additions & 3 deletions src/state.c
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,8 @@ int state_poll_timeout_ms(const State *state) {
return (int)interval_ms;
}

static char lowercase_char_for_key(int code); /* forward decl for keycode_name */

static const char *keycode_name(int code) {
static char buf[32];
switch (code) {
Expand All @@ -371,9 +373,15 @@ static const char *keycode_name(int code) {
default:
break;
}
if (code >= KEY_A && code <= KEY_Z) {
snprintf(buf, sizeof(buf), "KEY_%c", 'A' + (code - KEY_A));
return buf;
/* Use lowercase_char_for_key() to get the correct letter — evdev keycodes
are NOT contiguous A-Z (they follow keyboard rows: Q=16..P=25, A=30..L=38, Z=44..M=50).
The old code assumed contiguous codes and mislabeled most letters. */
{
char letter = lowercase_char_for_key(code);
if (letter >= 'a' && letter <= 'z') {
snprintf(buf, sizeof(buf), "KEY_%c", letter - 32); /* uppercase */
return buf;
}
}
if (code >= KEY_0 && code <= KEY_9) {
snprintf(buf, sizeof(buf), "KEY_%c", '0' + (code - KEY_0));
Expand Down Expand Up @@ -865,6 +873,11 @@ static void process_key(State *state, int code, const char *key_name, const char
buffer_append(buf, clipboard, strlen(clipboard));
changed = true;
}
} else if (state->modifiers[MOD_CTRL] || state->modifiers[MOD_ALT]) {
/* Skip buffer append when Ctrl or Alt is held — xkbcommon returns
control codes (0x01-0x1f) that corrupt the buffer. The press event
still logs the keycode for reconstruction from raw events. */
Comment on lines +876 to +879
break;
Comment on lines +876 to +880
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Allow AltGr text entry in buffer updates

The new state->modifiers[MOD_ALT] guard skips all text appends whenever Alt is pressed, which regresses input on international layouts where Right Alt (AltGr) is required to produce normal characters (for example @, , or locale-specific letters). In those sessions, keypresses are still logged but snapshots/buffer reconstruction lose actual typed text, so the captured output becomes incomplete for common user workflows outside US layouts.

Useful? React with 👍 / 👎.

Comment on lines +876 to +880
} else {
if (utf8_text && *utf8_text) {
buffer_append(buf, utf8_text, strlen(utf8_text));
Expand Down
4 changes: 2 additions & 2 deletions tools/replay.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ def load_events(log_path: Path) -> Iterable[dict]:

def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--log-dir", type=Path, default=Path("/realm/data/keylog/logs"))
parser.add_argument("--snapshot-dir", type=Path, default=Path("/realm/data/keylog/snapshots"))
parser.add_argument("--log-dir", type=Path, default=Path("/realm/data/captures/keylog/logs"))
parser.add_argument("--snapshot-dir", type=Path, default=Path("/realm/data/captures/keylog/snapshots"))
parser.add_argument(
"--date",
type=str,
Expand Down
Loading