Files
thoughtsync/frontend/src/views/CaptureView.vue
T
bvandeusenandClaude Opus 5 6c0153be1e
Android / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / Build now, or wait for Android? (push) Successful in 3s
CI & Build / Python lint (push) Successful in 4s
Desktop (Tauri) / Build, or is the channel already serving this? (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 7s
CI & Build / Python tests (push) Successful in 13s
CI & Build / integration (push) Successful in 29s
CI & Build / Build & push image (push) Skipped
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 1m5s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 1m52s
Desktop (Tauri) / Update manifest (push) Skipped
Android / Kotlin + Rust (APK) (push) Successful in 7m22s
desktop: a global hotkey opens a small window to write in, now with its lockfile
Restores 42e06da, which was reverted only because Cargo.lock had not been
updated for the new crate and every cargo invocation in CI passes `--locked`.
Both desktop jobs failed on that line before compiling anything, so nothing
about the code had been judged.

The lockfile was generated in CI's own `ci-tauri:1.97` image — one container,
`cargo fetch`, nothing built. `cargo fetch` and NOT `generate-lockfile`: the
latter re-resolves from scratch and would have churned versions across the
whole workspace to add one dependency. The diff is 67 insertions, zero
deletions, six packages — tauri-plugin-global-shortcut plus global-hotkey,
x11rb, x11rb-protocol, xkeysym and gethostname. Nothing existing moved.

The feature itself, unchanged from 42e06da:

Press the combination anywhere and a small window arrives over whatever you
were doing; type, Ctrl/Cmd+Enter, gone. The board never comes forward.

There is no default shortcut on purpose — any default is a key combination
taken away from something else on somebody's machine, silently, at install
time. CommandOrControl+Shift+N is offered as a one-click suggestion.

Stored and live are separate fields because they disagree: a combination
another app holds is saved and does nothing when pressed, and a Wayland
compositor may refuse global grabs outright. `capture_shortcut_set` registers
before storing, so a refused combination is never written down as if it worked.

The window hides rather than closes and keeps its text, so an interrupted
capture is still there next press — which is what makes Escape safe. A failed
save keeps it open too, rather than discarding the only copy of something just
written in order to report a retryable problem.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3MMqUtzX1TJgA1oypvm1c
2026-09-01 18:05:20 -04:00

88 lines
3.2 KiB
Vue

<script setup lang="ts">
// The quick-capture window: one field, and two ways out.
//
// This runs in a SECOND Tauri window, summoned by a global hotkey over whatever
// the person was doing. Everything here is shaped by that: no shell, no nav, no
// board — a window that arrives uninvited has to be finishable in one gesture and
// leave nothing behind if it isn't.
import { nextTick, onMounted, ref } from "vue";
import { repo } from "../adapters";
import { capture } from "../desktop/bridge";
const body = ref("");
const field = ref<HTMLTextAreaElement | null>(null);
const saving = ref(false);
const error = ref("");
onMounted(async () => {
// Focused on arrival, and after a save. The whole feature is "press the keys and
// start typing" — a window that needs a click first has not saved anyone a step.
await nextTick();
field.value?.focus();
});
async function save() {
const content = body.value.trim();
// Nothing typed is not an error, it is a change of mind — the same reading the
// board takes of tapping + and walking away.
if (!content) {
void capture.done(false);
return;
}
saving.value = true;
error.value = "";
try {
await repo.notes.create({ body: content });
body.value = "";
await capture.done(true);
} catch {
// The window STAYS OPEN on failure, holding the text. Hiding it would throw
// away the only copy of something the person just wrote, to report a problem
// they could otherwise retry their way out of.
error.value = "Couldn't save that. Your text is still here — try again.";
} finally {
saving.value = false;
}
}
function dismiss() {
// The text is deliberately KEPT. The window is hidden rather than destroyed, so
// a capture interrupted by something more urgent is still there on the next
// press — which is the behaviour that makes it safe to press Escape.
void capture.done(false);
}
</script>
<template>
<div
class="flex h-screen w-screen flex-col gap-2 bg-neutral-50 p-3 text-neutral-900 dark:bg-neutral-950 dark:text-neutral-100"
>
<textarea
ref="field"
v-model="body"
class="min-h-0 flex-1 resize-none rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-neutral-700 dark:bg-neutral-900"
placeholder="Write it down…"
aria-label="New note"
@keydown.esc.prevent="dismiss"
@keydown.enter.ctrl.prevent="save"
@keydown.enter.meta.prevent="save"
/>
<p v-if="error" class="text-xs text-red-600 dark:text-red-400">{{ error }}</p>
<div class="flex items-center justify-between gap-3">
<!-- The shortcuts are written down rather than assumed: this window is seen
rarely and briefly, and it is the only place they are discoverable. -->
<p class="text-xs text-neutral-400">
<kbd>Ctrl</kbd>/<kbd></kbd> + <kbd>Enter</kbd> to save · <kbd>Esc</kbd> to dismiss
</p>
<div class="flex shrink-0 items-center gap-2">
<button type="button" class="btn btn-ghost" @click="dismiss">Cancel</button>
<button type="button" class="btn btn-primary" :disabled="saving" @click="save">
{{ saving ? "Saving…" : "Save" }}
</button>
</div>
</div>
</div>
</template>