I’ve recently been working on a plugin system for Explorer++. It’s still in development, but this post describes some of the goals of the system and the current functionality that’s offered.

Goals

Simplicity

Explorer++ itself is written in C++. Having a plugin system that also uses C++ would be possible, but C++ brings in a lot of complexity, making it harder to write plugins. Using a simple, higher-level language means that plugin developers don’t have to worry about memory management or object lifetimes.

To that end, I’ve chosen Lua as the current scripting language. However, given the popularity of JavaScript, I think it might be a better option. I’ve been experimenting with the process of embedding a JavaScript engine in Explorer++ and may ultimately choose to go in that direction.

Familiarity

The current set of API methods have been explicitly designed to be similar to the extension API methods offered by Chrome. This can be clearly seen in the current set of tab API methods. While a lot of the API methods that Chrome provides are specific to a web browser, the tab management methods tend to apply equally well to Explorer++ and the methods that have been implemented so far look like the methods used in Chrome.

Capability

The plugin system should offer enough power for developers to create non-trivial plugins. Enough of the internal state of the application should be exposed to allow you to do things that aren’t possible through the user interface.

Current API methods

Note that, as mentioned above, the plugin system is still in development, so at least some of these API methods are likely to change in the future. Proper API documentation will be added at a later date.

Tabs

tabs.getAll()

Retrieves a list of all open tabs.

tabList = tabs.getAll()

for key = 1, #tabList do
    tab = tabList[key]

    -- The following properties are available:
    -- tab.id
    -- tab.location
    -- tab.name
    -- tab.viewMode
    -- tab.locked
    -- tab.addressLocked
end

tabs.get(tabId)

Retrieves information about an individual tab, specified by its tab ID.

-- The tab information available here is the same as the information
-- made available by tabs.getAll().
tab = tabs.get(1)

tabs.create(path)

Creates a new tab, opened to the specified path.

tabs.create("C:\\")

tabs.move(tabId, newIndex)

Moves the tab (specified by tabId) to a new position

-- The final position of the tab may not by the index you pass in. For
-- example, if the index is greater than the number of open tabs, then
-- the final position will be less than whatever value you pass in.
updatedPosition = tabs.move(1, 4)

tabs.close(tabId)

Closes a tab (specified by tabId).

tabs.close(1)

tabs.onCreated.addListener(callback)

Adds a tab creation listener.

listenerId = tabs.onCreated.addListener(function (tab)
    -- The tab information available here is the same as the
    -- information made available by tabs.getAll().
    end
)

tabs.onCreated.removeListener(listenerId)

Removes the specified tab creation listener.

tabs.onCreated.removeListener(listenerId)

Adds a menu entry to the main menu (i.e. the menu that appears at the top of the window). callback will be invoked when the menu is clicked.

menuItemId = menu.create("New menu item", function ()
    -- The menu was clicked. Take some action.
    end
)

Removes a menu entry added by menu.create.

menu.remove(menuItemId)

UI

ui.setListViewColors(backgroundColor, textColor)

Sets the background color and text color for the listview.

ui.setListViewColors("#1E1E1E", "#FFFFFF")

ui.setTreeViewColors(backgroundColor, textColor)

Sets the background color and text color for the treeview.

ui.setTreeViewColors("#1E1E1E", "#FFFFFF")

Sample plugin

Every plugin needs to have at least two files:

  1. A manifest file (plugin.json) that describes the plugin.
  2. A lua file that contains the plugin code.

Here are the necessary files for a plugin that allows tabs to be sorted by name:

plugin.json

{
  "name": "Sort tabs by name",
  "description": "Adds a menu entry that sorts tabs by name when clicked",
  "file": "sort_tabs_by_name.lua",
  "version": "1.0",
  "std_libs_required": ["table"],
  "author": "David Erceg"
}

sort_tabs_by_name.lua

menu.create("Sort tabs", function ()
    sortTabs()
  end
)

-- Retrieves all current tabs in Explorer++ and then sorts them by name.
function sortTabs()
  tabList = tabs.getAll()

  local tabsTable = {}

  -- Build a table containing the list of tabs to allow them to be easily
  -- sorted.
  for key = 1, #tabList do
    tab = tabList[key]
    
    table.insert(tabsTable, { id = tab.id, name = tab.name })
  end

  table.sort(tabsTable, function (tab1, tab2)
      return tab1.name < tab2.name
      end
  )

  for key = 1, #tabsTable do
    tab = tabsTable[key]
    
    -- Move the tab into its sorted position.
    tabs.move(tab.id, key - 1)
  end
end

This plugin adds a menu entry (“Sort tabs”) to the main menu. Once clicked, the plugin will sort all open tabs by name. You can see the source code for this plugin, plus the source code of other plugins in the repository.

Testing the functionality

The plugin functionality is disabled by default. To enable it, first grab a recent version of Explorer++ from https://ci.appveyor.com/project/derceg/explorerplusplus. Next, start Explorer++ with plugin support enabled:

explorer++.exe -enable_plugins

If you create a folder named “plugins” in the same directory as the executable, any plugins you create there will be loaded when Explorer++ starts.

You can also interactively run scripting commands using a new dialog that’s been added. This dialog can be opened by navigating to Tools > Run Script…

Pasting any of the commands shown above should work. For example, assuming you know that there’s a tab with ID 1, you could run the following commands:

tab = tabs.get(1)
return tostring(tab)

This would result in an output like the following:

id = 1, location = C:\, name = Local Disk (C:), viewMode = 1, locked = 0, addressLocked = 0

Scripting dialog results

Debugging

It is possible to debug Lua scripts, though doing so currently requires tweaking the Explorer++ build process (to build Lua as a DLL, rather than a static library). Details on how to do that will be given in a future post.

Future work

There are some further API methods that are planned or in progress:

  • Accelerator binding. This would allow a plugin to register a keyboard accelerator (e.g. “Alt+Shift+S”). The functionality required to do this is mostly done. In future, this could be expanded to allow plugins to redefine existing accelerators.

  • Ability for plugins to create menu entries on other menus (e.g. the right-click), rather than just the main menu.

  • Ability for plugins to create top-level menu items. Could be used by a plugin to create a menu that shows a list of frequently accessed folders, for example.

  • Further additions to the tabs API - e.g. the ability to update tabs, change view modes, lock/unlock tabs, etc.

  • Ability for plugins to load/save data. May not be strictly necessary, as a plugin could use the standard I/O library in Lua to load and save data itself.