import * as CSS from 'csstype' import EventEmitter from 'eventemitter3' import { LSPluginCaller } from './LSPlugin.caller' import { LSPluginExperiments } from './modules/LSPlugin.Experiments' import { IAsyncStorage, LSPluginFileStorage } from './modules/LSPlugin.Storage' import { LSPluginRequest } from './modules/LSPlugin.Request' export type WithOptional = Omit & Partial> export type PluginLocalIdentity = string export type ThemeMode = 'light' | 'dark' export interface LegacyTheme { name: string url: string description?: string mode?: ThemeMode pid: PluginLocalIdentity } export interface Theme extends LegacyTheme { mode: ThemeMode } export type StyleString = string export type StyleOptions = { key?: string style: StyleString } export type UIContainerAttrs = { draggable: boolean resizable: boolean } export type UIBaseOptions = { key?: string replace?: boolean template: string | null style?: CSS.Properties attrs?: Record close?: 'outside' | string reset?: boolean // reset slot content or not } export type UIPathIdentity = { /** * DOM selector */ path: string } export type UISlotIdentity = { /** * Slot key */ slot: string } export type UISlotOptions = UIBaseOptions & UISlotIdentity export type UIPathOptions = UIBaseOptions & UIPathIdentity export type UIOptions = UIBaseOptions | UIPathOptions | UISlotOptions export interface LSPluginPkgConfig { id: PluginLocalIdentity main: string entry: string // alias of main title: string mode: 'shadow' | 'iframe' themes: Theme[] icon: string /** * Alternative entrypoint for development. */ devEntry: unknown /** * For legacy themes, do not use. */ theme: unknown } export interface LSPluginBaseInfo { /** * Must be unique. */ id: string mode: 'shadow' | 'iframe' settings: { disabled: boolean } & Record effect: boolean /** * For internal use only. Indicates if plugin is installed in dot root. */ iir: boolean /** * For internal use only. */ lsr: string } export type IHookEvent = { [key: string]: any } export type IUserOffHook = () => void export type IUserHook = ( callback: (e: IHookEvent & E) => void ) => IUserOffHook export type IUserSlotHook = ( callback: (e: IHookEvent & UISlotIdentity & E) => void ) => void export type IUserConditionSlotHook = ( condition: C, callback: (e: IHookEvent & UISlotIdentity & E) => void ) => void export type EntityID = number export type BlockUUID = string export type BlockUUIDTuple = ['uuid', BlockUUID] export type IEntityID = { id: EntityID; [key: string]: any } export type IBatchBlock = { content: string /** * @NOTE: not supported for DB graph */ properties?: Record children?: Array } export type IDatom = [e: number, a: string, v: any, t: number, added: boolean] export type IGitResult = { stdout: string; stderr: string; exitCode: number } export interface AppUserInfo { [key: string]: any } export interface AppInfo { version: string supportDb: boolean [key: string]: unknown } /** * User's app configurations */ export interface AppUserConfigs { preferredThemeMode: ThemeMode preferredFormat: 'markdown' | 'org' preferredDateFormat: string preferredStartOfWeek: string preferredLanguage: string preferredWorkflow: string currentGraph: string showBracket: boolean enabledFlashcards: boolean enabledJournals: boolean [key: string]: unknown } /** * In Logseq, a graph represents a repository of connected pages and blocks */ export interface AppGraphInfo { name: string url: string path: string [key: string]: unknown } /** * Block - Logseq's fundamental data structure. */ export interface BlockEntity { id: EntityID // db id uuid: BlockUUID order: string format: 'markdown' | 'org' parent: IEntityID title: string content?: string // @deprecated. Use :title instead! page: IEntityID // owner page createdAt: number updatedAt: number ident?: string // ident for property block properties?: Record 'collapsed?': boolean // optional fields in dummy page anchor?: string body?: any children?: Array container?: string file?: IEntityID level?: number meta?: { timestamps: any; properties: any; startPos: number; endPos: number } marker?: string [key: string]: unknown } /** * Page is just a block with some specific properties. */ export interface PageEntity { id: EntityID uuid: BlockUUID name: string format: 'markdown' | 'org' type: 'page' | 'journal' | 'whiteboard' | 'class' | 'property' | 'hidden' updatedAt: number createdAt: number 'journal?': boolean title?: string file?: IEntityID originalName?: string namespace?: IEntityID children?: Array properties?: Record journalDay?: number [key: string]: unknown } export type BlockIdentity = BlockUUID | Pick export type BlockPageName = string export type PageIdentity = BlockPageName | BlockIdentity export type SlashCommandActionCmd = | 'editor/input' | 'editor/hook' | 'editor/clear-current-slash' | 'editor/restore-saved-cursor' export type SlashCommandAction = [cmd: SlashCommandActionCmd, ...args: any] export type SimpleCommandCallback = (e: IHookEvent & E) => void export type BlockCommandCallback = ( e: IHookEvent & { uuid: BlockUUID } ) => Promise export type BlockCursorPosition = { left: number top: number height: number pos: number rect: DOMRect } export type Keybinding = string | Array export type SimpleCommandKeybinding = { mode?: 'global' | 'non-editing' | 'editing' binding: Keybinding mac?: string // special for Mac OS } export type SettingSchemaDesc = { key: string type: 'string' | 'number' | 'boolean' | 'enum' | 'object' | 'heading' default: string | number | boolean | Array | object | null title: string description: string // support markdown inputAs?: 'color' | 'date' | 'datetime-local' | 'range' | 'textarea' enumChoices?: Array enumPicker?: 'select' | 'radio' | 'checkbox' // default: select } export type ExternalCommandType = | 'logseq.command/run' | 'logseq.editor/cycle-todo' | 'logseq.editor/down' | 'logseq.editor/up' | 'logseq.editor/expand-block-children' | 'logseq.editor/collapse-block-children' | 'logseq.editor/open-file-in-default-app' | 'logseq.editor/open-file-in-directory' | 'logseq.editor/select-all-blocks' | 'logseq.editor/toggle-open-blocks' | 'logseq.editor/zoom-in' | 'logseq.editor/zoom-out' | 'logseq.editor/indent' | 'logseq.editor/outdent' | 'logseq.editor/copy' | 'logseq.editor/cut' | 'logseq.go/home' | 'logseq.go/journals' | 'logseq.go/keyboard-shortcuts' | 'logseq.go/next-journal' | 'logseq.go/prev-journal' | 'logseq.go/search' | 'logseq.go/tomorrow' | 'logseq.go/backward' | 'logseq.go/forward' | 'logseq.search/re-index' | 'logseq.sidebar/clear' | 'logseq.sidebar/open-today-page' | 'logseq.ui/goto-plugins' | 'logseq.ui/select-theme-color' | 'logseq.ui/toggle-brackets' | 'logseq.ui/toggle-contents' | 'logseq.ui/toggle-document-mode' | 'logseq.ui/toggle-help' | 'logseq.ui/toggle-left-sidebar' | 'logseq.ui/toggle-right-sidebar' | 'logseq.ui/toggle-settings' | 'logseq.ui/toggle-theme' | 'logseq.ui/toggle-wide-mode' export type UserProxyNSTags = 'app' | 'editor' | 'db' | 'git' | 'ui' | 'assets' | 'utils' export type SearchIndiceInitStatus = boolean export type SearchBlockItem = { id: EntityID uuid: BlockIdentity content: string page: EntityID } export type SearchPageItem = string export type SearchFileItem = string export interface IPluginSearchServiceHooks { name: string options?: Record onQuery: ( graph: string, key: string, opts: Partial<{ limit: number }> ) => Promise<{ graph: string key: string blocks?: Array> pages?: Array files?: Array }> onIndiceInit: (graph: string) => Promise onIndiceReset: (graph: string) => Promise onBlocksChanged: ( graph: string, changes: { added: Array removed: Array } ) => Promise onGraphRemoved: (graph: string, opts?: {}) => Promise } /** * App level APIs */ export interface IAppProxy { /** * @added 0.0.4 * @param key */ getInfo: (key?: keyof AppInfo) => Promise getUserInfo: () => Promise getUserConfigs: () => Promise // services registerSearchService(s: T): void // commands registerCommand: ( type: string, opts: { key: string label: string desc?: string palette?: boolean keybinding?: SimpleCommandKeybinding }, action: SimpleCommandCallback ) => void registerCommandPalette: ( opts: { key: string label: string keybinding?: SimpleCommandKeybinding }, action: SimpleCommandCallback ) => void /** * Supported key names * @link https://gist.github.com/xyhp915/d1a6d151a99f31647a95e59cdfbf4ddc * @param keybinding * @param action */ registerCommandShortcut: ( keybinding: SimpleCommandKeybinding | string, action: SimpleCommandCallback, opts?: Partial<{ key: string label: string desc: string extras: Record }> ) => void /** * Supported all registered palette commands * @param type * @param args */ invokeExternalCommand: ( type: ExternalCommandType, ...args: Array ) => Promise /** * Call external plugin command provided by models or registered commands * @added 0.0.13 * @param type `xx-plugin-id.commands.xx-key`, `xx-plugin-id.models.xx-key` * @param args */ invokeExternalPlugin: (type: string, ...args: Array) => Promise /** * @added 0.0.13 * @param pid */ getExternalPlugin: (pid: string) => Promise<{} | null> /** * Get state from app store * valid state is here * https://github.com/logseq/logseq/blob/master/src/main/frontend/state.cljs#L27 * * @example * ```ts * const isDocMode = await logseq.App.getStateFromStore('document/mode?') * ``` * @param path */ getStateFromStore: (path: string | Array) => Promise setStateFromStore: (path: string | Array, value: any) => Promise // native relaunch: () => Promise quit: () => Promise openExternalLink: (url: string) => Promise /** * @deprecated Using `logseq.Git.execCommand` * @link https://github.com/desktop/dugite/blob/master/docs/api/exec.md * @param args */ execGitCommand: (args: string[]) => Promise // graph getCurrentGraph: () => Promise checkCurrentIsDbGraph: () => Promise getCurrentGraphConfigs: (...keys: string[]) => Promise setCurrentGraphConfigs: (configs: {}) => Promise getCurrentGraphFavorites: () => Promise | null> getCurrentGraphRecent: () => Promise | null> getCurrentGraphTemplates: () => Promise | null> // router pushState: ( k: string, params?: Record, query?: Record ) => void replaceState: ( k: string, params?: Record, query?: Record ) => void // templates getTemplate: (name: string) => Promise existTemplate: (name: string) => Promise createTemplate: ( target: BlockUUID, name: string, opts?: { overwrite: boolean } ) => Promise removeTemplate: (name: string) => Promise insertTemplate: (target: BlockUUID, name: string) => Promise setZoomFactor: (factor: number) => void setFullScreen: (flag: boolean | 'toggle') => void setLeftSidebarVisible: (flag: boolean | 'toggle') => void setRightSidebarVisible: (flag: boolean | 'toggle') => void clearRightSidebarBlocks: (opts?: { close: boolean }) => void registerUIItem: ( type: 'toolbar' | 'pagebar', opts: { key: string; template: string } ) => void registerPageMenuItem: ( tag: string, action: (e: IHookEvent & { page: string }) => void ) => void // hook events onCurrentGraphChanged: IUserHook onGraphAfterIndexed: IUserHook<{ repo: string }> onThemeModeChanged: IUserHook<{ mode: 'dark' | 'light' }> onThemeChanged: IUserHook< Partial<{ name: string; mode: string; pid: string; url: string }>> onTodayJournalCreated: IUserHook<{ title: string }> onBeforeCommandInvoked: (condition: ExternalCommandType | string, callback: (e: IHookEvent) => void) => IUserOffHook onAfterCommandInvoked: (condition: ExternalCommandType | string, callback: (e: IHookEvent) => void) => IUserOffHook /** * provide ui slot to specific block with UUID * * @added 0.0.13 */ onBlockRendererSlotted: IUserConditionSlotHook< BlockUUID, Omit > /** * provide ui slot to block `renderer` macro for `{{renderer arg1, arg2}}` * * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-pomodoro-timer * @example * ```ts * // e.g. {{renderer :h1, hello world, green}} * * logseq.App.onMacroRendererSlotted(({ slot, payload: { arguments } }) => { * let [type, text, color] = arguments * if (type !== ':h1') return * logseq.provideUI({ * key: 'h1-playground', * slot, template: ` *

${text}

* `, * }) * }) * ``` */ onMacroRendererSlotted: IUserSlotHook<{ payload: { arguments: Array; uuid: string; [key: string]: any } }> onPageHeadActionsSlotted: IUserSlotHook onRouteChanged: IUserHook<{ path: string; template: string }> onSidebarVisibleChanged: IUserHook<{ visible: boolean }> // internal _installPluginHook: (pid: string, hook: string, opts?: any) => void _uninstallPluginHook: (pid: string, hookOrAll: string | boolean) => void } /** * Editor related APIs */ export interface IEditorProxy extends Record { /** * register a custom command which will be added to the Logseq slash command list * @param tag - displayed name of command * @param action - can be a single callback function to run when the command is called, or an array of fixed commands with arguments * * * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-slash-commands * * @example * ```ts * logseq.Editor.registerSlashCommand("Say Hi", () => { * console.log('Hi!') * }) * ``` * * @example * ```ts * logseq.Editor.registerSlashCommand("💥 Big Bang", [ * ["editor/hook", "customCallback"], * ["editor/clear-current-slash"], * ]); * ``` */ registerSlashCommand: ( tag: string, action: BlockCommandCallback | Array ) => unknown /** * register a custom command in the block context menu (triggered by right-clicking the block dot) * @param label - displayed name of command * @param action - can be a single callback function to run when the command is called */ registerBlockContextMenuItem: ( label: string, action: BlockCommandCallback ) => unknown /** * Current it's only available for pdf viewer * @param label - displayed name of command * @param action - callback for the clickable item * @param opts - clearSelection: clear highlight selection when callback invoked */ registerHighlightContextMenuItem: ( label: string, action: SimpleCommandCallback, opts?: { clearSelection: boolean } ) => unknown // block related APIs checkEditing: () => Promise insertAtEditingCursor: (content: string) => Promise restoreEditingCursor: () => Promise exitEditingMode: (selectBlock?: boolean) => Promise getEditingCursorPosition: () => Promise getEditingBlockContent: () => Promise getCurrentPage: () => Promise getCurrentBlock: () => Promise getSelectedBlocks: () => Promise | null> clearSelectedBlocks: () => Promise /** * get all blocks of the current page as a tree structure * * @example * ```ts * const blocks = await logseq.Editor.getCurrentPageBlocksTree() * initMindMap(blocks) * ``` */ getCurrentPageBlocksTree: () => Promise> /** * get all blocks for the specified page * * @param srcPage - the page name or uuid */ getPageBlocksTree: (srcPage: PageIdentity) => Promise> /** * get all page/block linked references * @param srcPage */ getPageLinkedReferences: ( srcPage: PageIdentity ) => Promise]> | null> /** * get flatten pages from top namespace * @param namespace */ getPagesFromNamespace: ( namespace: BlockPageName ) => Promise | null> /** * construct pages tree from namespace pages * @param namespace */ getPagesTreeFromNamespace: ( namespace: BlockPageName ) => Promise | null> /** * Create a unique UUID string which can then be assigned to a block. * @added 0.0.8 */ newBlockUUID: () => Promise isPageBlock: (block: BlockEntity | PageEntity) => Boolean /** * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-reddit-hot-news * * @param srcBlock * @param content * @param opts */ insertBlock: ( srcBlock: BlockIdentity, content: string, opts?: Partial<{ before: boolean sibling: boolean isPageBlock: boolean focus: boolean customUUID: string properties: {} }> ) => Promise /** * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-reddit-hot-news * * `keepUUID` will allow you to set a custom UUID for blocks by setting their properties.id */ insertBatchBlock: ( srcBlock: BlockIdentity, batch: IBatchBlock | Array, opts?: Partial<{ before: boolean; sibling: boolean; keepUUID: boolean }> ) => Promise | null> updateBlock: ( srcBlock: BlockIdentity, content: string, opts?: Partial<{ properties: {} }> ) => Promise removeBlock: (srcBlock: BlockIdentity) => Promise getBlock: ( srcBlock: BlockIdentity | EntityID, opts?: Partial<{ includeChildren: boolean }> ) => Promise /** * @example * * ```ts * logseq.Editor.setBlockCollapsed('uuid', true) * logseq.Editor.setBlockCollapsed('uuid', 'toggle') * ``` * @param uuid * @param opts */ setBlockCollapsed: ( uuid: BlockUUID, opts: { flag: boolean | 'toggle' } | boolean | 'toggle' ) => Promise getPage: ( srcPage: PageIdentity | EntityID, opts?: Partial<{ includeChildren: boolean }> ) => Promise createPage: ( pageName: BlockPageName, properties?: {}, opts?: Partial<{ redirect: boolean createFirstBlock: boolean format: BlockEntity['format'] journal: boolean }> ) => Promise createJournalPage: ( date: string | Date ) => Promise deletePage: (pageName: BlockPageName) => Promise renamePage: (oldName: string, newName: string) => Promise getAllPages: (repo?: string) => Promise prependBlockInPage: ( page: PageIdentity, content: string, opts?: Partial<{ properties: {} }> ) => Promise appendBlockInPage: ( page: PageIdentity, content: string, opts?: Partial<{ properties: {} }> ) => Promise getPreviousSiblingBlock: ( srcBlock: BlockIdentity ) => Promise getNextSiblingBlock: ( srcBlock: BlockIdentity ) => Promise moveBlock: ( srcBlock: BlockIdentity, targetBlock: BlockIdentity, opts?: Partial<{ before: boolean; children: boolean }> ) => Promise editBlock: (srcBlock: BlockIdentity, opts?: { pos: number }) => Promise selectBlock: (srcBlock: BlockIdentity) => Promise saveFocusedCodeEditorContent: () => Promise // property entity related APIs (DB only) getProperty: (key: string) => Promise // insert or update property entity upsertProperty: ( key: string, schema?: Partial<{ type: 'default' | 'number' | 'node' | 'date' | 'checkbox' | 'url' | string, cardinality: 'many' | 'one', hide: boolean public: boolean }>, opts?: { name?: string }) => Promise // remove property entity removeProperty: (key: string) => Promise // block property related APIs upsertBlockProperty: ( block: BlockIdentity, key: string, value: any ) => Promise removeBlockProperty: (block: BlockIdentity, key: string) => Promise getBlockProperty: (block: BlockIdentity, key: string) => Promise getBlockProperties: (block: BlockIdentity) => Promise | null> scrollToBlockInPage: ( pageName: BlockPageName, blockId: BlockIdentity, opts?: { replaceState: boolean } ) => void openInRightSidebar: (id: BlockUUID | EntityID) => void /** * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-a-translator */ onInputSelectionEnd: IUserHook<{ caret: any point: { x: number; y: number } start: number end: number text: string }> } /** * Datascript related APIs */ export interface IDBProxy { /** * Run a DSL query * @link https://docs.logseq.com/#/page/queries * @param dsl */ q: (dsl: string) => Promise | null> /** * Run a datascript query */ datascriptQuery: (query: string, ...inputs: Array) => Promise /** * Hook all transaction data of DB * * @added 0.0.2 */ onChanged: IUserHook<{ blocks: Array txData: Array txMeta?: { outlinerOp: string; [key: string]: any } }> /** * Subscribe a specific block changed event * * @added 0.0.2 */ onBlockChanged( uuid: BlockUUID, callback: ( block: BlockEntity, txData: Array, txMeta?: { outlinerOp: string; [key: string]: any } ) => void ): IUserOffHook } /** * Git related APIS */ export interface IGitProxy { /** * @added 0.0.2 * @link https://github.com/desktop/dugite/blob/master/docs/api/exec.md * @param args */ execCommand: (args: string[]) => Promise loadIgnoreFile: () => Promise saveIgnoreFile: (content: string) => Promise } /** * UI related APIs */ export type UIMsgOptions = { key: string timeout: number // milliseconds. `0` indicate that keep showing } export type UIMsgKey = UIMsgOptions['key'] export interface IUIProxy { showMsg: ( content: string, status?: 'success' | 'warning' | 'error' | string, opts?: Partial ) => Promise closeMsg: (key: UIMsgKey) => void queryElementRect: (selector: string) => Promise queryElementById: (id: string) => Promise checkSlotValid: (slot: UISlotIdentity['slot']) => Promise resolveThemeCssPropsVals: (props: string | Array) => Promise | null> } export interface IUtilsProxy { toJs: (obj: {}) => Promise } /** * Assets related APIs */ export interface IAssetsProxy { /** * @added 0.0.2 * @param exts */ listFilesOfCurrentGraph(exts?: string | string[]): Promise< Array<{ path: string size: number accessTime: number modifiedTime: number changeTime: number birthTime: number }> > /** * @example https://github.com/logseq/logseq/pull/6488 * @added 0.0.10 */ makeSandboxStorage(): IAsyncStorage /** * make assets scheme url based on current graph * @added 0.0.15 * @param path */ makeUrl(path: string): Promise /** * try to open asset type file in Logseq app * @added 0.0.16 * @param path */ builtInOpen(path: string): Promise } export interface ILSPluginThemeManager { get themes(): Map registerTheme(id: PluginLocalIdentity, opt: Theme): Promise unregisterTheme(id: PluginLocalIdentity, effect?: boolean): Promise selectTheme( opt: Theme | LegacyTheme, options: { effect?: boolean; emit?: boolean } ): Promise } export type LSPluginUserEvents = 'ui:visible:changed' | 'settings:changed' export interface ILSPluginUser extends EventEmitter { /** * Connection status with the main app */ connected: boolean /** * Duplex message caller */ caller: LSPluginCaller /** * The plugin configurations from package.json */ baseInfo: LSPluginBaseInfo /** * The plugin user settings */ settings?: LSPluginBaseInfo['settings'] /** * The main Logseq app is ready to run the plugin * * @param model - same as the model in `provideModel` */ ready(model?: Record): Promise /** * @param callback - a function to run when the main Logseq app is ready */ ready(callback?: (e: any) => void | {}): Promise ready( model?: Record, callback?: (e: any) => void | {} ): Promise beforeunload: (callback: () => Promise) => void /** * Create a object to hold the methods referenced in `provideUI` * * @example * ```ts * logseq.provideModel({ * openCalendar () { * console.log('Open the calendar!') * } * }) * ``` */ provideModel(model: Record): this /** * Set the theme for the main Logseq app */ provideTheme(theme: Theme): this /** * Inject custom css for the main Logseq app * * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-awesome-fonts * @example * ```ts * logseq.provideStyle(` * @import url("https://at.alicdn.com/t/font_2409735_r7em724douf.css"); * ) * ``` */ provideStyle(style: StyleString | StyleOptions): this /** * Inject custom UI at specific DOM node. * Event handlers can not be passed by string, so you need to create them in `provideModel` * * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-a-translator * @example * ```ts * logseq.provideUI({ * key: 'open-calendar', * path: '#search', * template: ` * * * * ` * }) * ``` */ provideUI(ui: UIOptions): this /** * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-awesome-fonts * * @param schemas */ useSettingsSchema(schemas: Array): this /** * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-awesome-fonts * * @param attrs */ updateSettings(attrs: Record): void onSettingsChanged(cb: (a: T, b: T) => void): IUserOffHook showSettingsUI(): void hideSettingsUI(): void setMainUIAttrs(attrs: Record): void /** * Set the style for the plugin's UI * * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-awesome-fonts * @example * ```ts * logseq.setMainUIInlineStyle({ * position: 'fixed', * zIndex: 11, * }) * ``` */ setMainUIInlineStyle(style: CSS.Properties): void /** * show the plugin's UI */ showMainUI(opts?: { autoFocus: boolean }): void /** * hide the plugin's UI */ hideMainUI(opts?: { restoreEditingCursor: boolean }): void /** * toggle the plugin's UI */ toggleMainUI(): void isMainUIVisible: boolean resolveResourceFullUrl(filePath: string): string App: IAppProxy Editor: IEditorProxy DB: IDBProxy Git: IGitProxy UI: IUIProxy Assets: IAssetsProxy Request: LSPluginRequest FileStorage: LSPluginFileStorage Experiments: LSPluginExperiments }