Skip to content
Open
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
4 changes: 4 additions & 0 deletions apps/desktop/src/main/app-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ describe('TOML serialization', () => {
themeFamily: 'nord',
themeMode: 'dark',
showWindowTitleBar: false,
showStatusBar: false,
autoPairs: false,
autoPairQuotesInProse: true,
showHeadingLevelLabels: true,
Expand Down Expand Up @@ -151,6 +152,9 @@ describe('TOML serialization', () => {
expect(round.themeFamily).toBe('nord')
expect(text).toContain('show_window_title_bar = false')
expect(round.showWindowTitleBar).toBe(false)
// Piotr's status bar switch travels as [appearance].show_status_bar.
expect(text).toMatch(/\[appearance\][^[]*show_status_bar = false/)
expect(round.showStatusBar).toBe(false)
expect(round.autoPairs).toBe(false)
expect(round.autoPairQuotesInProse).toBe(true)
expect(round.showHeadingLevelLabels).toBe(true)
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/main/app-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,11 @@ const SCALAR_FIELDS: Partial<Record<PortablePrefKey, ScalarFieldMap>> = {
tomlKey: 'show_window_title_bar',
comment: 'show the main window title bar and window controls'
},
showStatusBar: {
section: 'appearance',
tomlKey: 'show_status_bar',
comment: 'show the bar along the bottom; while hidden it still appears when Cloud needs you'
},
showSidebarChevrons: {
section: 'appearance',
tomlKey: 'show_sidebar_chevrons',
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions packages/app-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,8 @@ import { installEditorHost, revealEditorCaret } from '@zennotes/app-core/editor'

const host = installEditorHost({
nativeTyping: true,
// A phone layout reads what a search finds; the keyboard waits for a tap.
openNotesToRead: () => isPhoneLayout(),
measureBottomInsets: ({ editor, scroll }) => ({
// These geometry helpers and overlay elements belong to the native shell.
layout: bottomOverlap(editor, selectionToolbarBounds()),
Expand All @@ -340,6 +342,23 @@ and writing suggestions through the editor's content attributes. The native
keyboard decides which features to provide. It does not install keyboard plugins
or change the host's spelling capabilities.

`openNotesToRead` is asked each time a pick in one of core's searches lands
(the note search, Search in all notes, the outline, the buffer switcher), and
each time a command-palette command that only goes to a note already there
finishes (Go Back, Go Forward, Switch to Previous Note, the tab commands, and
today's daily, weekly or monthly note when it exists). When it returns true the
note opens without taking the keyboard focus, so a soft keyboard stays down
until the user taps into the text. Commands whose point is typing (a new note,
Format Markdown, Reflow Paragraph) keep focusing the editor; every other
command (a toggle, a view, a theme), and closing any of those palettes or the
template or drawing palette without a pick, leaves the focus where it was.
Absent, false or throwing, the editor takes the focus as on desktop.

Live preview reveals a line's Markdown source (a heading's `#`, `**`, `[[ ]]`)
only on lines the user is editing: once an editor has taken the focus since its
note opened. A note opened to be read renders every line, the caret's too, with
no host configuration.

The measurement callback receives frozen copies of editor and scroll-viewport
bounds (`top`, `bottom`, `left`, `right`, `width`, `height`) in CSS pixels. It receives
no DOM element or CodeMirror object. Read host geometry there; do not mutate layout
Expand Down
2 changes: 1 addition & 1 deletion packages/app-core/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zennotes/app-core",
"private": true,
"version": "2.78.0",
"version": "2.78.1",
"type": "module",
"exports": {
"./main": "./src/main.tsx",
Expand Down
32 changes: 23 additions & 9 deletions packages/app-core/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ import {
} from './lib/panel-rows'
import { requestPaneMode } from './lib/pane-mode'
import { recordRendererPerf } from './lib/perf'
import { focusEditorNormalMode } from './lib/editor-focus'
import {
focusEditorAfterPaletteClose,
focusEditorAfterReadOpen,
focusEditorNormalMode
} from './lib/editor-focus'
import { isAppOverlayOpen, isOverlayOrDialogOpen } from './lib/overlay-open'
import { installMarkdownFileDropHandler } from './lib/markdown-file-drop'
import {
Expand Down Expand Up @@ -482,7 +486,15 @@ function App(): JSX.Element {
// pane jump or mouse click. Routed through a DOM event to keep the store free
// of an editor-focus import cycle. (#353)
useEffect(() => {
const handler = (): void => focusEditorNormalMode({ attempts: 10, delayMs: 24 })
// A request marked `read` (a periodic note that already existed, opened
// from the command palette) lands the way a search pick does: focused on
// desktop, left to read on a phone.
const handler = (event: Event): void => {
const options = { attempts: 10, delayMs: 24 }
const detail = (event as CustomEvent<{ read?: boolean } | null>).detail
if (detail?.read) focusEditorAfterReadOpen(options)
else focusEditorNormalMode(options)
}
window.addEventListener('zen:focus-editor', handler)
return () => window.removeEventListener('zen:focus-editor', handler)
}, [])
Expand Down Expand Up @@ -955,39 +967,41 @@ function App(): JSX.Element {
void state.reopenLastClosedTab()
return
}
// A palette closed here hands back the focus the way its own Escape and
// Cancel do: on a phone, that leaves the keyboard where it was.
if (e.key === 'Escape' && state.searchOpen) {
setSearchOpen(false)
focusEditorNormalMode()
focusEditorAfterPaletteClose()
return
}
if (e.key === 'Escape' && state.vaultTextSearchOpen) {
setVaultTextSearchOpen(false)
focusEditorNormalMode()
focusEditorAfterPaletteClose()
return
}
if (e.key === 'Escape' && state.commandPaletteOpen) {
setCommandPaletteOpen(false)
focusEditorNormalMode()
focusEditorAfterPaletteClose()
return
}
if (e.key === 'Escape' && state.bufferPaletteOpen) {
setBufferPaletteOpen(false)
focusEditorNormalMode()
focusEditorAfterPaletteClose()
return
}
if (e.key === 'Escape' && state.templatePaletteOpen) {
setTemplatePaletteOpen(false)
focusEditorNormalMode()
focusEditorAfterPaletteClose()
return
}
if (e.key === 'Escape' && state.embedDrawingPaletteOpen) {
setEmbedDrawingPaletteOpen(false)
focusEditorNormalMode()
focusEditorAfterPaletteClose()
return
}
if (e.key === 'Escape' && state.outlinePaletteOpen) {
setOutlinePaletteOpen(false)
focusEditorNormalMode()
focusEditorAfterPaletteClose()
return
}
// ⌘1 — toggle sidebar
Expand Down
6 changes: 3 additions & 3 deletions packages/app-core/src/components/BufferPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { isArchiveTabPath } from '@shared/archive'
import { isTrashTabPath } from '@shared/trash'
import { isQuickNotesTabPath } from '@shared/quick-notes'
import { resolveSystemFolderLabels, type SystemFolderLabels } from '../lib/system-folder-labels'
import { focusEditorNormalMode } from '../lib/editor-focus'
import { focusEditorAfterReadOpen, focusEditorAfterPaletteClose } from '../lib/editor-focus'
import { noteFolderLabel } from '../lib/vault-layout'
import { Modal } from './ui/Modal'

Expand Down Expand Up @@ -323,12 +323,12 @@ export function BufferPalette(): JSX.Element {
setActivePane(targetPaneId)
}

focusEditorNormalMode()
focusEditorAfterReadOpen()
}

const close = (): void => {
setOpen(false)
focusEditorNormalMode()
focusEditorAfterPaletteClose()
}

const closeSelected = async (): Promise<void> => {
Expand Down
23 changes: 19 additions & 4 deletions packages/app-core/src/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { Fragment, useEffect, useMemo, useRef, useState } from 'react'
import { useStore } from '../store'
import { buildCommands, type Command } from '../lib/commands'
import { commandLanding } from '../lib/command-landing'
import {
loadRecentCommandIds,
recordCommandUse,
Expand All @@ -27,7 +28,11 @@ import {
} from '../lib/vault-switcher'
import { runWorkflowById } from '../lib/workflow-trigger'
import type { WorkflowIndexEntry } from '../lib/workflow-index'
import { focusEditorNormalMode } from '../lib/editor-focus'
import {
focusEditorAfterPaletteClose,
focusEditorAfterReadOpen,
focusEditorNormalMode
} from '../lib/editor-focus'
import { useCloudSyncStatusStore } from '../lib/cloud-auto-sync'
import { getPublishNoteRequest } from '../lib/publish-note-requests'
import { Modal } from './ui/Modal'
Expand Down Expand Up @@ -280,7 +285,7 @@ export function CommandPalette(): JSX.Element {
revertTheme()
}
setOpen(false)
focusEditorNormalMode()
focusEditorAfterPaletteClose()
}

/* -------- Actions -------- */
Expand All @@ -304,6 +309,8 @@ export function CommandPalette(): JSX.Element {
return
}
setOpen(false)
// Asked before the run: afterwards today's note exists either way.
const landing = commandLanding(cmd)
try {
await cmd.run()
// A command that opens a note (or otherwise lands on the editor) should
Expand All @@ -316,15 +323,23 @@ export function CommandPalette(): JSX.Element {
// outline, …) so we don't pull focus behind it, and likewise for the Cloud
// conflict queue, the Cloud settings prompt and the Publish Note dialog,
// which claim focus themselves and are tracked outside the store.
// On a phone the command decides where the keyboard goes
// (lib/command-landing.ts): a note that was already there is left to
// read, a command whose point is typing takes the editor, and anything
// else (a toggle, a view) leaves the focus where it was. On desktop all
// three focus the editor.
if (
shouldRefocusEditorAfterCommand(
useStore.getState(),
useCloudSyncStatusStore.getState().conflictReviewOpen ||
useCloudSyncStatusStore.getState().settingsConflictPromptOpen ||
getPublishNoteRequest() !== null
)
)
focusEditorNormalMode()
) {
if (landing === 'read') focusEditorAfterReadOpen()
else if (landing === 'edit') focusEditorNormalMode()
else focusEditorAfterPaletteClose()
}
} catch (err) {
console.error('command failed', cmd.id, err)
}
Expand Down
18 changes: 14 additions & 4 deletions packages/app-core/src/components/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ import {
openExternalFileLink,
} from "../lib/external-file-link";
import { StatusBar } from "./StatusBar";
import { useCloudSyncStatusStore } from "../lib/cloud-auto-sync";
import { cloudSyncNeedsUser, useCloudSyncStatusStore } from "../lib/cloud-auto-sync";
import { statusBarVisible } from "../lib/status-bar-visibility";
import { defineStatusBarOption } from "../lib/vim-status-bar-option";
import { EditorPane } from "./EditorPane";
import { focusPaneInDirection, focusPaneOrEdgePanel } from "../lib/pane-nav";
import { requestPaneMode } from "../lib/pane-mode";
Expand Down Expand Up @@ -642,6 +644,7 @@ function registerVimCommands(): void {
return undefined;
},
);
defineStatusBarOption();
// `:harper on|off` (or bare `:harper` to toggle) is the ex twin of the
// Settings toggle "Grammar and spelling with Harper".
Vim.defineEx(
Expand Down Expand Up @@ -1817,6 +1820,15 @@ export function Editor(): JSX.Element {
const cloudSyncVisible = useCloudSyncStatusStore(
(state) => state.phase !== "hidden",
);
const showStatusBar = useStore((s) => s.showStatusBar);
const cloudNeedsUser = useCloudSyncStatusStore(cloudSyncNeedsUser);
const statusBarShown = statusBarVisible({
zenMode,
hasNote: activeNote !== null,
cloudShown: cloudSyncVisible,
showStatusBar,
cloudNeedsUser,
});

useEffect(() => {
registerVimCommands();
Expand All @@ -1836,9 +1848,7 @@ export function Editor(): JSX.Element {
<div className="flex min-h-0 min-w-0 flex-1">
<PaneTreeView node={paneLayout} />
</div>
{!zenMode && (activeNote || cloudSyncVisible) && (
<StatusBar note={activeNote} />
)}
{statusBarShown && <StatusBar note={activeNote} />}
</section>
);
}
Expand Down
13 changes: 11 additions & 2 deletions packages/app-core/src/components/EditorPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import {
import { Vim, getCM, vim } from '@replit/codemirror-vim'
import type { AssetMeta, ImportedAsset, NoteComment, NoteFolder } from '@shared/ipc'
import { registerNoteEditor } from '../lib/note-editor-context'
import { noteEditorHostExtension } from '../lib/editor-host'
import { hostOpensNotesToRead, noteEditorHostExtension } from '../lib/editor-host'
import {
indentWithTab,
moveLineDown,
Expand Down Expand Up @@ -114,6 +114,7 @@ import type { LineNumberMode } from '../store'
import type { PaneEdge, PaneLeaf } from '../lib/pane-layout'
import { findLeaf, inferPaneDropEdge } from '../lib/pane-layout'
import { livePreviewPlugin } from '../lib/cm-live-preview'
import { sourceRevealExtension, sourceRevealForNewNote } from '../lib/cm-source-reveal'
import { codeBlockFlairPlugin } from '../lib/cm-code-block-flair'
import { tablePlugin, tableVimEntry } from '../lib/cm-table'
import { wysiwygBlocksPlugin } from '../lib/cm-wysiwyg-blocks'
Expand Down Expand Up @@ -1773,7 +1774,9 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
})
})
setFocusedPanel('editor')
view.focus()
// An outline jump is a search pick too: where the host opens notes to
// read, the heading is shown without raising the keyboard.
if (!hostOpensNotesToRead()) view.focus()
return true
}, [schedulePreviewOutlineJump, setFocusedPanel])

Expand Down Expand Up @@ -2101,6 +2104,9 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
extensions: [
noteEditingLockExtension(() => ({ vault: useStore.getState().vault, path: viewPathRef.current })),
noteEditorHostExtension(),
// Outside the live-preview compartment, so toggling it keeps whether
// the note is being edited or read.
sourceRevealExtension,
appMarkdownSnippetExtension(),
vimCompartment.of(s0.vimMode ? vim() : []),
// No text input outside Vim insert mode, so a CJK input method
Expand Down Expand Up @@ -2534,6 +2540,9 @@ export function EditorPane({ pane }: { pane: PaneLeaf }): JSX.Element {
!!markdownCompartment &&
!!markdownSyntaxCompartment
const effects: StateEffect<unknown>[] = []
// Another note is read until the editor takes the focus (on desktop, the
// focus every opener hands over right after), unless it holds it now.
if (switched) effects.push(sourceRevealForNewNote(view))
if (deferRichMarkdown && markdownCompartment && markdownSyntaxCompartment) {
richMarkdownDeferredRef.current = true
effects.push(
Expand Down
4 changes: 2 additions & 2 deletions packages/app-core/src/components/EmbedDrawingPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { isExcalidrawPath, isObsidianExcalidrawPath } from '@shared/excalidraw'
import { rankItems } from '../lib/fuzzy-score'
import { isPaletteNextKey, isPalettePreviousKey } from '../lib/palette-nav'
import { isImeComposing } from '../lib/ime'
import { focusEditorNormalMode } from '../lib/editor-focus'
import { focusEditorAfterPaletteClose, focusEditorNormalMode } from '../lib/editor-focus'
import { Modal } from './ui/Modal'

/**
Expand Down Expand Up @@ -55,7 +55,7 @@ export function EmbedDrawingPalette(): JSX.Element {

const close = (): void => {
setOpen(false)
focusEditorNormalMode()
focusEditorAfterPaletteClose()
}

return (
Expand Down
2 changes: 2 additions & 0 deletions packages/app-core/src/components/ExternalFileApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { isTouchPrimaryDevice, vimImeGuard } from '../lib/cm-vim-ime-guard'
import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language'
import type { ExternalFileContent } from '@shared/ipc'
import { livePreviewPlugin } from '../lib/cm-live-preview'
import { sourceRevealExtension } from '../lib/cm-source-reveal'
import { headingFolding } from '../lib/cm-heading-fold'
import { LazyPreview as Preview } from './LazyPreview'
import { CloseIcon, InboxIcon } from './icons'
Expand Down Expand Up @@ -167,6 +168,7 @@ export function ExternalFileApp(): JSX.Element {
doc: bodyRef.current ?? '',
extensions: [
appMarkdownSnippetExtension(),
sourceRevealExtension,
new Compartment().of(prefs.vimMode ? vim() : []),
// No text input outside Vim insert mode, as in every other editor:
// without it a CJK input method composed over hjkl and typed `ia`
Expand Down
Loading
Loading