<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Python GUIs - Desktop-Integration</title><link href="https://www.pythonguis.com/" rel="alternate"/><link href="https://www.pythonguis.com/feeds/desktop-integration.tag.atom.xml" rel="self"/><id>https://www.pythonguis.com/</id><updated>2021-03-25T09:00:00+00:00</updated><subtitle>Create GUI applications with Python and Qt</subtitle><entry><title>Implementing "Open With" Context Menus in PyQt6 — Query your Linux desktop for available applications and let users choose how to open files</title><link href="https://www.pythonguis.com/faq/right-click-open-with/" rel="alternate"/><published>2021-03-25T09:00:00+00:00</published><updated>2021-03-25T09:00:00+00:00</updated><author><name>Martin Fitzpatrick</name></author><id>tag:www.pythonguis.com,2021-03-25:/faq/right-click-open-with/</id><summary type="html">I'm finishing an application that has a custom context menu with "Open" and "Open Containing Folder" working. I can't seem to get an "Open With" option like you see in file explorers. Depending on the file type there would be different options &amp;mdash; so I don't think we're talking about predefined lists, but rather something extracted from the host OS. Any help with "Open With" would be greatly appreciated.</summary><content type="html">
            &lt;blockquote&gt;
&lt;p&gt;I'm finishing an application that has a custom context menu with "Open" and "Open Containing Folder" working. I can't seem to get an "Open With" option like you see in file explorers. Depending on the file type there would be different options &amp;mdash; so I don't think we're talking about predefined lists, but rather something extracted from the host OS. Any help with "Open With" would be greatly appreciated.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;When you right-click a file in a file manager like Nemo or Nautilus, you see a list of applications that can handle that file type. That list comes from the operating system's knowledge of installed applications and their supported MIME types. To recreate this in your own PyQt6 application, you need to query that same information.&lt;/p&gt;
&lt;p&gt;On Linux desktops (GNOME, Cinnamon, XFCE, etc.), this information is managed through the &lt;strong&gt;freedesktop.org&lt;/strong&gt; standards. The &lt;code&gt;gio&lt;/code&gt; module from &lt;strong&gt;PyGObject&lt;/strong&gt; (the Python bindings for GLib/GIO) gives you clean access to all of it &amp;mdash; which applications support a given MIME type, what their names and icons are, and how to launch them.&lt;/p&gt;
&lt;p&gt;Let's walk through building this step by step.&lt;/p&gt;
&lt;h2 id="understanding-the-moving-parts"&gt;Understanding the moving parts&lt;/h2&gt;
&lt;p&gt;There are three things you need to do:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Determine the MIME type&lt;/strong&gt; of a file (e.g., &lt;code&gt;image/png&lt;/code&gt;, &lt;code&gt;text/plain&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Find the applications&lt;/strong&gt; that can open that MIME type.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Launch the chosen application&lt;/strong&gt; with the file as an argument.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The GIO library handles all three. If you're on a Linux desktop with GTK-based tools installed, you almost certainly have &lt;code&gt;gi&lt;/code&gt; (PyGObject) available already. If not, you can install it:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-sh"&gt;sh&lt;/span&gt;
&lt;pre&gt;&lt;code class="sh"&gt;sudo apt install python3-gi gir1.2-gio-2.0
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 id="getting-the-mime-type-for-a-file"&gt;Getting the MIME type for a file&lt;/h2&gt;
&lt;p&gt;GIO can detect the MIME type of a file by inspecting its contents and name:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;from gi.repository import Gio

def get_mime_type(filepath):
    """Return the MIME type string for a given file path."""
    gfile = Gio.File.new_for_path(filepath)
    info = gfile.query_info(
        "standard::content-type", Gio.FileQueryInfoFlags.NONE, None
    )
    return info.get_content_type()
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Calling &lt;code&gt;get_mime_type("/home/user/photo.png")&lt;/code&gt; would return something like &lt;code&gt;"image/png"&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="finding-applications-for-a-mime-type"&gt;Finding applications for a MIME type&lt;/h2&gt;
&lt;p&gt;Once you have the MIME type, you can ask GIO for a list of applications that support it:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;from gi.repository import Gio

def get_apps_for_mime(mime_type):
    """Return a list of Gio.AppInfo objects that can open the given MIME type."""
    return Gio.AppInfo.get_all_for_type(mime_type)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Each &lt;code&gt;Gio.AppInfo&lt;/code&gt; object gives you the application's display name (e.g., "GIMP Image Editor"), its icon, and the ability to launch it with a file.&lt;/p&gt;
&lt;h2 id="launching-an-application-with-a-file"&gt;Launching an application with a file&lt;/h2&gt;
&lt;p&gt;To open a file with a specific application:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;from gi.repository import Gio

def open_file_with_app(app_info, filepath):
    """Launch the given app with the specified file."""
    gfile = Gio.File.new_for_path(filepath)
    app_info.launch([gfile], None)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;That's the complete backend. Now let's wire it into a PyQt6 context menu.&lt;/p&gt;
&lt;h2 id="building-the-context-menu-in-pyqt6"&gt;Building the context menu in PyQt6&lt;/h2&gt;
&lt;p&gt;The idea is straightforward: when the user right-clicks, you build a &lt;code&gt;QMenu&lt;/code&gt; that includes an "Open With" submenu. That submenu is populated dynamically based on the file they clicked on. If you're new to building menus and actions in PyQt6, the &lt;a href="https://www.pythonguis.com/tutorials/pyqt6-actions-toolbars-menus/"&gt;Actions, Toolbars &amp;amp; Menus tutorial&lt;/a&gt; covers the fundamentals.&lt;/p&gt;
&lt;p&gt;Here's a minimal example using a &lt;code&gt;QListWidget&lt;/code&gt; to display some file paths. Right-clicking a file shows the context menu with an "Open With" submenu:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;import sys
import os
import subprocess

from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
    QApplication,
    QListWidget,
    QMainWindow,
    QMenu,
)

from gi.repository import Gio


def get_mime_type(filepath):
    """Return the MIME type string for a given file path."""
    gfile = Gio.File.new_for_path(filepath)
    info = gfile.query_info(
        "standard::content-type", Gio.FileQueryInfoFlags.NONE, None
    )
    return info.get_content_type()


def get_apps_for_mime(mime_type):
    """Return a list of Gio.AppInfo objects for the given MIME type."""
    return Gio.AppInfo.get_all_for_type(mime_type)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Open With Example")
        self.resize(500, 400)

        self.list_widget = QListWidget()
        self.setCentralWidget(self.list_widget)

        # Add some example files &amp;mdash; replace these with real paths on your system.
        example_files = [
            os.path.expanduser("~/Documents"),
            os.path.expanduser("~/Pictures"),
        ]

        # Walk through directories and add files.
        for folder in example_files:
            if os.path.isdir(folder):
                for entry in os.listdir(folder):
                    full_path = os.path.join(folder, entry)
                    if os.path.isfile(full_path):
                        self.list_widget.addItem(full_path)

        self.list_widget.setContextMenuPolicy(
            Qt.ContextMenuPolicy.CustomContextMenu
        )
        self.list_widget.customContextMenuRequested.connect(
            self.show_context_menu
        )

    def show_context_menu(self, position):
        item = self.list_widget.itemAt(position)
        if item is None:
            return

        filepath = item.text()

        menu = QMenu(self)

        # "Open" action &amp;mdash; uses the system default application.
        open_action = menu.addAction("Open")
        open_action.triggered.connect(
            lambda: self.open_default(filepath)
        )

        # "Open Containing Folder" action.
        open_folder_action = menu.addAction("Open Containing Folder")
        open_folder_action.triggered.connect(
            lambda: self.open_containing_folder(filepath)
        )

        # "Open With" submenu.
        open_with_menu = menu.addMenu("Open With...")
        self.populate_open_with_menu(open_with_menu, filepath)

        menu.exec(self.list_widget.mapToGlobal(position))

    def populate_open_with_menu(self, submenu, filepath):
        """Fill the 'Open With' submenu with available applications."""
        try:
            mime_type = get_mime_type(filepath)
        except Exception:
            no_apps = submenu.addAction("(unable to detect file type)")
            no_apps.setEnabled(False)
            return

        apps = get_apps_for_mime(mime_type)

        if not apps:
            no_apps = submenu.addAction("(no applications found)")
            no_apps.setEnabled(False)
            return

        for app_info in apps:
            app_name = app_info.get_display_name()
            action = submenu.addAction(app_name)
            # Use a default argument in the lambda to capture the current
            # app_info, avoiding the common closure-in-a-loop issue.
            action.triggered.connect(
                lambda checked, ai=app_info: self.open_with_app(
                    ai, filepath
                )
            )

    def open_default(self, filepath):
        """Open the file with the system default application."""
        subprocess.Popen(["xdg-open", filepath])

    def open_containing_folder(self, filepath):
        """Open the folder containing the file."""
        folder = os.path.dirname(filepath)
        subprocess.Popen(["xdg-open", folder])

    def open_with_app(self, app_info, filepath):
        """Launch a specific application with the given file."""
        gfile = Gio.File.new_for_path(filepath)
        try:
            app_info.launch([gfile], None)
        except Exception as e:
            print(f"Failed to launch {app_info.get_display_name()}: {e}")


app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;You can copy this, update the &lt;code&gt;example_files&lt;/code&gt; list with folders that exist on your system, and run it. Right-clicking any file in the list will show a context menu with the full "Open With..." submenu populated from your installed applications.&lt;/p&gt;
&lt;h2 id="how-the-lambda-capture-works"&gt;How the lambda capture works&lt;/h2&gt;
&lt;p&gt;You might have noticed this pattern in the loop:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;action.triggered.connect(
    lambda checked, ai=app_info: self.open_with_app(ai, filepath)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;ai=app_info&lt;/code&gt; part is a default argument that captures the &lt;em&gt;current&lt;/em&gt; value of &lt;code&gt;app_info&lt;/code&gt; at the time the lambda is created. Without it, every menu action would end up using the &lt;em&gt;last&lt;/em&gt; value of &lt;code&gt;app_info&lt;/code&gt; from the loop &amp;mdash; a common and frustrating Python gotcha when connecting signals inside loops. For more on how signals and slots work in PyQt6, including lambda connections, see &lt;a href="https://www.pythonguis.com/tutorials/pyqt6-signals-slots-events/"&gt;Signals, Slots &amp;amp; Events&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="adding-application-icons"&gt;Adding application icons&lt;/h2&gt;
&lt;p&gt;You can make the submenu look more polished by including application icons. GIO provides icon information through &lt;code&gt;app_info.get_icon()&lt;/code&gt;, and you can resolve this to a file path using the system icon theme. Here's an updated version of the &lt;code&gt;populate_open_with_menu&lt;/code&gt; method:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;from PyQt6.QtGui import QIcon


def populate_open_with_menu(self, submenu, filepath):
    """Fill the 'Open With' submenu with available applications."""
    try:
        mime_type = get_mime_type(filepath)
    except Exception:
        action = submenu.addAction("(unable to detect file type)")
        action.setEnabled(False)
        return

    apps = get_apps_for_mime(mime_type)

    if not apps:
        action = submenu.addAction("(no applications found)")
        action.setEnabled(False)
        return

    for app_info in apps:
        app_name = app_info.get_display_name()
        icon = app_info.get_icon()

        action = submenu.addAction(app_name)

        # Try to load the icon from the system icon theme.
        if icon is not None and hasattr(icon, "get_names"):
            for icon_name in icon.get_names():
                qt_icon = QIcon.fromTheme(icon_name)
                if not qt_icon.isNull():
                    action.setIcon(qt_icon)
                    break

        action.triggered.connect(
            lambda checked, ai=app_info: self.open_with_app(
                ai, filepath
            )
        )
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;&lt;code&gt;QIcon.fromTheme()&lt;/code&gt; looks up icons from the system's icon theme (the same one your desktop environment uses), so the icons in your submenu will match what users see in their native file manager.&lt;/p&gt;
&lt;h2 id="a-note-on-cross-platform-compatibility"&gt;A note on cross-platform compatibility&lt;/h2&gt;
&lt;p&gt;This approach relies on GIO, which is a GLib/GNOME technology. It works well on Linux desktops that follow freedesktop.org standards, which includes Linux Mint, Ubuntu, Fedora, and most others.&lt;/p&gt;
&lt;p&gt;If you need cross-platform support:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;On macOS&lt;/strong&gt;, you can use &lt;code&gt;subprocess.Popen(["open", "-a", app_name, filepath])&lt;/code&gt; and query available apps using &lt;code&gt;NSWorkspace&lt;/code&gt; via PyObjC.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On Windows&lt;/strong&gt;, you can use the &lt;code&gt;SHAssocEnumHandlers&lt;/code&gt; COM API or query the registry, though this is more involved.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For a Linux-focused application, GIO is the natural and reliable choice. When you're ready to distribute your finished application, take a look at &lt;a href="https://www.pythonguis.com/tutorials/packaging-pyqt5-applications-linux-pyinstaller/"&gt;packaging PyQt6 apps for Linux with PyInstaller&lt;/a&gt; to create standalone executables.&lt;/p&gt;
&lt;h2 id="complete-working-example"&gt;Complete working example&lt;/h2&gt;
&lt;p&gt;Here is the full application with icons included. If you need help setting up your PyQt6 development environment first, see the &lt;a href="https://www.pythonguis.com/installation/install-pyqt6-linux/"&gt;PyQt6 installation guide for Linux&lt;/a&gt;.&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;import sys
import os
import subprocess

from PyQt6.QtCore import Qt
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import (
    QApplication,
    QListWidget,
    QMainWindow,
    QMenu,
)

from gi.repository import Gio


def get_mime_type(filepath):
    """Return the MIME type string for a given file path."""
    gfile = Gio.File.new_for_path(filepath)
    info = gfile.query_info(
        "standard::content-type", Gio.FileQueryInfoFlags.NONE, None
    )
    return info.get_content_type()


def get_apps_for_mime(mime_type):
    """Return a list of Gio.AppInfo objects for the given MIME type."""
    return Gio.AppInfo.get_all_for_type(mime_type)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Open With Example")
        self.resize(500, 400)

        self.list_widget = QListWidget()
        self.setCentralWidget(self.list_widget)

        # Add files from common directories. Update these paths to
        # match your system.
        search_dirs = [
            os.path.expanduser("~/Documents"),
            os.path.expanduser("~/Pictures"),
            os.path.expanduser("~/Downloads"),
        ]

        for folder in search_dirs:
            if os.path.isdir(folder):
                for entry in sorted(os.listdir(folder)):
                    full_path = os.path.join(folder, entry)
                    if os.path.isfile(full_path):
                        self.list_widget.addItem(full_path)

        self.list_widget.setContextMenuPolicy(
            Qt.ContextMenuPolicy.CustomContextMenu
        )
        self.list_widget.customContextMenuRequested.connect(
            self.show_context_menu
        )

    def show_context_menu(self, position):
        item = self.list_widget.itemAt(position)
        if item is None:
            return

        filepath = item.text()
        menu = QMenu(self)

        # Open with default application.
        open_action = menu.addAction("Open")
        open_action.triggered.connect(
            lambda: self.open_default(filepath)
        )

        # Open containing folder.
        open_folder_action = menu.addAction("Open Containing Folder")
        open_folder_action.triggered.connect(
            lambda: self.open_containing_folder(filepath)
        )

        menu.addSeparator()

        # Open With submenu.
        open_with_menu = menu.addMenu("Open With...")
        self.populate_open_with_menu(open_with_menu, filepath)

        menu.exec(self.list_widget.mapToGlobal(position))

    def populate_open_with_menu(self, submenu, filepath):
        """Fill the 'Open With' submenu with available applications."""
        try:
            mime_type = get_mime_type(filepath)
        except Exception:
            action = submenu.addAction("(unable to detect file type)")
            action.setEnabled(False)
            return

        apps = get_apps_for_mime(mime_type)

        if not apps:
            action = submenu.addAction("(no applications found)")
            action.setEnabled(False)
            return

        for app_info in apps:
            app_name = app_info.get_display_name()
            icon = app_info.get_icon()

            action = submenu.addAction(app_name)

            # Try to set the application's icon from the system theme.
            if icon is not None and hasattr(icon, "get_names"):
                for icon_name in icon.get_names():
                    qt_icon = QIcon.fromTheme(icon_name)
                    if not qt_icon.isNull():
                        action.setIcon(qt_icon)
                        break

            action.triggered.connect(
                lambda checked, ai=app_info: self.open_with_app(
                    ai, filepath
                )
            )

    def open_default(self, filepath):
        """Open the file using the system default application."""
        subprocess.Popen(["xdg-open", filepath])

    def open_containing_folder(self, filepath):
        """Open the directory that contains the file."""
        folder = os.path.dirname(filepath)
        subprocess.Popen(["xdg-open", folder])

    def open_with_app(self, app_info, filepath):
        """Launch a specific application to open the given file."""
        gfile = Gio.File.new_for_path(filepath)
        try:
            app_info.launch([gfile], None)
        except Exception as e:
            print(f"Failed to launch {app_info.get_display_name()}: {e}")


app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Run this application, and you'll see a list of files from your home directories. Right-click any file and you'll get a context menu with "Open", "Open Containing Folder", and an "Open With..." submenu listing every application your system knows can handle that file type &amp;mdash; just like a native file manager.&lt;/p&gt;
            &lt;p&gt;For an in-depth guide to building Python GUIs with PyQt6 see my book, &lt;a href="https://www.martinfitzpatrick.com/pyqt6-book/"&gt;Create GUI Applications with Python &amp; Qt6.&lt;/a&gt;&lt;/p&gt;
            </content><category term="PyQt6"/><category term="QMenu"/><category term="Context-Menu"/><category term="Linux"/><category term="Desktop-Integration"/></entry></feed>