Proposal: Passing command arguments (view + options)

Summary

I would like to propose:

  1. Having executeCommandById() and listCommands() accept a view and a (possibly null/undefined) options object in addition to the id (with the Obsidian core always passing in at least the id and view)
  2. Passing those arguments through to the underlying command callback or checkCallback
  3. Returning the callback’s return value to the caller (from executeCommandById())
  4. Recommend to plugin developers that commands taking user input (e.g. via dialogs) first query the callback’s options argument to see if the required input has been supplied as a property
  5. Actually expose (at least a portion of) the commands API as part of the official API
  6. (Optional) Allow command metadata to specify what kind of views they apply to, so that plugin authors don’t have to do the view type checking themselves

The first three steps are very minor code changes (less text to code than it took to describe them!) and the sixth is optional, but implementing them would provide the following benefits:

  • Writing macro or automation plugins would become much simpler
  • Plugins can expose an API for other plugins to use, in the form of commands
  • Sidebar views (including notes dragged there) could accept or apply commands, without needing to be “the active leaf”
  • Commands can be based on other commands, and wait for them to finish or fail (e.g. if the callback returns a promise), and/or process some result of the called command (if the command returns or promises one)
  • Reduced duplication of view-retrieval code and race conditions related to changing the active leaf

Background

So, as I’ve been writing plugins for myself, I notice that I spend a lot of time recoding the same basic pattern for dealing with commands that need to work with a particular type of view. While I can push these off into a library (work under way), I notice that a lot of other plugins are doing the same thing… and they’re sometimes doing them in different ways that have slightly different semantics.

A big part of the problem is that commands do not have an explicit target: you just run a command, and then it has to figure out what to apply to. Most target the active leaf, or getActiveViewOfType(). However, in many cases, this will not do the right thing because the workspace item that needs to be targeted is something in a sidebar. (For example, if you drag a note into your sidebar.) Non-main leaves cannot be the “active leaf”, which means that when you try to edit a note in the sidebar, any commands you try to apply will instead apply to the last central leaf.

(In addition, race conditions can occur, whereby during the execution of an async command the active leaf can change.)

The other challenge I’ve been having is the desire to do more automation in Obsidian, but this is hard to do without recoding, because there is no way to pass arguments to commands at the scripting level. Instead of simply calling existing commands, one must re-implement them to not take any user input.

Hence this proposal to solve the above problems, via argument passing.

By passing the view as an argument, command callbacks are decoupled from the workspace state, so that they remain unaffected by changes in the active leaf. They can also be passed other types of views (like calendar, graph, tags, etc.) that aren’t ever the “active leaf”, but which should still be able to obtain focus. (This may become increasingly important as more plugins add new view types to Obsidian.)

Second, by passing an optional “options” argument with command-specific options or parameters, automation and command reuse would be better supported. Commands could get needed information from the supplied options object, rather than querying the user.

So for example, if the “insert template” command checked its options for a template first, it could immediately perform the operation instead of needing to open a dialog. This would allow scripting or macro plugins (or just plugins that don’t want to reinvent wheels) to invoke commands without also having to somehow automate the full UI.

Overall, this would be a very minor change to Obsidian’s core, which has relatively few calls to executeCommandById() and listCommands(), and the changes to return the callback’s result after execution would be similarly minor. (Actually changing any core commands to support options would be more work, but could be done on a rolling/on-demand basis since most core commands that take an input dialog map straightforwardly to existing APIs.)

But just making the argument and passthrough possible would make it easier for plugins to expose API to other plugins, without needing to poke and probe at other plugin objects. Instead, a plugin that wants to use functionality from another command can simply try to invoke it by name. (And maybe receive an error if it’s not installed, prompt the user to install the plugin, etc.)

Regarding the sixth step, while it’s not immediately critical, allowing commands to target a view type using a string, rather than a class, would be important as the plugin economy expands, because plugins will want to target other plugins’ views, not just core views (whose classes they can import). So having a mechanism where you can say,e e.g. “this command applies to calendar:calendar-pane views” would be helpful.

7 Likes

I’ll second this. I have a lot of thoughts around plugins that would allow add/remove tags in metadata. But those are dependent on passing arguments to the command. My thought is to create something that lets you type tag work-in-progress and it does the process of creating the front matter if it’s not there or appending the tag to the tags item.

But again, this is all dependent on arguments.

To do that right now, you would just prompt the user for the argument, presumably using a suggestion dialog for tags. My proposal here is for when plugins need to pass arguments to commands in order to automate something (i.e., when prompting the user isn’t an option or would defeat the purpose.)

1 Like

Got it. That makes sense. In my head, this was more along the lines of passing commands in the same way you do with Alfred or a launcher of sorts.

It will be a great fundamental change. It will make Obsidian really working more like a OS or Emacs.

Room 101: Taking it to Th’emacs (gbracha.blogspot.com)
一种新的操作系统设计 (yinwang.org)

I don’t know it’s prohibited or not, but some code as below can work as a solution:

app.commands.executeCommandById = function executeCommandById(id, t, options) {
  this.app.lastEvent = t || null;
  var n = this.findCommand(id);
  if (!n) return !1;
  try {
    (function(e) {
        console.log(options)
        e.checkCallback ? e.checkCallback(!1, options) : e.callback ? e.callback(options) : console.error("Command " + e + " did not provide a callback")
    })(n);
  } catch (id) {
    return (
      console.log("Command failed to execute: ", n.id), console.error(e), !1
    );
  }
  return !0;
}
this.addCommand({
  id: "open-sample-modal-simple",
  name: "Open sample modal (simple)",
  callback: (options: any) => {
    console.log(111, options);
    new SampleModal(this.app).open();
  },
});
app.commands.executeCommandById('obsidian-sample-plugin:open-sample-modal-simple', function(){}, {name: 123})
2 Likes