From a5a3e8f1080b201220c6fd8e9f57351fda5a0e3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Michelle?= Date: Tue, 23 Dec 2025 16:40:49 +0100 Subject: [PATCH] finished ShortcutManager --- packages/app/studio/src/mixins.sass | 4 +- .../app/studio/src/service/StudioDialogs.ts | 31 +++++- .../app/studio/src/service/StudioService.ts | 4 - .../src/service/StudioShortcutManager.ts | 98 +++++++++++++------ .../studio/src/shortcuts/GlobalShortcuts.ts | 5 +- .../src/shortcuts/ShortcutDefinition.ts | 3 + .../src/shortcuts/ShortcutDefinitions.ts | 38 +++++++ .../studio/src/shortcuts/ShortcutValidator.ts | 5 +- .../ui/components/ShortcutManagerView.sass | 58 +++++++---- .../src/ui/components/ShortcutManagerView.tsx | 50 ++++++---- packages/lib/dom/src/shortcut-manager.ts | 19 +++- 11 files changed, 226 insertions(+), 89 deletions(-) create mode 100644 packages/app/studio/src/shortcuts/ShortcutDefinition.ts create mode 100644 packages/app/studio/src/shortcuts/ShortcutDefinitions.ts diff --git a/packages/app/studio/src/mixins.sass b/packages/app/studio/src/mixins.sass index 66852a8d..c13657c7 100644 --- a/packages/app/studio/src/mixins.sass +++ b/packages/app/studio/src/mixins.sass @@ -170,9 +170,9 @@ @if $semitransparent backdrop-filter: blur(2px) - background-color: hsla(200, 12%, 9%, 0.98) + background-color: color-mix(in srgb, var(--color-panel-background-dark), transparent 1%) @else - background-color: hsl(200, 12%, 9%) + background-color: var(--color-panel-background-dark) @mixin corner-frame($color: rgba(white, 0.06)) position: relative diff --git a/packages/app/studio/src/service/StudioDialogs.ts b/packages/app/studio/src/service/StudioDialogs.ts index c7889df2..1f35c80f 100644 --- a/packages/app/studio/src/service/StudioDialogs.ts +++ b/packages/app/studio/src/service/StudioDialogs.ts @@ -1,8 +1,11 @@ -import {Terminator} from "@opendaw/lib-std" +import {Notifier, Terminator} from "@opendaw/lib-std" import {Promises} from "@opendaw/lib-runtime" import {Dialogs} from "@/ui/components/dialogs" import {PreferencePanel} from "@/ui/PreferencePanel" import {ShortcutManagerView} from "@/ui/components/ShortcutManagerView" +import {GlobalShortcuts, GlobalShortcutsFactory} from "@/shortcuts/GlobalShortcuts" +import {ShortcutDefinitions} from "@/shortcuts/ShortcutDefinitions" +import {StudioShortcutManager} from "@/service/StudioShortcutManager" export namespace StudioDialogs { export const showPreferences = async () => { @@ -17,11 +20,31 @@ export namespace StudioDialogs { export const showShortcutManager = async () => { const lifecycle = new Terminator() + const abortController = new AbortController() + const updateNotifier = new Notifier() + const contexts = { + "Global": ShortcutDefinitions.copy(GlobalShortcuts) + } satisfies Record await Promises.tryCatch(Dialogs.show({ headline: "Shortcut Manager", - content: ShortcutManagerView({lifecycle}), - growWidth: true - })) + content: ShortcutManagerView({lifecycle, contexts, updateNotifier}), + growWidth: true, + abortSignal: abortController.signal, + buttons: [ + { + text: "Reset", onClick: () => { + contexts["Global"] = ShortcutDefinitions.copy(GlobalShortcutsFactory) + updateNotifier.notify() + } + }, + {text: "Cancel", onClick: () => abortController.abort()} + ] + })).then(() => { + if (!abortController.signal.aborted) { + ShortcutDefinitions.copyInto(contexts.Global, GlobalShortcuts) + StudioShortcutManager.store() + } + }) lifecycle.terminate() } } \ No newline at end of file diff --git a/packages/app/studio/src/service/StudioService.ts b/packages/app/studio/src/service/StudioService.ts index 31f7fbca..80f80a9e 100644 --- a/packages/app/studio/src/service/StudioService.ts +++ b/packages/app/studio/src/service/StudioService.ts @@ -65,7 +65,6 @@ import {Surface} from "@/ui/surface/Surface" import {SoftwareMIDIPanel} from "@/ui/software-midi/SoftwareMIDIPanel" import {Mixdowns} from "@/service/Mixdowns" import {ShadertoyState} from "@/ui/shadertoy/ShadertoyState" -import {StudioDialogs} from "@/service/StudioDialogs" /** * I am just piling stuff after stuff in here to boot the environment. @@ -137,9 +136,6 @@ export class StudioService implements ProjectEnv { this.#configBeforeUnload() this.#checkRecovery() this.#listenPreferences() - - // TODO Remove when done - setTimeout(() => StudioDialogs.showShortcutManager().then(), 500) } get sampleRate(): number {return this.audioContext.sampleRate} diff --git a/packages/app/studio/src/service/StudioShortcutManager.ts b/packages/app/studio/src/service/StudioShortcutManager.ts index 3e359df8..6628f0e0 100644 --- a/packages/app/studio/src/service/StudioShortcutManager.ts +++ b/packages/app/studio/src/service/StudioShortcutManager.ts @@ -1,53 +1,89 @@ import {ShortcutManager, ShortcutOptions} from "@opendaw/lib-dom" import {StudioService} from "@/service/StudioService" import {PanelType} from "@/ui/workspace/PanelType" -import {Arrays, asInstanceOf, isNull, RuntimeNotifier, Subscription, Terminable} from "@opendaw/lib-std" +import { + Arrays, + asInstanceOf, + isDefined, + isNull, + RuntimeNotifier, + Subscription, + Terminable, + tryCatch +} from "@opendaw/lib-std" import {DefaultWorkspace} from "@/ui/workspace/Default" import {Workspace} from "@/ui/workspace/Workspace" import {AudioUnitBox} from "@opendaw/studio-boxes" import {ProjectUtils} from "@opendaw/studio-adapters" import {StudioDialogs} from "@/service/StudioDialogs" -import {GlobalShortcuts} from "@/shortcuts/GlobalShortcuts" +import {GlobalShortcuts, GlobalShortcutsFactory} from "@/shortcuts/GlobalShortcuts" +import {ShortcutDefinitions} from "@/shortcuts/ShortcutDefinitions" export namespace StudioShortcutManager { + const localStorageKey = "shortcuts" + + export const reset = (): void => { + for (const [key, {keys}] of Object.entries(GlobalShortcutsFactory)) { + GlobalShortcuts[key].keys.overrideWith(keys) + } + } + + export const store = () => { + try { + const jsonString = JSON.stringify({global: ShortcutDefinitions.toJSON(GlobalShortcuts)}) + localStorage.setItem(localStorageKey, jsonString) + console.debug("Shortcuts saved.") + } catch (reason) { + console.warn(reason) + } + } + export const install = (service: StudioService): Subscription => { - const {global: g} = ShortcutManager.get() + const {global: gc} = ShortcutManager.get() const { engine: {metronomeEnabled}, panelLayout, timeline: {clips: {visible: clipsVisibility}, followCursor, primaryVisibility: {markers, tempo}} } = service - const s = GlobalShortcuts + const gs = GlobalShortcuts + const storedShortcuts = localStorage.getItem(localStorageKey) + if (isDefined(storedShortcuts)) { + const {status, value} = tryCatch(() => JSON.parse(storedShortcuts)) + if (status === "success" && "global" in value) { + console.debug("Custom shortcuts loaded.") + ShortcutDefinitions.fromJSON(gs, value.global) + } + } const subscriptions = Terminable.many( - g.register(s["project-undo"].keys, () => service.runIfProject(project => project.editing.undo())), - g.register(s["project-redo"].keys, () => service.runIfProject(project => project.editing.redo())), - g.register(s["project-open"].keys, async () => await service.browseLocalProjects()), - g.register(s["project-save"].keys, async () => await service.projectProfileService.save(), + gc.register(gs["project-undo"].keys, () => service.runIfProject(project => project.editing.undo())), + gc.register(gs["project-redo"].keys, () => service.runIfProject(project => project.editing.redo())), + gc.register(gs["project-open"].keys, async () => await service.browseLocalProjects()), + gc.register(gs["project-save"].keys, async () => await service.projectProfileService.save(), ShortcutOptions.of({activeInTextField: true})), - g.register(s["project-save-as"].keys, async () => await service.projectProfileService.saveAs(), + gc.register(gs["project-save-as"].keys, async () => await service.projectProfileService.saveAs(), ShortcutOptions.of({activeInTextField: true})), - g.register(s["toggle-playback"].keys, () => { + gc.register(gs["toggle-playback"].keys, () => { const {engine} = service const isPlaying = engine.isPlaying.getValue() if (isPlaying) {engine.stop()} else {engine.play()} }), - g.register(s["toggle-software-keyboard"].keys, () => service.toggleSoftwareKeyboard()), - g.register(s["toggle-device-panel"].keys, () => panelLayout.getByType(PanelType.DevicePanel).toggleMinimize()), - g.register(s["toggle-content-editor-panel"].keys, () => panelLayout.getByType(PanelType.ContentEditor).toggleMinimize()), - g.register(s["toggle-browser-panel"].keys, () => panelLayout.getByType(PanelType.BrowserPanel).toggleMinimize()), - g.register(s["toggle-tempo-track"].keys, () => tempo.setValue(!tempo.getValue())), - g.register(s["toggle-markers-track"].keys, () => markers.setValue(!markers.getValue())), - g.register(s["toggle-clips"].keys, () => clipsVisibility.setValue(!clipsVisibility.getValue())), - g.register(s["toggle-follow-cursor"].keys, () => followCursor.setValue(!followCursor.getValue())), - g.register(s["toggle-metronome"].keys, () => metronomeEnabled.setValue(!metronomeEnabled.getValue())), - g.register(s["copy-device"].keys, () => service.runIfProject( + gc.register(gs["toggle-software-keyboard"].keys, () => service.toggleSoftwareKeyboard()), + gc.register(gs["toggle-device-panel"].keys, () => panelLayout.getByType(PanelType.DevicePanel).toggleMinimize()), + gc.register(gs["toggle-content-editor-panel"].keys, () => panelLayout.getByType(PanelType.ContentEditor).toggleMinimize()), + gc.register(gs["toggle-browser-panel"].keys, () => panelLayout.getByType(PanelType.BrowserPanel).toggleMinimize()), + gc.register(gs["toggle-tempo-track"].keys, () => tempo.setValue(!tempo.getValue())), + gc.register(gs["toggle-markers-track"].keys, () => markers.setValue(!markers.getValue())), + gc.register(gs["toggle-clips"].keys, () => clipsVisibility.setValue(!clipsVisibility.getValue())), + gc.register(gs["toggle-follow-cursor"].keys, () => followCursor.setValue(!followCursor.getValue())), + gc.register(gs["toggle-metronome"].keys, () => metronomeEnabled.setValue(!metronomeEnabled.getValue())), + gc.register(gs["copy-device"].keys, () => service.runIfProject( ({editing, userEditingManager, skeleton}) => userEditingManager.audioUnit.get().ifSome(({box}) => { const audioUnitBox = asInstanceOf(box, AudioUnitBox) const copies = editing.modify(() => ProjectUtils .extractAudioUnits([audioUnitBox], skeleton), false).unwrap() userEditingManager.audioUnit.edit(copies[0].editing) }))), - g.register(s["workspace-next-screen"].keys, () => { + gc.register(gs["workspace-next-screen"].keys, () => { const keys = Object.entries(DefaultWorkspace).map(([key]) => key as Workspace.ScreenKeys) const screen = service.layout.screen const current = screen.getValue() @@ -55,7 +91,7 @@ export namespace StudioShortcutManager { screen.setValue(Arrays.getNext(keys, current)) } ), - g.register(s["workspace-prev-screen"].keys, () => { + gc.register(gs["workspace-prev-screen"].keys, () => { const keys = Object.entries(DefaultWorkspace).map(([key]) => key as Workspace.ScreenKeys) const screen = service.layout.screen const current = screen.getValue() @@ -63,16 +99,16 @@ export namespace StudioShortcutManager { screen.setValue(Arrays.getPrev(keys, current)) } ), - g.register(s["workspace-screen-dashboard"].keys, async () => await service.closeProject()), - g.register(s["workspace-screen-default"].keys, () => service.runIfProject(() => service.switchScreen("default"))), - g.register(s["workspace-screen-mixer"].keys, () => service.runIfProject(() => service.switchScreen("mixer"))), - g.register(s["workspace-screen-piano"].keys, () => service.runIfProject(() => service.switchScreen("piano"))), - g.register(s["workspace-screen-project"].keys, () => service.runIfProject(() => service.switchScreen("project"))), - g.register(s["workspace-screen-meter"].keys, () => service.runIfProject(() => service.switchScreen("meter"))), - g.register(s["workspace-screen-shadertoy"].keys, () => service.runIfProject(() => service.switchScreen("shadertoy"))), - g.register(s["show-preferences"].keys, () => StudioDialogs.showPreferences()) + gc.register(gs["workspace-screen-dashboard"].keys, async () => await service.closeProject()), + gc.register(gs["workspace-screen-default"].keys, () => service.runIfProject(() => service.switchScreen("default"))), + gc.register(gs["workspace-screen-mixer"].keys, () => service.runIfProject(() => service.switchScreen("mixer"))), + gc.register(gs["workspace-screen-piano"].keys, () => service.runIfProject(() => service.switchScreen("piano"))), + gc.register(gs["workspace-screen-project"].keys, () => service.runIfProject(() => service.switchScreen("project"))), + gc.register(gs["workspace-screen-meter"].keys, () => service.runIfProject(() => service.switchScreen("meter"))), + gc.register(gs["workspace-screen-shadertoy"].keys, () => service.runIfProject(() => service.switchScreen("shadertoy"))), + gc.register(gs["show-preferences"].keys, () => StudioDialogs.showPreferences()) ) - const conflicts = g.hasConflicts() + const conflicts = gc.hasConflicts() if (conflicts) { RuntimeNotifier.info({ headline: "Shortcut Conflict", diff --git a/packages/app/studio/src/shortcuts/GlobalShortcuts.ts b/packages/app/studio/src/shortcuts/GlobalShortcuts.ts index f9410e2f..1a391eb8 100644 --- a/packages/app/studio/src/shortcuts/GlobalShortcuts.ts +++ b/packages/app/studio/src/shortcuts/GlobalShortcuts.ts @@ -1,10 +1,11 @@ import {Key, ShortcutKeys} from "@opendaw/lib-dom" import {ShortcutValidator} from "@/shortcuts/ShortcutValidator" +import {ShortcutDefinitions} from "@/shortcuts/ShortcutDefinitions" const shift = true const ctrl = true -export const GlobalShortcuts = ShortcutValidator.validate({ +export const GlobalShortcutsFactory = ShortcutValidator.validate({ "project-undo": { keys: ShortcutKeys.of(Key.KeyZ, {ctrl}), description: "Undo last action" @@ -110,3 +111,5 @@ export const GlobalShortcuts = ShortcutValidator.validate({ description: "Open preferences" } }) + +export const GlobalShortcuts = ShortcutDefinitions.copy(GlobalShortcutsFactory) \ No newline at end of file diff --git a/packages/app/studio/src/shortcuts/ShortcutDefinition.ts b/packages/app/studio/src/shortcuts/ShortcutDefinition.ts new file mode 100644 index 00000000..c9ff83d8 --- /dev/null +++ b/packages/app/studio/src/shortcuts/ShortcutDefinition.ts @@ -0,0 +1,3 @@ +import {ShortcutKeys} from "@opendaw/lib-dom" + +export type ShortcutDefinition = { keys: ShortcutKeys, description: string } \ No newline at end of file diff --git a/packages/app/studio/src/shortcuts/ShortcutDefinitions.ts b/packages/app/studio/src/shortcuts/ShortcutDefinitions.ts new file mode 100644 index 00000000..31358234 --- /dev/null +++ b/packages/app/studio/src/shortcuts/ShortcutDefinitions.ts @@ -0,0 +1,38 @@ +import {ShortcutKeys} from "@opendaw/lib-dom" +import {ShortcutDefinition} from "@/shortcuts/ShortcutDefinition" +import {isAbsent, JSONValue} from "@opendaw/lib-std" + +export type ShortcutDefinitions = Record + +export namespace ShortcutDefinitions { + export const copy = (defs: ShortcutDefinitions): ShortcutDefinitions => { + const result: ShortcutDefinitions = {} + for (const [key, {keys, description}] of Object.entries(defs)) { + result[key] = {keys: keys.copy(), description} + } + return result + } + + export const copyInto = (source: ShortcutDefinitions, target: ShortcutDefinitions): void => { + for (const [key, {keys}] of Object.entries(source)) { + target[key].keys.overrideWith(keys.copy()) + } + } + + export const toJSON = (defs: ShortcutDefinitions): JSONValue => { + const result: Record = {} + for (const [key, {keys}] of Object.entries(defs)) { + result[key] = keys.toJSON() + } + return result + } + + export const fromJSON = (defs: ShortcutDefinitions, values: JSONValue): void => { + if (typeof values !== "object" || values === null || Array.isArray(values)) {return} + for (const [key, value] of Object.entries(values) as Array<[string, JSONValue]>) { + const def = defs[key] + if (isAbsent(def)) {continue} + ShortcutKeys.fromJSON(value).ifSome(keys => def.keys.overrideWith(keys)) + } + } +} \ No newline at end of file diff --git a/packages/app/studio/src/shortcuts/ShortcutValidator.ts b/packages/app/studio/src/shortcuts/ShortcutValidator.ts index 7d4b6888..00ad5228 100644 --- a/packages/app/studio/src/shortcuts/ShortcutValidator.ts +++ b/packages/app/studio/src/shortcuts/ShortcutValidator.ts @@ -1,7 +1,4 @@ -import {ShortcutKeys} from "@opendaw/lib-dom" - -export type ShortcutDefinition = { keys: ShortcutKeys, description: string } -export type ShortcutDefinitions = Record +import {ShortcutDefinitions} from "@/shortcuts/ShortcutDefinitions" export namespace ShortcutValidator { export const validate = (actions: T): T => { diff --git a/packages/app/studio/src/ui/components/ShortcutManagerView.sass b/packages/app/studio/src/ui/components/ShortcutManagerView.sass index 26ddd518..599db017 100644 --- a/packages/app/studio/src/ui/components/ShortcutManagerView.sass +++ b/packages/app/studio/src/ui/components/ShortcutManagerView.sass @@ -1,33 +1,49 @@ component display: flex flex-direction: column + row-gap: 1em outline: none + height: 280px + overflow: hidden auto - > div.shortcuts - flex: 0 0 320px - display: flex - flex-direction: column - row-gap: 0.25em - padding: 0 1em 0 0 - overflow: hidden auto + > details.context outline: none + border: none - > div.shortcut + > summary + list-style: none + color: var(--color-blue) display: flex - justify-content: space-between + align-items: center cursor: pointer - color: var(--color-dark) - > span - white-space: nowrap + > h3 + display: inline-block - > hr - margin: 0.5em - height: 1px - border: none - background-color: var(--color-dark) - opacity: 0.1 - flex: 1 + > div.shortcuts + margin-top: 0.5em + display: flex + flex-direction: column + row-gap: 0.25em + padding: 0 1em 0 0 + outline: none - &:hover - color: var(--color-bright) \ No newline at end of file + > div.shortcut + display: flex + justify-content: space-between + cursor: pointer + color: var(--color-dark) + + > span + white-space: nowrap + + > hr + margin: 0.5em + height: 1px + border: none + background-color: var(--color-dark) + opacity: 0.1 + flex: 1 + + &:hover + color: var(--color-bright) \ No newline at end of file diff --git a/packages/app/studio/src/ui/components/ShortcutManagerView.tsx b/packages/app/studio/src/ui/components/ShortcutManagerView.tsx index 101420e6..f532fd39 100644 --- a/packages/app/studio/src/ui/components/ShortcutManagerView.tsx +++ b/packages/app/studio/src/ui/components/ShortcutManagerView.tsx @@ -1,20 +1,23 @@ import css from "./ShortcutManagerView.sass?inline" import {Events, Html, ShortcutKeys} from "@opendaw/lib-dom" -import {DefaultObservableValue, isAbsent, Lifecycle, Objects, Terminator} from "@opendaw/lib-std" +import {DefaultObservableValue, isAbsent, Lifecycle, Notifier, Objects, Terminator} from "@opendaw/lib-std" import {createElement, replaceChildren} from "@opendaw/lib-jsx" -import {GlobalShortcuts} from "@/shortcuts/GlobalShortcuts" -import {ShortcutDefinition, ShortcutDefinitions} from "@/shortcuts/ShortcutValidator" import {Dialogs} from "@/ui/components/dialogs" import {Surface} from "@/ui/surface/Surface" import {Colors} from "@opendaw/studio-enums" +import {ShortcutDefinition} from "@/shortcuts/ShortcutDefinition" +import {ShortcutDefinitions} from "@/shortcuts/ShortcutDefinitions" const className = Html.adoptStyleSheet(css, "ShortcutManagerView") type Construct = { lifecycle: Lifecycle + contexts: Record + updateNotifier: Notifier } -const editShortcut = async (definitions: ShortcutDefinitions, original: ShortcutDefinition): Promise => { +const editShortcut = async (definitions: ShortcutDefinitions, + original: ShortcutDefinition): Promise => { const lifecycle = new Terminator() const abortController = new AbortController() const shortcut = lifecycle.own(new DefaultObservableValue(original.keys)) @@ -56,24 +59,29 @@ const editShortcut = async (definitions: ShortcutDefinitions, original: Shortcut }).then(() => shortcut.getValue(), () => original.keys).finally(() => lifecycle.terminate()) } -export const ShortcutManagerView = ({}: Construct) => { +export const ShortcutManagerView = ({lifecycle, contexts, updateNotifier}: Construct) => { return ( -
-
{ - const update = () => replaceChildren(element, ( - Objects.entries(GlobalShortcuts).map(([key, entry]) => ( -
{ - const keys = await editShortcut(GlobalShortcuts, entry) - GlobalShortcuts[key].keys.overrideWith(keys) - update() - }}>{entry.description} -
- {entry.keys.format()} -
- )) - )) - update() - }}/> +
{ + const update = () => replaceChildren(element, Objects.entries(contexts).map(([name, shortcuts], index) => ( +
+

{name}

+
+ {Objects.entries(shortcuts).map(([key, entry]) => ( +
{ + const keys = await editShortcut(shortcuts, entry) + shortcuts[key].keys.overrideWith(keys) + update() + }}>{entry.description} +
+ {entry.keys.format()} +
+ ))} +
+
+ ))) + lifecycle.own(updateNotifier.subscribe(update)) + update() + }}>
) } \ No newline at end of file diff --git a/packages/lib/dom/src/shortcut-manager.ts b/packages/lib/dom/src/shortcut-manager.ts index dfa89a10..32750999 100644 --- a/packages/lib/dom/src/shortcut-manager.ts +++ b/packages/lib/dom/src/shortcut-manager.ts @@ -2,6 +2,7 @@ import { BinarySearch, Exec, isAbsent, + JSONValue, Lazy, NumberComparator, Option, @@ -111,12 +112,24 @@ export class ShortcutKeys { return new ShortcutKeys(code, modifiers?.ctrl, modifiers?.shift, modifiers?.alt) } + static fromJSON(value: JSONValue): Option { + if (typeof value !== "object" || value === null || Array.isArray(value)) {return Option.None} + const {code, ctrl, shift, alt} = value as Record + if (typeof code !== "string") {return Option.None} + if (typeof ctrl !== "boolean") {return Option.None} + if (typeof shift !== "boolean") {return Option.None} + if (typeof alt !== "boolean") {return Option.None} + return Option.wrap(new ShortcutKeys(code, ctrl, shift, alt)) + } + static fromEvent(event: KeyboardEvent): Option { const code = event.code if (code.startsWith("Shift") || code.startsWith("Control") || code.startsWith("Alt") - || code.startsWith("Meta")) { + || code.startsWith("Meta") + || code === Key.Escape + ) { return Option.None } return Option.wrap(new ShortcutKeys(code, Keyboard.isControlKey(event), event.shiftKey, event.altKey)) @@ -204,6 +217,10 @@ export class ShortcutKeys { this.#alt = keys.#alt } + toJSON(): JSONValue {return {code: this.#code, ctrl: this.#ctrl, shift: this.#shift, alt: this.#alt}} + + copy(): ShortcutKeys {return new ShortcutKeys(this.#code, this.#ctrl, this.#shift, this.#alt)} + toString(): string {return `{ShortcutKeys ${this.format()}}`} }