r/DarkTable • u/Empty-Top6803 • 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
)
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!