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 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}
|
||||||
|
|||||||
@@ -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 })] : []),
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in new issue
Block a user