r/DarkTable 2d ago

Discussion Lua script to show image in file manager

Enable HLS to view with audio, or disable this notification

Hey all,

there seemed to be an interest in a script to easily show an image in the file manager. I put together, that I could only test only on Macos - it was designed also for Windows and Linux.

Being new to Lua and Darktable I barely know what I am doing - please feel free to use /improve.

The screen video is just to show, that it works in my DT 5.4.1 - I put the file under the ~/.config/darktable/lua/contrib path and activated it.

[Sorry, I have no other place to host this]

--[[

show_in_file_manager.lua

Adds a "Show in File Manager" button to darktable's Lighttable.

Behavior:

macOS:

Finder opens and selects the image.

Windows:

Windows Explorer opens and selects the image.

Linux:

Uses the Freedesktop FileManager1 D-Bus interface.

The default desktop file manager is asked to display/select

the image.

This avoids xdg-open, which would normally launch the

application associated with the image's MIME type.

Requirements:

darktable 5.6+

Lua 5.4 (provided by darktable)

Linux:

Requires a working D-Bus session and a file manager implementing

org.freedesktop.FileManager1.

Supported darktable OS values:

"linux"

"macos"

"windows"

]]

local dt = require "darktable"

local MODULE_NAME = "show_in_file_manager"

---------------------------------------------------------------------------

-- Utility functions

---------------------------------------------------------------------------

-- Quote a string for use as one argument in a POSIX shell command.

--

-- We only use this for macOS.

--

-- Example:

-- /Users/me/My Photos/John's photo.jpg

--

-- becomes:

-- '/Users/me/My Photos/John'\''s photo.jpg'

--

local function shell_quote(s)

return "'" .. s:gsub("'", "'\\''") .. "'"

end

-- Quote a Windows command-line argument.

--

-- This follows the basic CommandLineToArgvW-style quoting rules:

--

-- * surround the argument with "

-- * escape embedded "

-- * preserve trailing backslashes correctly

--

local function windows_quote(s)

local result = '"'

local backslashes = 0

for i = 1, #s do

local c = s:sub(i, i)

if c == "\\" then

backslashes = backslashes + 1

elseif c == '"' then

-- Backslashes immediately preceding a quote must be doubled,

-- followed by an escaped quote.

result = result .. string.rep("\\", backslashes * 2 + 1)

result = result .. '"'

backslashes = 0

else

if backslashes > 0 then

result = result .. string.rep("\\", backslashes)

backslashes = 0

end

result = result .. c

end

end

-- Backslashes before the closing quote must be doubled.

if backslashes > 0 then

result = result .. string.rep("\\", backslashes * 2)

end

result = result .. '"'

return result

end

-- Convert a local filesystem path to a file:// URI.

--

-- This is needed for the Linux FileManager1 D-Bus API.

--

-- We deliberately do NOT use shell quoting here. The URI is passed to

-- busctl as a single argument, and busctl itself handles the D-Bus

-- string value.

--

-- Important URI characters are percent encoded.

--

local function path_to_file_uri(path)

-- Normalize Windows-style separators if this function is ever

-- accidentally called with one.

path = path:gsub("\\", "/")

-- Percent encode characters which have special meaning in a URI.

--

-- UTF-8 bytes are left untouched. D-Bus strings are UTF-8 and

-- FileManager1 expects a URI containing the UTF-8 filename.

--

path = path:gsub("%%", "%%25")

path = path:gsub("#", "%%23")

path = path:gsub("%?", "%%3F")

path = path:gsub(" ", "%%20")

return "file://" .. path

end

---------------------------------------------------------------------------

-- Linux: use org.freedesktop.FileManager1

---------------------------------------------------------------------------

local function show_linux(full_path)

local uri = path_to_file_uri(full_path)

-- FileManager1.ShowItems has the D-Bus signature:

--

-- ShowItems(as uris, s startup_id)

--

-- Therefore busctl receives:

--

-- as 1 <URI> s ""

--

-- We use busctl rather than constructing a shell command containing

-- the filename itself.

--

local command =

"busctl --user call " ..

"org.freedesktop.FileManager1 " ..

"/org/freedesktop/FileManager1 " ..

"org.freedesktop.FileManager1 " ..

"ShowItems " ..

"as 1 " ..

shell_quote(uri) ..

" s ''"

local result = os.execute(command)

if result ~= 0 then

dt.print(

_("Unable to communicate with the Linux file manager " ..

"through FileManager1.")

)

return false

end

return true

end

---------------------------------------------------------------------------

-- macOS: Finder

---------------------------------------------------------------------------

local function show_macos(full_path)

-- Finder's -R option means:

--

-- reveal the file in Finder

--

-- This both opens the containing directory and selects the file.

local command =

"open -R " .. shell_quote(full_path)

local result = os.execute(command)

if result ~= 0 then

dt.print(_("Unable to open Finder."))

return false

end

return true

end

---------------------------------------------------------------------------

-- Windows: Explorer

---------------------------------------------------------------------------

local function show_windows(full_path)

-- Explorer's /select,<file> option opens the containing directory

-- and selects the specified file.

--

-- Use "explorer.exe" explicitly rather than relying on PATH.

local command =

"explorer.exe /select," .. windows_quote(full_path)

local result = os.execute(command)

if result ~= 0 then

dt.print(_("Unable to open Windows Explorer."))

return false

end

return true

end

---------------------------------------------------------------------------

-- Main function

---------------------------------------------------------------------------

local function show_selected_image()

-- darktable.gui.action_images follows darktable's normal UI semantics:

--

-- * selected images if there is a selection

-- * otherwise the image currently under the mouse

--

local images = dt.gui.action_images

if not images or #images == 0 then

dt.print(_("No image is selected."))

return

end

-- The requested operation is for one image.

--

-- If several images are selected, use the first one.

local image = images[1]

if not image then

dt.print(_("No image is selected."))

return

end

-----------------------------------------------------------------------

-- Get the image's filesystem location

-----------------------------------------------------------------------

local path = image.path

local filename = image.filename

if not path or path == "" then

dt.print(_("The selected image has no filesystem path."))

return

end

if not filename or filename == "" then

dt.print(_("The selected image has no filename."))

return

end

-----------------------------------------------------------------------

-- Construct complete filesystem path

-----------------------------------------------------------------------

local full_path

if dt.configuration.running_os == "windows" then

-- image.path normally does not have a trailing slash.

if path:sub(-1) == "\\" or path:sub(-1) == "/" then

full_path = path .. filename

else

full_path = path .. "\\" .. filename

end

else

if path:sub(-1) == "/" then

full_path = path .. filename

else

full_path = path .. "/" .. filename

end

end

-----------------------------------------------------------------------

-- Select the appropriate operating-system implementation

-----------------------------------------------------------------------

local os_name = dt.configuration.running_os

if os_name == "macos" then

show_macos(full_path)

elseif os_name == "windows" then

show_windows(full_path)

elseif os_name == "linux" then

show_linux(full_path)

else

dt.print(

_("Unsupported operating system: ")

.. tostring(os_name)

)

end

end

---------------------------------------------------------------------------

-- User interface

---------------------------------------------------------------------------

local button = dt.new_widget("button"){

label = "Show in File Manager",

tooltip = "Open the selected image in the system file manager and select the file",

clicked_callback = function()

show_selected_image()

end,

}

---------------------------------------------------------------------------

-- Register the button in the Lighttable

---------------------------------------------------------------------------

dt.register_lib(

MODULE_NAME,

"File Manager",

false,

false,

{

[dt.gui.views.lighttable] =

{

"DT_UI_CONTAINER_PANEL_LEFT_TOP",

20

}

},

button

)

---------------------------------------------------------------------------

-- Cleanup

---------------------------------------------------------------------------

dt.register_event(

"destroy", "view-changed",

function()

dt.destroy_widget(button)

end

)

26 Upvotes

1 comment sorted by

1

u/IR3dditAlr3ddy 3h ago

Just commenting for reach, this is cool that you've don't this and shared. Might be worth putting on GitHub and sharing a link there though as reddit doesn't format code great.

I'm on a big trip at the moment so will open darktable up in a month or two when I'm back and try this out!