zudo-text

検索したい単語を入力

いつでも検索バーを開ける

Edit key bar

The editing palette presents the same editing commands on one of several mutually exclusive surfaces. On desktop the surface is chosen by editKeyBar.placement, which has four values — docked top, docked bottom, floating horizontal bar, floating vertical rail. On compact mobile chrome the shell owns its own palette around a permanent More tools launcher and ignores placement entirely. All of them share key definitions and execution guards; the desktop placements additionally share one grouped … / right-click menu, which the compact palette does not use. End-user walkthrough (Japanese): manual/src/content/docs/basics/mobile-keys.mdx.

enabled is a separate show/hide bit, independent of placement. That separation is the whole point of epic #6674: hiding the keys never discards where they were, so showing them again restores the last placement.

Where the code lives

PieceFile
Settings shape, key id unionpackages/app-defaults/src/types.ts (AppSettings.editKeyBar, EditKeyBarKeyId, EditKeyPaletteSlot)
Catalog order + default key listpackages/app-defaults/src/defaults.ts (EDIT_KEY_BAR_KEY_IDS, DEFAULT_EDIT_KEY_BAR_KEYS, defaultEditKeyBarSettings)
Validationpackages/app-defaults/src/validate-settings.ts
Key catalog (label / title / behavior)tauri-app/renderer/components/edit-key-bar/key-catalog.ts
CodeMirror commands behind the keystauri-app/renderer/components/edit-key-bar/key-commands.ts
Shared key buttons and groupingtauri-app/renderer/components/edit-key-bar/palette-key-button.tsx, palette-key-groups.tsx
Shared executor, focus status, Control choosertauri-app/renderer/components/edit-key-bar/use-edit-key-executor.ts, control-chord-chooser.tsx
Docked bartauri-app/renderer/components/edit-key-bar/edit-key-bar.tsx
Compact palette + geometrytauri-app/renderer/components/edit-key-bar/floating-edit-key-palette.tsx, palette-layout.ts, use-palette-position.ts, tauri-app/renderer/hooks/use-compact-mobile-viewport.ts
Compact move modetauri-app/renderer/components/edit-key-bar/palette-move-overlay.tsx
Desktop floating keys + local geometrytauri-app/renderer/components/edit-key-bar/floating-keys-slot.tsx, floating-keys.tsx, floating-keys-geometry.ts, use-floating-keys-state.ts
Mount + render rule + layout reservationtauri-app/renderer/app.tsx, tauri-app/renderer/lib/edit-key-bar-layout-style.ts
Settings section (Editing palette)tauri-app/renderer/components/settings/sections/edit-key-bar-settings.tsx
Placement → surface resolutiontauri-app/renderer/lib/edit-key-bar-placement.ts (resolveEditKeyBarSurface)
Grouped … / right-click menu itemstauri-app/renderer/components/edit-key-bar/edit-key-menu-items.tsx
Header toggle icon (edit-key-bar)tauri-app/renderer/components/toolbar.tsx, toolbar/use-right-cluster-icons.ts

Settings shape

editKeyBar: {
  enabled: boolean;          // default false (opt-in)
  keys: EditKeyBarKeyId[];   // shared visible key list; absent ids are hidden
  favorites: Partial<Record<EditKeyPaletteSlot, EditKeyBarKeyId>>;
  compactMobile: boolean;    // default true
  placement: EditKeyBarPlacement; // default "dock-top"
}

// packages/app-defaults/src/types.ts
type EditKeyBarPlacement = "dock-top" | "dock-bottom" | "float-bar" | "float-rail";
  • validateSettings falls back to defaultEditKeyBarSettings when the object is missing or malformed, drops unknown ids, de-duplicates keys, and keeps only valid slot/key pairs in favorites. An empty keys array or favorites object is valid. placement is checked against EDIT_KEY_BAR_PLACEMENTS and falls back to "dock-top" on anything else.

  • EDIT_KEY_BAR_KEY_IDS is the full catalog order (27 ids). The Settings section lists visible keys in keys order, then hidden keys in catalog order. DEFAULT_EDIT_KEY_BAR_KEYS contains 20 ids; the remaining catalog ids (tab, number, strike, fence, hr, home, end) are opt-in.

  • The eight EditKeyPaletteSlot values are upFar, upNear, leftFar, leftNear, rightNear, rightFar, downNear, and downFar. Empty slots have no element and no pointer-blocking overlay; the center More button is always present while compact mode is mounted.

  • All fields above, including placement, are workspace preferences in .zudotext.settings.json. The compact palette position and desktop floating geometry are separate device/window-local localStorage values. Desktop state retains independent bar/rail positions, the rail's snapped edge, and its collapsed flag; none belongs in AppSettings.

Entry points

  • Header icon — exactly one: edit-key-bar (keyboard glyph, "Toggle editing keys"). It is a plain show/hide toggle over enabled, exposes its value with aria-pressed, and can be hidden in Menu Bar settings. There is no second float icon and the icon does not open a menu — both were considered and rejected in epic #6674. MENU_BAR_ICON_METADATA therefore lists eleven right-cluster ids, not twelve.

  • Command palette — toggle-edit-key-bar ("Toggle editing keys", category View), plus one row per placement: set-edit-key-placement-dock-top, -dock-bottom, -float-bar, -float-rail ("Editing keys: dock to top", "… dock to bottom", "… float as horizontal bar", "… float as vertical rail", all category View). The placement rows are omitted while compact mobile chrome is mounted.

  • Shortcut ids — toggleEditKeyBar, no default binding and no shortcut-groups.ts row, so it is currently not user-assignable. moveEditingPalette has a Settings → Shortcuts row and no default binding. There is no placement shortcut and no toggleEditKeyFloat — that id was removed with the float flag.

  • Additional commands — move-editing-palette ("Move editing palette") is disabled unless compact mode is mounted.

  • Settings → Editing palette — Show editing keys, the Placement select (Docked to top / Docked to bottom / Floating horizontal bar / Floating vertical rail), Compact mobile chrome, the eight-slot Directional favorites map, and Keys inside More (checkbox, up / down move buttons, Reset keys). The section's registry key remains edit-key-bar.

setEditKeyPlacement in app.tsx writes { placement, enabled: true }: choosing a placement also shows the keys. That is what makes the four palette rows the reachable path back when the keys are hidden — without it they would look like no-ops. hideEditKeyBar is a separate writer that always sets enabled: false rather than flipping it, so it stays correct in the mobile edge case where the docked bar renders with enabled already false. Settings writes the full object while preserving unrelated fields.

Render rule

resolveEditKeyBarSurface in lib/edit-key-bar-placement.ts is the single decision. It returns one EditKeyBarSurface — "dock-top" | "dock-bottom" | "float" | "compact-palette" | "none" — and app.tsx mounts exactly the matching element:

const compact = compactMobile &&
  (cssWidth <= 640 || (primaryPointerIsCoarse && cssWidth <= 1024));

function resolveEditKeyBarSurface({ placement, enabled, compact, headerVisible, isMobileViewport }) {
  if (compact) return "compact-palette";
  if (placement === "dock-top" || placement === "dock-bottom") {
    return enabled || (!headerVisible && isMobileViewport) ? placement : "none";
  }
  return enabled ? "float" : "none";
}

const navigationOnly = !editKeyBar.enabled || isWorkspaceSwitching;
  • Compact wins over placement: the compact palette is mounted whether or not the keys are enabled, and navigationOnly degrades it to App/Frame navigation.

  • The header-hidden disjunct applies to the docked placements only. A floating surface with enabled false renders nothing, so Toggle Global Header in Command Palette remains the way back in that case.

  • dock-top and dock-bottom mount the same EditKeyBar element at two positions in the content column — under the header, or as the last in-flow child below the frameset. It is a non-shrinking flex child at both, so the bottom dock needs no extra layout reservation.

  • The shared 640 px CSS breakpoint still governs fullscreen/dialog choices. Compact eligibility additionally includes a coarse primary pointer up to 1024 px.

  • When compact, the normal toolbar, edit-key bar, workspace rail, and mobile frame strip are replaced by FloatingEditKeyPalette. The center More tools control remains even when enabled is false; navigationOnly hides favorites and Keys while App/Frame navigation remains available. A float-* placement stays stored while compact mode is active; it simply does not mount.

  • A compact palette filters getPaletteEditorView() through the currently focused and visible frame. A retained CodeMirror view from a hidden leaf never receives a key.

  • The shell computes fixed CSS coordinates from visualViewport, safe-area insets, and offline-banner height. If the requested favorite footprint cannot fit, it renders the center-only footprint and lists displaced favorites in More → Keys.

  • Show header is pinned only in the non-compact mobile docked bar, including a keys-off bar. A float placement suppresses that bar, so Toggle Global Header in Command Palette remains the way back. In compact mode, Show editor controls and Return to writing are frame actions; they do not change the palette's settings.

Grouped menu

One menu serves every desktop placement, built by the pure item builders in edit-key-menu-items.tsx and rendered through the shared ContextMenu primitive. There is no placement submenu — a submenu put placement three hops away and was rejected as hostile on touch.

The bar menu, in order:

  1. Placement label

  2. Four checkbox rows — Docked top, Docked bottom, Float horizontal, Float vertical, each with an icon, the current placement checked

  3. separator

  4. Configure palette… (dispatches OPEN_EDITING_PALETTE_SETTINGS_EVENT)

  5. separator

  6. Hide editing keys

Three openers reach it. The trailing … button (data-testid="edit-keys-menu-trigger") is present on every placement, including both docked ones. A right-click on empty bar area opens the same list. A right-click on a key resolves the id via the nearest data-palette-key-id ancestor and prepends the key's own rows — the key title as a label, Hide this key, Move earlier, Move later (each disabled at its end of keys) — then a separator, then the same bar menu. moveEditKeyEarlier / moveEditKeyLater are pure swaps over editKeyBar.keys.

The bar always calls preventDefault() on contextmenu, so the native WebKit menu never appears over the keys. Menu triggers use the same keepEditorFocus pointerdown/mousedown guard as the keys themselves. The compact mobile palette does not use this menu at all — it keeps its own Move palette / Configure palette header buttons and ignores placement.

For the shared rules this menu follows — grouping, checked groups, one ContextMenu per surface, the keep-native list — see Buttons and context menus.

More tools and editor focus

useEditKeyExecutor observes whether the live editor has focus, independently from whether a command may run. Focus/composition events refresh the snapshot, a 60 ms blur settle avoids flicker on brief focus hops, and a small status poll catches retained-view changes. Read-only and composing editors can still count as focused; command availability uses separate guards.

The compact launcher shows favorites while editing. When opening More tools, it captures that editing state: focused opens Keys, otherwise it opens App without a Keys selector. This state stays latched for the open panel so keyboard navigation inside it does not remove its own controls. The configured footprint stays unchanged when unfocused favorites disappear, keeping the launcher's position stable.

Keys uses a four-column keycap grid in configured order, including across catalog categories. When the palette is constrained, displaced favorites absent from the configured list are added before it so they remain reachable. Favorites already in the list keep their configured position. App uses full-width icon-and-label rows under Go, Note, Workspace, Frames. Frame contains the active frame controls when available. The header provides Move palette, Configure palette, and Close tools.

PaletteKeyGroups groups by EDIT_KEY_GROUP_ORDER by default and preserves configured order within each group. The compact More Keys grid opts into flat input-order rendering so cross-category moves in the globally reorderable keys setting are visible. The docked bar uses PaletteKeyButton directly and preserves the complete keys order; the desktop floating bar/rail reuse the grouped presentation without captions.

Compact move mode

Move palette or the move-editing-palette command enters PaletteMoveOverlay. It closes tools/configuration and cancels an armed Control chord. The modal overlay shows reachable snap stops, the occupied footprint, the starting outline, and active edges. Dragging, arrow buttons, arrow keys, or H/J/K/L move the preview; Shift moves four stops. R/Reset position previews the default position. Done/Enter commits and persists; Cancel/Esc restores the start. Tab visits the controls, where Enter/Space activate the focused button.

The overlay captures keyboard events before editor/global shortcut handlers and keeps focus inside its controls. An interrupted/unmounted move cancels; a completed move restores the original editor only if it is still the current connected target and is not composing, otherwise it focuses the launcher. Preview motion never persists before commit.

Desktop floating keys

FloatingKeysSlot mounts FloatingKeys, which portals a compact surface into the same window. float-bar lays the key groups out in one row; float-rail renders two columns per group. The overall default placement is dock-top, so neither mounts until the user picks a float placement. A grip drags the surface within window bounds. The rail snaps to either side near an edge and can collapse to its grip and expand button. Overflow scrolls along the active orientation.

FloatingKeys derives its layout from placement (layout = placement === "float-rail" ? "rail" : "bar"); there is no separate layout field. The trailing surface toggle ("Switch to vertical rail" / "Switch to horizontal bar") writes the other float placement, and the grouped menu's placement rows cover the same ground plus the two docked values — the old dedicated docking item is gone, because docking is now simply one of the placement rows. useFloatingKeysState stores normalized bar/rail positions, snapped edge, and collapsed state under zudotext.floating-keys:<window-name> (falling back to main). The shared executor and Control chooser retain the same focus, read-only, and composition guards as the other presentations.

Key catalog contract

Every id in EditKeyBarKeyId must have exactly one EditKeyDef in key-catalog.ts:

interface EditKeyDef {
  id: EditKeyBarKeyId;
  label: string;   // fallback glyph/text when no icon is supplied
  title: string;   // aria-label + tooltip, also the Settings row name
  group: EditKeyGroup;
  icon?: IconComponent;
  run: (view: EditorView, context: EditKeyContext) => boolean;
  focusAfter: "refocus" | "none";
  readOnlyAllowed?: boolean;
}

interface EditKeyContext {
  onInsertAsset?: () => void;
  onArmCtrl?: () => void;
}
  • run receives the live editor from the presentation's getter and returns whether it handled the key. useEditKeyExecutor rejects detached views and rechecks the target when invoked. If an open compact Keys panel loses its target, it reports Select an editor to use editing keys.; opening More without editor focus omits Keys entirely.

  • focusAfter: "refocus" calls view.focus() after run when the target is still current. "none" is for hide-keyboard (blurs contentDOM to dismiss the soft keyboard), insert-asset (opens a dialog), and ctrl (the executor arms the Control chooser without invoking its catalog run).

  • Editing commands in key-commands.ts return false on a read-only state and dispatch with userEvent: "input" and scrollIntoView: true. The executor exposes This editor is read-only. and leaves read-only-safe Escape, cursor, and Hide keyboard actions available. During composition it exposes Finish composing text to use editing keys. and disables all editing keys.

  • Escape and the Control chord are dispatched to the active editor scope only. They never call the global application/OS shortcut bus.

Notable behaviors, for when a key "does the wrong thing":

  • wrapSelection(left, right) toggles: it unwraps when the markers sit just outside the range or at both ends of the selection. Italic uses _, not *.

  • toggleLinePrefix(prefix, family) applies to every line touched by any range (blank lines skipped when more than one line is selected), preserves leading indentation, replaces a different prefix from the same family (e.g. - → - [ ] ), and removes the prefix only when every target line already carries the same family member.

  • insertLink wraps the selection as [selected]() and leaves the cursor inside the parentheses; with no selection it inserts []().

  • fence inserts a fenced block with the cursor on its empty middle line.

Adding a key

  1. Add the id to the EditKeyBarKeyId union in packages/app-defaults/src/types.ts.

  2. Add it to EDIT_KEY_BAR_KEY_IDS in defaults.ts at the catalog position you want; add it to DEFAULT_EDIT_KEY_BAR_KEYS only if it should be on by default. Run pnpm generate:core-defaults if the default list changed.

  3. Add an EditKeyDef in key-catalog.ts — use editing(id, label, title, command, group, icon?, readOnlyAllowed?) for plain editor commands, or write the object out for a key that needs context or focusAfter: "none". Reuse an existing icon when available.

  4. If the behavior is new, add a pure KeyCommand to key-commands.ts with a unit test in key-commands.test.ts.

Keep fallback labels short: the docked row and mobile palette keys use the 44 px --spacing-touch-min floor; desktop floating keys use the smaller --spacing-icon-btn-square size. The compact palette uses sparse grid cells, so empty cross-corners stay pointer-transparent and each occupied favorite remains a touch target.

Why fixed and app-level

  • Fixed viewport, not above the keyboard. A palette that follows the soft keyboard tracks the Visual Viewport API and safe bounds. At non-natural zoom, the shell retains its previous natural-scale footprint; at natural scale it recomputes after viewport resize/scroll events.

  • App-level, not in the editor chrome. The docked bar, compact palette, and desktop floating keys are mutually exclusive presentations mounted in app.tsx, outside EditorExperience's headerActions slot. The inbox editor renders EditorPane directly and bypasses that slot.

  • In layout or an overlay. Only the docked bar reserves --edit-key-bar-height. Compact mode removes that reservation and mounts a sparse fixed grid whose empty cells have no pointer target. Desktop floating mode also removes the reservation and portals its surface above the content.

  • Focus. PaletteKeyButton calls preventDefault() on pointerdown and mousedown and acts on click. This keeps focus in CodeMirror for editing commands, while Hide keyboard and Insert asset intentionally leave focus. Move mode has its own modal focus lifecycle.

Insert asset and the captured-target contract

The insert-asset key dispatches INSERT_ASSET_EVENT, the same event as the editor context-menu item Insert asset…, the palette command Insert Asset… (insert-asset), and the hidden insertAsset shortcut (Mod+Shift+U). AssetsFilerHost (mounted in app.tsx, outside Toolbar, so it works with the header hidden) opens the Assets dialog in pick mode with From Assets and Upload tabs.

Insertion must land in the editor the action was invoked for, even though the dialog takes focus:

  1. Capture at invocation. The host's event listener calls captureEditorTarget() synchronously, before the dialog renders. It records { view, docIdentity } from getActiveEditorView() — which is still the editor, because the bar never took focus.

  2. Validate at insert. checkCapturedEditorTarget cancels with a reason when the view has been destroyed, is read-only, or now shows a different document (getEditorDocumentIdentity changed). The reason is surfaced as an error notification (describeEditorTargetUnavailable).

  3. Insert at the live selection. A valid target receives the Markdown at its current selection.main.head, not a position frozen at capture time.

  4. Fallback. With no captured target (no editor was active), insertion falls back to the asset:insert-reference app event for the active pane.

The Upload tab (asset-upload-tab.tsx → upload-and-insert.ts) saves each file and inserts through the same captured target; a target that becomes unavailable mid-batch stops further inserts (already-saved files stay saved). The browse-mode panel's own Insert into note captures the target when the button is pressed instead, because that panel is long-lived and non-modal. Escape cancels pick mode; in browse mode it only backs out of transient surfaces.

  • Media uploader — every way a file reaches the workspace, including the Upload tab.