mirror of
https://github.com/narrrl/narl-workspace.git
synced 2026-11-03 16:41:27 +01:00
yank clipboard + ? fix
This commit is contained in:
1 parent
888e2bf7fe
commit
515a82dfc2
5 files changed
+89
-12
No files matched your search
+23
-7
@@ -18,6 +18,7 @@
|
||||
let cmdline = $state<string | null>(null)
|
||||
let fileInput = $state<HTMLInputElement>()
|
||||
let shell = $state<HTMLDivElement>()
|
||||
let overlayEl = $state<HTMLDivElement>()
|
||||
let pending = $state('')
|
||||
|
||||
if (!shareToken) void diary.boot()
|
||||
@@ -32,6 +33,17 @@
|
||||
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.
|
||||
$effect(() => {
|
||||
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 (event.ctrlKey || event.metaKey || event.altKey) return
|
||||
|
||||
@@ -88,11 +108,6 @@
|
||||
const previous = pending
|
||||
pending = ''
|
||||
|
||||
if (diary.overlay !== 'none') {
|
||||
if (key === 'q' || key === '?') diary.overlay = 'none'
|
||||
return
|
||||
}
|
||||
|
||||
// Two-key sequences: gg and dd.
|
||||
if (previous === 'g' && key === 'g') {
|
||||
event.preventDefault()
|
||||
@@ -216,8 +231,9 @@
|
||||
{/if}
|
||||
|
||||
{#if diary.overlay !== 'none'}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
||||
<div class="overlay" onclick={(e) => e.target === e.currentTarget && (diary.overlay = 'none')}>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions, a11y_no_noninteractive_tabindex -->
|
||||
<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}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -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">
|
||||
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands'
|
||||
import { markdown, markdownLanguage } from '@codemirror/lang-markdown'
|
||||
@@ -53,12 +58,24 @@
|
||||
borderColor: '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': {
|
||||
fontFamily: 'inherit',
|
||||
fontSize: 'inherit',
|
||||
lineHeight: 'inherit',
|
||||
color: 'var(--accent)',
|
||||
caretColor: 'var(--accent)',
|
||||
flex: '1',
|
||||
},
|
||||
},
|
||||
{ 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
|
||||
* file never cuts a sentence in half.
|
||||
@@ -108,6 +153,8 @@
|
||||
|
||||
onMount(() => {
|
||||
bridgeExCommands()
|
||||
bindHelpKey()
|
||||
mirrorUnnamedRegisterToClipboard()
|
||||
|
||||
const extensions = [
|
||||
...(diary.vimEnabled ? [vim({ status: false })] : []),
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
['x · dd', 'delete the entry (asks first)'],
|
||||
[':', 'command line'],
|
||||
['Ctrl-S', 'save, from anywhere'],
|
||||
['?', 'this screen'],
|
||||
['?', 'this screen — in the editor too'],
|
||||
]
|
||||
|
||||
const titles: Record<Group, string> = {
|
||||
@@ -75,7 +75,9 @@
|
||||
<footer class="faint">
|
||||
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
|
||||
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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -373,11 +373,15 @@ export const commands: CommandSpec[] = [
|
||||
name: 'set',
|
||||
args: '<option>',
|
||||
group: 'view',
|
||||
help: 'theme=mocha|green|amber|ice, vim, novim',
|
||||
help: 'theme=mocha|green|amber|ice, vim, novim, clipboard, noclipboard',
|
||||
run: (arg) => {
|
||||
const option = arg.trim()
|
||||
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 === '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]
|
||||
if (theme) return setTheme(theme)
|
||||
diary.say(`unknown option: ${option}`, 'error')
|
||||
|
||||
@@ -34,6 +34,9 @@ class Diary {
|
||||
|
||||
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 {
|
||||
return this.entries[this.cursor]
|
||||
}
|
||||
@@ -186,6 +189,11 @@ class Diary {
|
||||
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. */
|
||||
async guard(action: () => Promise<unknown>) {
|
||||
try {
|
||||
|
||||
Reference in new issue
Block a user