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
7 changes: 4 additions & 3 deletions config/app-profiles.conf
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
# grab_strategy:
# copy safe Ctrl+Insert probe, then copy_fallback
# primary-first use a primary selection proven to belong to this focus epoch,
# otherwise safe copy probe + copy_fallback
# otherwise use copy_fallback directly (no generic safe probe)
# primary-only require an affine primary selection; never inject Copy
#
# Use "-" to disable an unsafe/unknown app-specific fallback. Grab uses the
# safe Ctrl+Insert probe first and may then use copy_fallback. Paste is different:
# Use "-" to disable an unsafe/unknown app-specific fallback. The ordinary
# `copy` strategy may use the generic Ctrl+Insert probe before copy_fallback;
# `primary-first` skips that probe and uses copy_fallback directly. Paste is different:
# ClipReg injects exactly ONE accelerator so it can never double-paste. A "-"
# paste profile selects safe_paste_chord (normally Shift+Insert) when primary
# staging is available. Unknown applications never receive an unclassified Ctrl+C.
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ CopyQ is optional history integration. It is not a correctness or persistence ba

## Application-copy boundary

Wayland does not provide a universal semantic API for "serialize the current arbitrary selection." Rich capture therefore uses the active application's Copy behavior. A safe Copy accelerator is attempted where appropriate; application profiles control fallbacks such as terminal `Ctrl+Shift+C`.
Wayland does not provide a universal semantic API for "serialize the current arbitrary selection." Rich capture therefore uses the active application's Copy behavior. A generic Copy probe is attempted only where the application profile declares the ordinary `copy` strategy. `primary-first` terminal profiles use an affine primary selection when available and otherwise go directly to their explicit profile Copy accelerator such as terminal `Ctrl+Shift+C`; they do not receive the generic probe first.

Primary selection is an explicit alternate capture source, not a silent fallback for a failed application Copy.

Expand Down
67 changes: 57 additions & 10 deletions src/clipreg.c
Original file line number Diff line number Diff line change
Expand Up @@ -1339,6 +1339,44 @@ static int capture_primary_if_affine(struct daemon_state*d,struct item*out){
return snapshot_current(d,SEL_PRIMARY,out);
}

struct grab_copy_plan {
const char *first_chord;
const char *fallback_chord;
int first_timeout_ms;
};

static struct grab_copy_plan grab_copy_plan_for_profile(const struct daemon_state *d,
const struct app_profile *p) {
struct grab_copy_plan plan = {0};
if (!d || !p) return plan;

if (!strcmp(p->grab_strategy, "primary-only")) return plan;

if (!strcmp(p->grab_strategy, "primary-first")) {
if (strcmp(p->copy_chord, "-")) {
plan.first_chord = p->copy_chord;
plan.first_timeout_ms = d->cfg.copy_timeout_ms;
}
return plan;
}

if (strcmp(d->cfg.safe_copy_chord, "-")) {
plan.first_chord = d->cfg.safe_copy_chord;
plan.first_timeout_ms = d->cfg.safe_probe_timeout_ms;
}

if (strcmp(p->copy_chord, "-") &&
(!plan.first_chord || strcasecmp(plan.first_chord, p->copy_chord))) {
if (!plan.first_chord) {
plan.first_chord = p->copy_chord;
plan.first_timeout_ms = d->cfg.copy_timeout_ms;
} else {
plan.fallback_chord = p->copy_chord;
}
}
return plan;
}

static int restore_if_still_ours(struct daemon_state *d, enum sel_kind kind,
const struct item *original, bool *preserved_newer);

Expand Down Expand Up @@ -1413,16 +1451,25 @@ static int command_grab(struct daemon_state *d, const char *reg, uint64_t receiv
}

uint64_t focus_generation = d->active_app_generation;
uint64_t before = d->clipboard_generation;
operation_phase(d, "grab-safe-copy");
r = inject_chord(d, d->cfg.safe_copy_chord);
if (r == 0) r = wait_external_generation(d, SEL_CLIPBOARD, before, d->cfg.safe_probe_timeout_ms);

if (r < 0 && d->active_app_generation == focus_generation && strcmp(p->copy_chord, "-")) {
operation_phase(d, "grab-profile-copy");
before = d->clipboard_generation;
r = inject_chord(d, p->copy_chord);
if (r == 0) r = wait_external_generation(d, SEL_CLIPBOARD, before, d->cfg.copy_timeout_ms);
struct grab_copy_plan copy_plan = grab_copy_plan_for_profile(d, p);
if (!copy_plan.first_chord) {
r = -ENOTSUP;
} else {
uint64_t before = d->clipboard_generation;
operation_phase(d, !strcasecmp(copy_plan.first_chord, d->cfg.safe_copy_chord)
? "grab-safe-copy" : "grab-profile-copy");
r = inject_chord(d, copy_plan.first_chord);
if (r == 0)
r = wait_external_generation(d, SEL_CLIPBOARD, before, copy_plan.first_timeout_ms);

if (r < 0 && copy_plan.fallback_chord &&
d->active_app_generation == focus_generation) {
operation_phase(d, "grab-profile-copy");
before = d->clipboard_generation;
r = inject_chord(d, copy_plan.fallback_chord);
if (r == 0)
r = wait_external_generation(d, SEL_CLIPBOARD, before, d->cfg.copy_timeout_ms);
}
}

if (r < 0 || d->active_app_generation != focus_generation) {
Expand Down
30 changes: 30 additions & 0 deletions tests/test-runtime.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,37 @@ static int fail(const char *message) {
return 1;
}

static int test_grab_copy_policy(void) {
struct daemon_state d = {0};
default_config(&d.cfg);

struct app_profile p = {0};
snprintf(p.copy_chord, sizeof(p.copy_chord), "CTRL+SHIFT+C");

snprintf(p.grab_strategy, sizeof(p.grab_strategy), "primary-first");
struct grab_copy_plan plan = grab_copy_plan_for_profile(&d, &p);
if (!plan.first_chord || strcmp(plan.first_chord, "CTRL+SHIFT+C") ||
plan.fallback_chord != NULL || plan.first_timeout_ms != d.cfg.copy_timeout_ms)
return fail("primary-first routed through the generic Copy probe");

snprintf(p.grab_strategy, sizeof(p.grab_strategy), "copy");
plan = grab_copy_plan_for_profile(&d, &p);
if (!plan.first_chord || strcmp(plan.first_chord, d.cfg.safe_copy_chord) ||
!plan.fallback_chord || strcmp(plan.fallback_chord, "CTRL+SHIFT+C") ||
plan.first_timeout_ms != d.cfg.safe_probe_timeout_ms)
return fail("ordinary copy strategy lost safe-probe then fallback ordering");

snprintf(p.grab_strategy, sizeof(p.grab_strategy), "primary-only");
plan = grab_copy_plan_for_profile(&d, &p);
if (plan.first_chord || plan.fallback_chord)
return fail("primary-only unexpectedly planned injected Copy input");

return 0;
}

int main(void) {
if (test_grab_copy_policy() != 0) return 1;

struct daemon_state d = {0};
default_config(&d.cfg);
d.display = (struct wl_display *)(uintptr_t)1;
Expand Down