yank clipboard + ? fix

This commit is contained in:
nvrl committed 2026-09-05 08:13:18 +02:00
1 parent 888e2bf7fe
commit 515a82dfc2
5 files changed
+89 -12

No files matched your search

+23 -7
View File
@@ -18,6 +18,7 @@
let cmdline = $state<string | null>(null) let cmdline = $state<string | null>(null)
let fileInput = $state<HTMLInputElement>() let fileInput = $state<HTMLInputElement>()
let shell = $state<HTMLDivElement>() let shell = $state<HTMLDivElement>()
let overlayEl = $state<HTMLDivElement>()
let pending = $state('') let pending = $state('')
if (!shareToken) void diary.boot() if (!shareToken) void diary.boot()
@@ -32,6 +33,17 @@
hooks.openCommandLine = (initial) => (cmdline = initial) hooks.openCommandLine = (initial) => (cmdline = initial)
}) })
/*
* The overlay takes focus while it is up, so its keys (q, ?, esc) reach the
* window handler instead of being swallowed by the editor underneath.
*/
$effect(() => {
if (diary.overlay !== 'none') {
overlayEl?.focus()
return () => (diary.editing ? hooks.focusEditor() : shell?.focus())
}
})
// Guard against losing a half-written entry to a stray refresh. // Guard against losing a half-written entry to a stray refresh.
$effect(() => { $effect(() => {
if (!diary.dirty) return if (!diary.dirty) return
@@ -81,6 +93,14 @@
} }
} }
if (diary.overlay !== 'none') {
if (event.key === 'q' || event.key === '?') {
event.preventDefault()
diary.overlay = 'none'
}
return
}
if (cmdline !== null || isTypingTarget(event.target)) return if (cmdline !== null || isTypingTarget(event.target)) return
if (event.ctrlKey || event.metaKey || event.altKey) return if (event.ctrlKey || event.metaKey || event.altKey) return
@@ -88,11 +108,6 @@
const previous = pending const previous = pending
pending = '' pending = ''
if (diary.overlay !== 'none') {
if (key === 'q' || key === '?') diary.overlay = 'none'
return
}
// Two-key sequences: gg and dd. // Two-key sequences: gg and dd.
if (previous === 'g' && key === 'g') { if (previous === 'g' && key === 'g') {
event.preventDefault() event.preventDefault()
@@ -216,8 +231,9 @@
{/if} {/if}
{#if diary.overlay !== 'none'} {#if diary.overlay !== 'none'}
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions, a11y_no_noninteractive_tabindex -->
<div class="overlay" onclick={(e) => e.target === e.currentTarget && (diary.overlay = 'none')}> <div class="overlay" tabindex="-1" bind:this={overlayEl} onclick={(e) => e.target === e.currentTarget && (diary.overlay = 'none')}
>
{#if diary.overlay === 'help'}<Help />{:else}<MediaBrowser />{/if} {#if diary.overlay === 'help'}<Help />{:else}<MediaBrowser />{/if}
</div> </div>
{/if} {/if}
+49 -2
View File
@@ -1,3 +1,8 @@
<script lang="ts" module>
/* The vim register controller is global, so it is wrapped once per page. */
let clipboardMirrored = false
</script>
<script lang="ts"> <script lang="ts">
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands' import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands'
import { markdown, markdownLanguage } from '@codemirror/lang-markdown' import { markdown, markdownLanguage } from '@codemirror/lang-markdown'
@@ -53,12 +58,24 @@
borderColor: 'var(--line)', borderColor: 'var(--line)',
}, },
'.cm-panels-bottom': { borderTop: '1px solid var(--line)' }, '.cm-panels-bottom': { borderTop: '1px solid var(--line)' },
// The `:` prompt is a bare text node in the panel; the input follows it. /*
'.cm-vim-panel': { padding: '2px 8px', color: 'var(--accent)' }, * The `:` prompt is a bare text node in the panel; the input follows it.
* The input is an inline-block whose baseline sits below the text node's,
* so the two are laid out as flex items instead of aligned by baseline.
*/
'.cm-vim-panel': {
padding: '2px 8px',
color: 'var(--accent)',
display: 'flex',
alignItems: 'center',
},
'.cm-vim-panel input': { '.cm-vim-panel input': {
fontFamily: 'inherit', fontFamily: 'inherit',
fontSize: 'inherit',
lineHeight: 'inherit',
color: 'var(--accent)', color: 'var(--accent)',
caretColor: 'var(--accent)', caretColor: 'var(--accent)',
flex: '1',
}, },
}, },
{ dark: true }, { dark: true },
@@ -80,6 +97,34 @@
} }
} }
/**
* `set clipboard=unnamed`, in effect: every yank or delete that lands in the
* unnamed register is copied out to the system clipboard too. An explicit
* register (`"ay`) is left alone, and `"+p` still pastes the clipboard back.
*/
function mirrorUnnamedRegisterToClipboard() {
if (clipboardMirrored) return
const registers = Vim.getRegisterController?.()
if (!registers) return
clipboardMirrored = true
const push = registers.pushText.bind(registers)
registers.pushText = (name, operator, text, linewise, blockwise) => {
push(name, operator, text, linewise, blockwise)
if (!name && text && diary.clipboard) void diary.copy(text)
}
}
/**
* `?` is vim's search-backwards, but the diary advertises it as the help key
* everywhere else, so it opens the reference here too. `/` still searches.
*/
function bindHelpKey() {
Vim.defineAction('diaryHelp', () => void runCommand('help'))
for (const context of ['normal', 'visual'] as const) {
Vim.mapCommand('?', 'action', 'diaryHelp', {}, { context })
}
}
/** /**
* Media goes on its own line after the one the cursor is on, so attaching a * Media goes on its own line after the one the cursor is on, so attaching a
* file never cuts a sentence in half. * file never cuts a sentence in half.
@@ -108,6 +153,8 @@
onMount(() => { onMount(() => {
bridgeExCommands() bridgeExCommands()
bindHelpKey()
mirrorUnnamedRegisterToClipboard()
const extensions = [ const extensions = [
...(diary.vimEnabled ? [vim({ status: false })] : []), ...(diary.vimEnabled ? [vim({ status: false })] : []),
+4 -2
View File
@@ -16,7 +16,7 @@
['x · dd', 'delete the entry (asks first)'], ['x · dd', 'delete the entry (asks first)'],
[':', 'command line'], [':', 'command line'],
['Ctrl-S', 'save, from anywhere'], ['Ctrl-S', 'save, from anywhere'],
['?', 'this screen'], ['?', 'this screen — in the editor too'],
] ]
const titles: Record<Group, string> = { const titles: Record<Group, string> = {
@@ -75,7 +75,9 @@
<footer class="faint"> <footer class="faint">
inside the editor the full vim keymap is live — hjkl, w/b, dd, ciw, visual mode, macros, and inside the editor the full vim keymap is live — hjkl, w/b, dd, ciw, visual mode, macros, and
<span class="accent">:w</span> to write. drop or paste an image, video or audio file into the <span class="accent">:w</span> to write. drop or paste an image, video or audio file into the
editor to attach it. <span class="accent">:12</span> jumps straight to entry 12. editor to attach it. <span class="accent">:12</span> jumps straight to entry 12. yanks and
deletes are copied to the system clipboard —
<span class="accent">:set noclipboard</span> keeps them in vim's registers.
</footer> </footer>
</div> </div>
+5 -1
View File
@@ -373,11 +373,15 @@ export const commands: CommandSpec[] = [
name: 'set', name: 'set',
args: '<option>', args: '<option>',
group: 'view', group: 'view',
help: 'theme=mocha|green|amber|ice, vim, novim', help: 'theme=mocha|green|amber|ice, vim, novim, clipboard, noclipboard',
run: (arg) => { run: (arg) => {
const option = arg.trim() const option = arg.trim()
if (option === 'vim') return diary.setVim(true), diary.say('vim keys on') if (option === 'vim') return diary.setVim(true), diary.say('vim keys on')
if (option === 'novim') return diary.setVim(false), diary.say('vim keys off') if (option === 'novim') return diary.setVim(false), diary.say('vim keys off')
if (option === 'clipboard')
return diary.setClipboard(true), diary.say('yanks go to the system clipboard')
if (option === 'noclipboard')
return diary.setClipboard(false), diary.say('yanks stay in vim registers')
const theme = /^theme=(\w+)$/.exec(option)?.[1] const theme = /^theme=(\w+)$/.exec(option)?.[1]
if (theme) return setTheme(theme) if (theme) return setTheme(theme)
diary.say(`unknown option: ${option}`, 'error') diary.say(`unknown option: ${option}`, 'error')
+8
View File
@@ -34,6 +34,9 @@ class Diary {
vimEnabled = $state(localStorage.getItem('diary:vim') !== 'off') vimEnabled = $state(localStorage.getItem('diary:vim') !== 'off')
/** Mirror vim yanks and deletes into the system clipboard, like `set clipboard=unnamed`. */
clipboard = $state(localStorage.getItem('diary:clipboard') !== 'off')
get selected(): EntrySummary | undefined { get selected(): EntrySummary | undefined {
return this.entries[this.cursor] return this.entries[this.cursor]
} }
@@ -186,6 +189,11 @@ class Diary {
localStorage.setItem('diary:vim', enabled ? 'on' : 'off') localStorage.setItem('diary:vim', enabled ? 'on' : 'off')
} }
setClipboard(enabled: boolean) {
this.clipboard = enabled
localStorage.setItem('diary:clipboard', enabled ? 'on' : 'off')
}
/** Surface a failed API call in the status line instead of the console. */ /** Surface a failed API call in the status line instead of the console. */
async guard(action: () => Promise<unknown>) { async guard(action: () => Promise<unknown>) {
try { try {