├── .cirrus.yml ├── .clang-format ├── .github └── workflows │ └── ubuntu.yml ├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── man └── launch.1.md ├── src ├── AppDiscovery.cpp ├── AppDiscovery.h ├── ApplicationInfo.cpp ├── ApplicationInfo.h ├── ApplicationSelectionDialog.cpp ├── ApplicationSelectionDialog.h ├── ApplicationSelectionDialog.ui ├── DbManager.cpp ├── DbManager.h ├── Executable.cpp ├── Executable.h ├── bundle-thumbnailer.cpp ├── extattrs.cpp ├── extattrs.h ├── launch.cpp ├── launcher.cpp └── launcher.h └── tests ├── CMakeLists.txt ├── CTestTestfile.cmake ├── errortest.py └── testExecutable.cpp /.cirrus.yml: -------------------------------------------------------------------------------- 1 | freebsd_instance: 2 | image: freebsd-13-1-release-amd64 3 | 4 | env: 5 | CIRRUS_CLONE_DEPTH: 1 6 | GITHUB_TOKEN: ENCRYPTED[5658d2d703118b074accb59d28381d46e41ab09651434f8b85c64951b69d49c966a83f3206123070f56ab78f6fc84aef] 7 | 8 | task: 9 | # This name gets reported as a build status in GitHub 10 | name: freebsd-12-1-release-amd64 11 | auto_cancellation: false 12 | stateful: false 13 | setup_script: 14 | - pkg install -y curl wget zip pkgconf cmake qt5-qmake qt5-widgets qt5-buildtools kf5-kwindowsystem qt5-testlib 15 | test_script: 16 | - mkdir build ; cd build 17 | - cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr 18 | - make -j$(sysctl -n hw.ncpu) 19 | - zip --symlinks -r launch_FreeBSD.zip launch open bundle-thumbnailer 20 | - case "$CIRRUS_BRANCH" in *pull/*) echo "Skipping since it's a pull request" ;; * ) wget https://github.com/tcnksm/ghr/files/5247714/ghr.zip ; unzip ghr.zip ; rm ghr.zip ; fetch https://github.com/probonopd/continuous-release-manager/releases/download/continuous/continuous-release-manager-freebsd && chmod +x continuous-release-manager-freebsd && ./continuous-release-manager-freebsd && ./ghr -replace -t "${GITHUB_TOKEN}" -u "${CIRRUS_REPO_OWNER}" -r "${CIRRUS_REPO_NAME}" -c "${CIRRUS_CHANGE_IN_REPO}" continuous "${CIRRUS_WORKING_DIR}"/build/*zip ; esac 21 | only_if: $CIRRUS_TAG != 'continuous' 22 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | # https://code.qt.io/cgit/qt/qt5.git/plain/_clang-format 2 | # Copyright (C) 2016 Olivier Goffart 3 | # 4 | # You may use this file under the terms of the 3-clause BSD license. 5 | # See the file LICENSE from this package for details. 6 | 7 | # This is the clang-format configuration style to be used by Qt, 8 | # based on the rules from https://wiki.qt.io/Qt_Coding_Style and 9 | # https://wiki.qt.io/Coding_Conventions 10 | 11 | --- 12 | # Webkit style was loosely based on the Qt style 13 | BasedOnStyle: WebKit 14 | 15 | Standard: c++17 16 | 17 | # Column width is limited to 100 in accordance with Qt Coding Style. 18 | # https://wiki.qt.io/Qt_Coding_Style 19 | # Note that this may be changed at some point in the future. 20 | ColumnLimit: 100 21 | # How much weight do extra characters after the line length limit have. 22 | # PenaltyExcessCharacter: 4 23 | 24 | # Disable reflow of some specific comments 25 | # qdoc comments: indentation rules are different. 26 | # Translation comments and SPDX license identifiers are also excluded. 27 | CommentPragmas: "^!|^:|^ SPDX-License-Identifier:" 28 | 29 | # We want a space between the type and the star for pointer types. 30 | PointerBindsToType: false 31 | 32 | # We use template< without space. 33 | SpaceAfterTemplateKeyword: false 34 | 35 | # We want to break before the operators, but not before a '='. 36 | BreakBeforeBinaryOperators: NonAssignment 37 | 38 | # Braces are usually attached, but not after functions or class declarations. 39 | BreakBeforeBraces: Custom 40 | BraceWrapping: 41 | AfterClass: true 42 | AfterControlStatement: false 43 | AfterEnum: false 44 | AfterFunction: true 45 | AfterNamespace: false 46 | AfterObjCDeclaration: false 47 | AfterStruct: true 48 | AfterUnion: false 49 | BeforeCatch: false 50 | BeforeElse: false 51 | IndentBraces: false 52 | 53 | # When constructor initializers do not fit on one line, put them each on a new line. 54 | ConstructorInitializerAllOnOneLineOrOnePerLine: true 55 | # Indent initializers by 4 spaces 56 | ConstructorInitializerIndentWidth: 4 57 | 58 | # Indent width for line continuations. 59 | ContinuationIndentWidth: 8 60 | 61 | # No indentation for namespaces. 62 | NamespaceIndentation: None 63 | 64 | # Allow indentation for preprocessing directives (if/ifdef/endif). https://reviews.llvm.org/rL312125 65 | IndentPPDirectives: AfterHash 66 | # We only indent with 2 spaces for preprocessor directives 67 | PPIndentWidth: 2 68 | 69 | # Horizontally align arguments after an open bracket. 70 | # The coding style does not specify the following, but this is what gives 71 | # results closest to the existing code. 72 | AlignAfterOpenBracket: true 73 | AlwaysBreakTemplateDeclarations: true 74 | 75 | # Ideally we should also allow less short function in a single line, but 76 | # clang-format does not handle that. 77 | AllowShortFunctionsOnASingleLine: Inline 78 | 79 | # The coding style specifies some include order categories, but also tells to 80 | # separate categories with an empty line. It does not specify the order within 81 | # the categories. Since the SortInclude feature of clang-format does not 82 | # re-order includes separated by empty lines, the feature is not used. 83 | SortIncludes: false 84 | 85 | # macros for which the opening brace stays attached. 86 | ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH, forever, Q_FOREVER, QBENCHMARK, QBENCHMARK_ONCE ] 87 | 88 | # Break constructor initializers before the colon and after the commas. 89 | BreakConstructorInitializers: BeforeColon 90 | 91 | # Add "// namespace " comments on closing brace for a namespace 92 | # Ignored for namespaces that qualify as a short namespace, 93 | # see 'ShortNamespaceLines' 94 | FixNamespaceComments: true 95 | 96 | # Definition of how short a short namespace is, default 1 97 | ShortNamespaceLines: 1 98 | 99 | # When escaping newlines in a macro attach the '\' as far left as possible, e.g. 100 | ##define a \ 101 | # something; \ 102 | # other; \ 103 | # thelastlineislong; 104 | AlignEscapedNewlines: Left 105 | 106 | # Avoids the addition of a space between an identifier and the 107 | # initializer list in list-initialization. 108 | SpaceBeforeCpp11BracedList: false 109 | -------------------------------------------------------------------------------- /.github/workflows/ubuntu.yml: -------------------------------------------------------------------------------- 1 | name: Build for Ubuntu 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | build_ubuntu: 11 | runs-on: ubuntu-22.04 # TODO: Make build on 20.04 Qt 5.12 12 | 13 | steps: 14 | - name: Check out code 15 | uses: actions/checkout@v2 16 | 17 | - name: Install dependencies for Ubuntu 18 | run: | 19 | sudo apt-get update 20 | sudo apt-get install -y git curl wget zip cmake pkgconf libqt5widgets5 qttools5-dev libkf5windowsystem-dev 21 | 22 | - name: Build and package for Ubuntu 23 | run: | 24 | git submodule update --init --recursive 25 | mkdir build ; cd build 26 | cmake .. -DCMAKE_BUILD_TYPE=Release 27 | make -j$(nproc) 28 | make DESTDIR=out install 29 | find out/ 30 | cd out/ && zip --symlinks -r ../launch_Ubuntu.zip . && cd - 31 | 32 | - name: Upload Ubuntu artifact 33 | uses: actions/upload-artifact@v2 34 | with: 35 | name: launch-artifact-ubuntu 36 | path: build/launch_Ubuntu.zip 37 | 38 | - name: Create GitHub Release using Continuous Release Manager 39 | if: github.event_name == 'push' # Only run for push events, not pull requests 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | run: | 43 | curl -L -o continuous-release-manager-linux https://github.com/probonopd/continuous-release-manager/releases/download/continuous/continuous-release-manager-linux 44 | chmod +x continuous-release-manager-linux 45 | ./continuous-release-manager-linux 46 | RELEASE_ID=$(./continuous-release-manager-linux) 47 | echo "RELEASE_ID=${RELEASE_ID}" >> $GITHUB_ENV 48 | 49 | - name: Upload to GitHub Release 50 | if: github.event_name == 'push' # Only run for push events, not pull requests 51 | uses: xresloader/upload-to-github-release@v1.3.12 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | with: 55 | file: "build/*zip" 56 | draft: false 57 | verbose: true 58 | branches: main 59 | tag_name: continuous 60 | release_id: ${{ env.RELEASE_ID }} 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # C++ objects and libs 2 | *.slo 3 | *.lo 4 | *.o 5 | *.a 6 | *.la 7 | *.lai 8 | *.so 9 | *.so.* 10 | *.dll 11 | *.dylib 12 | 13 | # Qt-es 14 | object_script.*.Release 15 | object_script.*.Debug 16 | *_plugin_import.cpp 17 | /.qmake.cache 18 | /.qmake.stash 19 | *.pro.user 20 | *.pro.user.* 21 | *.qbs.user 22 | *.qbs.user.* 23 | *.moc 24 | moc_*.cpp 25 | moc_*.h 26 | qrc_*.cpp 27 | ui_*.h 28 | *.qmlc 29 | *.jsc 30 | Makefile* 31 | *build-* 32 | *.qm 33 | *.prl 34 | 35 | # Qt unit tests 36 | target_wrapper.* 37 | 38 | # QtCreator 39 | *.autosave 40 | 41 | # QtCreator Qml 42 | *.qmlproject.user 43 | *.qmlproject.user.* 44 | 45 | # QtCreator CMake 46 | CMakeLists.txt.user* 47 | 48 | # QtCreator 4.8< compilation database 49 | compile_commands.json 50 | 51 | # QtCreator local machine specific files for imported projects 52 | *creator.user* 53 | 54 | build/* 55 | 56 | .idea 57 | .vscode 58 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # To build without debug output, run: 2 | # cmake .. -DCMAKE_BUILD_TYPE=Release 3 | 4 | cmake_minimum_required(VERSION 3.5) 5 | 6 | # Add the tests subdirectory 7 | add_subdirectory(tests) 8 | 9 | # Do not print deprecated warnings for Qt5 or KF5 10 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-deprecated-declarations") 11 | 12 | project(launch LANGUAGES CXX) 13 | 14 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 15 | 16 | set(CMAKE_AUTOUIC ON) 17 | set(CMAKE_AUTOMOC ON) 18 | set(CMAKE_AUTORCC ON) 19 | 20 | set(CMAKE_CXX_STANDARD 17) 21 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 22 | 23 | # TODO: Make everything compile under Qt6 24 | find_package(QT NAMES Qt5 REQUIRED COMPONENTS Widgets DBus Core) 25 | find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets DBus Core) 26 | find_package(KF5WindowSystem REQUIRED) 27 | 28 | # Do not put qDebug() into Release builds 29 | if(NOT CMAKE_BUILD_TYPE STREQUAL Debug) 30 | add_definitions(-DQT_NO_DEBUG_OUTPUT) 31 | endif() 32 | 33 | # Set rpath 34 | set(CMAKE_INSTALL_RPATH $ORIGIN/../lib) 35 | 36 | 37 | # FIXME: Instead of building the same source code three times 38 | # under different names, find a way to install symlinks to the 39 | # 'launch' binary in different paths 40 | add_executable(launch 41 | src/launch.cpp 42 | src/DbManager.h 43 | src/DbManager.cpp 44 | src/ApplicationInfo.h 45 | src/ApplicationInfo.cpp 46 | src/AppDiscovery.h 47 | src/AppDiscovery.cpp 48 | src/extattrs.h 49 | src/extattrs.cpp 50 | src/launcher.h 51 | src/launcher.cpp 52 | src/ApplicationSelectionDialog.h 53 | src/ApplicationSelectionDialog.cpp 54 | src/ApplicationSelectionDialog.ui 55 | src/Executable.cpp 56 | src/Executable.h 57 | ) 58 | 59 | add_executable(open 60 | src/launch.cpp 61 | src/DbManager.h 62 | src/DbManager.cpp 63 | src/ApplicationInfo.h 64 | src/ApplicationInfo.cpp 65 | src/AppDiscovery.h 66 | src/AppDiscovery.cpp 67 | src/extattrs.h 68 | src/extattrs.cpp 69 | src/launcher.h 70 | src/launcher.cpp 71 | src/ApplicationSelectionDialog.h 72 | src/ApplicationSelectionDialog.cpp 73 | src/ApplicationSelectionDialog.ui 74 | src/Executable.cpp 75 | src/Executable.h 76 | ) 77 | 78 | add_executable(xdg-open 79 | src/launch.cpp 80 | src/DbManager.h 81 | src/DbManager.cpp 82 | src/ApplicationInfo.h 83 | src/ApplicationInfo.cpp 84 | src/AppDiscovery.h 85 | src/AppDiscovery.cpp 86 | src/extattrs.h 87 | src/extattrs.cpp 88 | src/launcher.h 89 | src/launcher.cpp 90 | src/ApplicationSelectionDialog.h 91 | src/ApplicationSelectionDialog.cpp 92 | src/ApplicationSelectionDialog.ui 93 | src/Executable.cpp 94 | src/Executable.h 95 | ) 96 | 97 | add_executable(bundle-thumbnailer 98 | src/bundle-thumbnailer.cpp 99 | src/DbManager.h 100 | src/DbManager.cpp 101 | src/extattrs.h 102 | src/extattrs.cpp 103 | ) 104 | 105 | if (CMAKE_SYSTEM_NAME MATCHES "FreeBSD") 106 | target_link_libraries(launch Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::DBus KF5::WindowSystem procstat) 107 | target_link_libraries(open Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::DBus KF5::WindowSystem procstat) 108 | target_link_libraries(xdg-open Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::DBus KF5::WindowSystem procstat) 109 | endif() 110 | 111 | if (CMAKE_SYSTEM_NAME MATCHES "Linux") 112 | target_link_libraries(launch Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::DBus KF5::WindowSystem) 113 | target_link_libraries(open Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::DBus KF5::WindowSystem) 114 | target_link_libraries(xdg-open Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::DBus KF5::WindowSystem) 115 | endif() 116 | 117 | ADD_CUSTOM_TARGET(link_target ALL 118 | COMMAND ${CMAKE_COMMAND} -E create_symlink launch open) 119 | 120 | target_link_libraries(bundle-thumbnailer Qt${QT_VERSION_MAJOR}::Widgets) 121 | 122 | # Allow for 'make install' 123 | install(TARGETS launch open bundle-thumbnailer 124 | RUNTIME DESTINATION bin) 125 | 126 | # On most systems, sbin has priority on the $PATH over bin 127 | install(TARGETS xdg-open 128 | RUNTIME DESTINATION sbin) 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2020-23 Simon Peter 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # launch [![Build Status](https://api.cirrus-ci.com/github/helloSystem/launch.svg)](https://cirrus-ci.com/github/helloSystem/launch) 2 | 3 | Command line tools (`launch`, `open`,...) to launch applications and open documents (and protocols). Will first search `$PATH` and then `.app` bundles and `.AppDir` directories using the launch "database", and will show launch errors in the GUI. GUI applications in [helloSystem](https://hellosystem.github.io/) like like Filer, Menu, and other applications use these tools to launch applications and open documents (and protocols). The command line tools can also be invoked directly from the command line. 4 | 5 | The use of XDG standards relating to launching applications and opening documents (and protocols) is only supported to a certain extent to provide smooth backward compatibility to legacy applications, but is otherwise discouraged. 6 | 7 | While the tools are developed with helloSystem in mind, they are also built and occasionally tested on Linux to ensure platform independence. 8 | 9 | ## Build 10 | 11 | On Alpine Linux: 12 | 13 | ``` 14 | apk add --no-cache qt5-qtbase-dev kwindowsystem-dev git cmake musl-dev alpine-sdk clang 15 | ``` 16 | 17 | ```shell 18 | mkdir build 19 | cd build 20 | cmake .. 21 | make 22 | ``` 23 | 24 | ## Launch "database" 25 | 26 | The tools use a filesystem-based "database" to look up which applications should be launched to open documents (or protocols) of certain (MIME) types. 27 | 28 | Currently the implementation is like this: 29 | 30 | ``` 31 | ~/.local/share/launch/Applications 32 | ~/.local/share/launch/MIME 33 | ~/.local/share/launch/MIME/x-scheme-handler_https 34 | # The following entries get populated automatically whenever the system "sees" an application 35 | ~/.local/share/launch/MIME/x-scheme-handler_https/Chromium.app 36 | ~/.local/share/launch/MIME/x-scheme-handler_https/firefox.desktop 37 | # The following entries do not get populated automatically, but only after the user chooses a default application for a (MIME) type 38 | ~/.local/share/launch/MIME/x-scheme-handler_https/Default # Symlink to the default application for this MIME type 39 | ``` 40 | 41 | ## Types of error messages 42 | 43 | In general, `launch` shows error messages that would otherwise get printed to stderr (and hence be invisible for GUI users) in a dialog box. 44 | 45 | ![image](https://user-images.githubusercontent.com/2480569/96336678-be08b780-1081-11eb-8665-32eee927f231.png) 46 | 47 | Some of the default error messages are less then user friendly, and do not give any clues to the user on how the situation can be resolved: 48 | 49 | ![image](https://user-images.githubusercontent.com/2480569/96020556-84039f80-0e4e-11eb-9a43-dd21b28e209b.png) 50 | 51 | Hence, `launch` can handle certain types of error messages and provide more useful information: 52 | 53 | ![image](https://user-images.githubusercontent.com/2480569/96335893-0cb35300-107c-11eb-9871-76e477391202.png) 54 | 55 | ![image](https://user-images.githubusercontent.com/2480569/96336616-60746b00-1081-11eb-9c1e-a8c06da46e2a.png) 56 | 57 | It is also possible to get additional information, e.g., from the package manager: 58 | 59 | ![image](https://user-images.githubusercontent.com/2480569/96335900-1f2d8c80-107c-11eb-9b30-5925d6d06df0.png) 60 | 61 | It would even be conceivable that the dialog just asks the user for confirmation to run the suggested command automatically. 62 | -------------------------------------------------------------------------------- /man/launch.1.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: LAUNCH 3 | section: 1 4 | header: User Manual 5 | footer: helloSystem 0.8.0 6 | date: November 13, 2022 7 | --- 8 | # NAME 9 | launch - Command line tool to launch applications in the helloDesktop desktop environment. 10 | 11 | # SYNOPSIS 12 | **launch** *application* [*arguments*]... 13 | 14 | # DESCRIPTION 15 | **launch** is used to launch applications from the command line, and from other applications 16 | such as the Filer or the Menu. It determines the path of the application to be launched, 17 | sets some environment variables, and starts a child process for the application. 18 | 19 | **launch** tries to re-use existing application instances when possible, instead of 20 | launching new ones. 21 | If the application to be launched is an application bundle, no arguments have been supplied 22 | to the application, a process is already running for an executable with the same path 23 | as the executable of the application to be launched, and there are existing 24 | windows for this process, then the existing windows are activated 25 | instead of launching a new instance of the application. 26 | 27 | If the application to be launched is an application bundle that is not already running, **launch** asks the Menu via D-Bus to show the name of the application while the application is being launched. 28 | 29 | If the application cannot be found, cannot be launched, or exits with a return code other than 0, 30 | **launch** displays a graphical error message on the screen. 31 | 32 | # ARGUMENTS 33 | 34 | The following environment variables get set on the child process: 35 | 36 | **application** 37 | : the path of an executable, the filename of an executable on the $PATH, the path of an application bundle (.app, .AppDir, .AppImage), or the name of an application bundle. 38 | 39 | **arguments** 40 | : Arguments passed through to the launched application. 41 | 42 | # ENVIRONMENT 43 | **LAUNCHED_EXECUTABLE** 44 | : The executable being executed, e.g., "/System/Filer.app/Filer". 45 | 46 | **LAUNCHED_BUNDLE** 47 | : The executable being executed, e.g., "/System/Filer.app". 48 | 49 | # FILES 50 | **~/.local/share/launch/launch.db** 51 | : The launch database that holds information about the applications known to the system. 52 | 53 | # EXAMPLES 54 | **launch FeatherPad** 55 | : Launches an application from an application bundle located at any location known to the launch database named FeatherPad that might end in .app, .AppDir, or .AppImage, or in .desktop as a fallback for legacy compatibility. 56 | 57 | **launch FeatherPad.{app,AppDir,AppImage}** 58 | : Launches an application from an application bundle located at any location known to the launch database named FeatherPad with the respective suffix. 59 | 60 | **launch /Applications/FeatherPad** 61 | : Launches an application from an application bundle located at /Applications named FeatherPad that might end in .app, .AppDir, or .AppImage, or in .desktop as a fallback for legacy compatibility. 62 | 63 | **launch /Applications/FeatherPad.{app,AppDir,AppImage}** 64 | : Launches an application from an application bundle located at /Applications named FeatherPad with the respective suffix. 65 | 66 | **launch /usr/local/share/applications/featherpad.desktop** 67 | : Launches an application from an application bundle located at /usr/local/share/applications/ named featherpad.desktop. This is provided as a fallback for legacy applications. Only a minimal subset of the XDG specifications is supported. 68 | 69 | **launch featherpad** 70 | : Launches an application from an executable on the $PATH given as the first argument. 71 | 72 | **launch /usr/local/bin/featherpad** 73 | : Launches an application from the path of an executable given as the first argument. 74 | 75 | # SEE ALSO 76 | open(1) 77 | 78 | # BUGS 79 | Submit bug reports online at: 80 | 81 | # SEE ALSO 82 | Full documentation and sources at: 83 | 84 | # AUTHORS 85 | Written by Simon Peter for helloSystem. 86 | -------------------------------------------------------------------------------- /src/AppDiscovery.cpp: -------------------------------------------------------------------------------- 1 | #include "AppDiscovery.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "DbManager.h" 9 | 10 | AppDiscovery::AppDiscovery(DbManager *db) 11 | { 12 | dbman = db; 13 | } 14 | 15 | AppDiscovery::~AppDiscovery() { } 16 | 17 | QStringList AppDiscovery::wellKnownApplicationLocations() 18 | { 19 | QStringList wellKnownApplicationLocations = {}; 20 | 21 | // Add some location in $HOME 22 | wellKnownApplicationLocations.append(QDir::homePath() + "/Applications"); 23 | wellKnownApplicationLocations.append(QDir::homePath() + "/bin"); 24 | wellKnownApplicationLocations.append(QDir::homePath() + "/.bin"); 25 | 26 | // Add system-wide locations 27 | // TODO: Find a better and more complete way to specify the GNUstep ones 28 | wellKnownApplicationLocations.append( 29 | { "/Applications", "/System", "/Library", "/usr/local/GNUstep/Local/Applications", 30 | "/usr/local/GNUstep/System/Applications", "/usr/GNUstep/Local/Applications", 31 | "/usr/GNUstep/System/Applications" }); 32 | 33 | // Add legacy locations for XDG compatibility 34 | // On FreeBSD: "/home/user/.local/share/applications", 35 | // "/usr/local/share/applications", "/usr/share/applications" 36 | wellKnownApplicationLocations.append( 37 | QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation)); 38 | 39 | wellKnownApplicationLocations.removeDuplicates(); 40 | 41 | return wellKnownApplicationLocations; 42 | } 43 | 44 | void AppDiscovery::findAppsInside(QStringList locationsContainingApps) 45 | // probono: Check locationsContainingApps for applications and add them to the 46 | // m_systemMenu. 47 | // TODO: Nested submenus rather than flat ones with '→' 48 | // This code is similar to the code in the 'launch' command 49 | { 50 | QStringList nameFilter({ "*.app", "*.AppDir", "*.desktop", "*.AppImage", "*.appimage" }); 51 | foreach (QString directory, locationsContainingApps) { 52 | // Shall we process this directory? Only if it contains at least one 53 | // application, to optimize for speed by not descending into directory trees 54 | // that do not contain any applications at all. Can make a big difference. 55 | 56 | QDir dir(directory); 57 | int numberOfAppsInDirectory = dir.entryList(nameFilter).length(); 58 | 59 | if (directory.endsWith(".app") == false && directory.endsWith(".AppDir") == false 60 | && numberOfAppsInDirectory > 0) { 61 | } else { 62 | continue; 63 | } 64 | 65 | // Use QDir::entryList() insted of QDirIterator because it supports sorting 66 | QStringList candidates = dir.entryList(); 67 | for (QString candidate : candidates) { 68 | candidate = dir.path() + "/" + candidate; 69 | // Do not show Autostart directories (or should we?) 70 | if ((candidate.endsWith("/Autostart") == true) || (candidate.endsWith("/.") == true) 71 | || (candidate.endsWith("/..") == true)) { 72 | continue; 73 | } 74 | qDebug() << "Processing" << candidate; 75 | 76 | if (candidate.endsWith(".app")) { 77 | dbman->handleApplication(candidate); 78 | } else if (candidate.endsWith(".AppDir")) { 79 | dbman->handleApplication(candidate); 80 | } else if (candidate.endsWith(".desktop")) { 81 | dbman->handleApplication(candidate); 82 | } else if (candidate.endsWith(".AppImage") || candidate.endsWith(".appimage")) { 83 | dbman->handleApplication(candidate); 84 | } else if (locationsContainingApps.contains(candidate) == false 85 | && QFileInfo(candidate).isDir() && candidate.endsWith("/..") == false 86 | && candidate.endsWith("/.") == false && candidate.endsWith(".app") == false 87 | && candidate.endsWith(".AppDir") == false) { 88 | // qDebug() << "# Found" << file.fileName() << ", a directory that is 89 | // not an .app bundle nor an .AppDir"; 90 | QStringList locationsToBeChecked({ candidate }); 91 | findAppsInside(locationsToBeChecked); 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/AppDiscovery.h: -------------------------------------------------------------------------------- 1 | #ifndef APPDISCOVERY_H 2 | #define APPDISCOVERY_H 3 | 4 | #include 5 | 6 | #include "DbManager.h" 7 | 8 | /** 9 | * @file AppDiscovery.h 10 | * @class AppDiscovery 11 | * @brief A class for discovering and handling application locations. 12 | * 13 | * This class is responsible for discovering well-known application locations and 14 | * finding applications within those locations. 15 | */ 16 | class AppDiscovery 17 | { 18 | public: 19 | /** 20 | * Constructor. 21 | * 22 | * @param db A pointer to the DbManager instance for database handling. 23 | */ 24 | AppDiscovery(DbManager *db); 25 | 26 | /** 27 | * Destructor. 28 | */ 29 | ~AppDiscovery(); 30 | 31 | /** 32 | * Retrieve a list of well-known application locations. 33 | * 34 | * @return A QStringList containing well-known application locations. 35 | */ 36 | QStringList wellKnownApplicationLocations(); 37 | 38 | /** 39 | * Find and process applications within specified locations. 40 | * 41 | * This function searches for applications within the provided locations and 42 | * handles each discovered application using the associated DbManager instance. 43 | * 44 | * @param locationsContainingApps A list of locations to search for applications. 45 | */ 46 | void findAppsInside(QStringList locationsContainingApps); 47 | 48 | private: 49 | DbManager *dbman; /**< A pointer to the DbManager instance. */ 50 | }; 51 | 52 | #endif // APPDISCOVERY_H 53 | -------------------------------------------------------------------------------- /src/ApplicationInfo.cpp: -------------------------------------------------------------------------------- 1 | #include "ApplicationInfo.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #if defined(__FreeBSD__) 13 | # include 14 | # include 15 | # include 16 | # include 17 | # include 18 | # include 19 | # include 20 | # include 21 | #endif 22 | 23 | ApplicationInfo::ApplicationInfo() { } 24 | 25 | ApplicationInfo::~ApplicationInfo() { } 26 | 27 | // Returns the name of the most nested bundle a file is in, 28 | // or an empty string if the file is not in a bundle 29 | QString ApplicationInfo::bundlePath(const QString &path) 30 | { 31 | QString ourPath = path; 32 | QDir(path).cleanPath(ourPath); 33 | // Remove trailing slashes 34 | while (ourPath.endsWith("/")) { 35 | ourPath.remove(path.length() - 1, 1); 36 | } 37 | if (ourPath.endsWith(".app")) { 38 | return ourPath; 39 | } else if (ourPath.contains(".app/")) { 40 | QStringList parts = ourPath.split(".app"); 41 | parts.removeLast(); 42 | return parts.join(".app"); 43 | } else if (ourPath.endsWith(".AppDir")) { 44 | return ourPath; 45 | } else if (ourPath.contains(".AppDir/")) { 46 | QStringList parts = ourPath.split(".AppDir"); 47 | parts.removeLast(); 48 | return parts.join(".AppDir"); 49 | } else if (ourPath.endsWith(".AppImage")) { 50 | return ourPath; 51 | } else if (ourPath.endsWith(".desktop")) { 52 | return ourPath; 53 | } else { 54 | return ""; 55 | } 56 | } 57 | 58 | QString ApplicationInfo::applicationNiceNameForPath(const QString &path) 59 | { 60 | QString applicationNiceName; 61 | QString bp = bundlePath(path); 62 | if (bp != "") { 63 | applicationNiceName = QFileInfo(bp).completeBaseName(); 64 | } else { 65 | applicationNiceName = 66 | QFileInfo(path).fileName(); // TODO: Somehow figure out via the desktop file a 67 | // properly capitalized name... 68 | } 69 | return applicationNiceName; 70 | } 71 | 72 | // Returns the name of the bundle 73 | // based on the LAUNCHED_BUNDLE environment variable set by the 'launch' command 74 | QString ApplicationInfo::bundlePathForPId(unsigned int pid) 75 | { 76 | QString path; 77 | 78 | #if defined(__FreeBSD__) 79 | 80 | struct procstat *prstat = procstat_open_sysctl(); 81 | if (prstat == NULL) { 82 | return ""; 83 | } 84 | unsigned int cnt; 85 | kinfo_proc *procinfo = procstat_getprocs(prstat, KERN_PROC_PID, pid, &cnt); 86 | if (procinfo == NULL || cnt != 1) { 87 | procstat_close(prstat); 88 | return ""; 89 | } 90 | char **envs = procstat_getenvv(prstat, procinfo, 0); 91 | if (envs == NULL) { 92 | procstat_close(prstat); 93 | return ""; 94 | } 95 | 96 | for (int i = 0; envs[i] != NULL; i++) { 97 | const QString &entry = QString::fromLocal8Bit(envs[i]); 98 | const int splitPos = entry.indexOf('='); 99 | 100 | if (splitPos != -1) { 101 | const QString &name = entry.mid(0, splitPos); 102 | const QString &value = entry.mid(splitPos + 1, -1); 103 | // qDebug() << "name:" << name; 104 | // qDebug() << "value:" << value; 105 | if (name == "LAUNCHED_BUNDLE") { 106 | path = value; 107 | break; 108 | } 109 | } 110 | } 111 | 112 | procstat_freeenvv(prstat); 113 | procstat_close(prstat); 114 | 115 | #else 116 | // Linux; see Menu for a cross-platform solution? 117 | qDebug() << "TODO: Implement getting env"; 118 | path = "ThisIsOnlyImplementedForFreeBSDSoFar"; 119 | #endif 120 | 121 | // qDebug() << "probono: bundlePathForPId returns:" << path; 122 | return path; 123 | } 124 | 125 | QString ApplicationInfo::bundlePathForWId(unsigned long long id) 126 | { 127 | QString path; 128 | KWindowInfo info(id, NET::WMPid, NET::WM2TransientFor | NET::WM2WindowClass); 129 | return bundlePathForPId(info.pid()); 130 | } 131 | 132 | QString ApplicationInfo::pathForWId(unsigned long long id) 133 | { 134 | QString path; 135 | KWindowInfo info(id, NET::WMPid, NET::WM2TransientFor | NET::WM2WindowClass); 136 | 137 | // qDebug() << "probono: info.pid():" << info.pid(); 138 | // qDebug() << "probono: info.windowClassName():" << info.windowClassName(); 139 | 140 | QProcess p; 141 | QStringList arguments; 142 | if (QFile::exists(QString("/proc/%1/file").arg(info.pid()))) { 143 | // FreeBSD 144 | arguments = QStringList() << "-f" << QString("/proc/%1/file").arg(info.pid()); 145 | } else if (QFile::exists(QString("/proc/%1/exe").arg(info.pid()))) { 146 | // Linux 147 | arguments = QStringList() << "-f" << QString("/proc/%1/exe").arg(info.pid()); 148 | } 149 | p.start("readlink", arguments); 150 | p.waitForFinished(); 151 | QString retStr(p.readAllStandardOutput().trimmed()); 152 | if (!retStr.isEmpty()) { 153 | // qDebug() << "probono:" << p.program() << p.arguments(); 154 | // qDebug() << "probono: retStr:" << retStr; 155 | path = retStr; 156 | } 157 | // qDebug() << "probono: pathForWId returns:" << path; 158 | return path; 159 | } 160 | 161 | QString ApplicationInfo::applicationNiceNameForWId(unsigned long long id) 162 | { 163 | QString path; 164 | QString applicationNiceName; 165 | KWindowInfo info(id, NET::WMPid, NET::WM2TransientFor | NET::WM2WindowClass); 166 | applicationNiceName = applicationNiceNameForPath(bundlePathForPId(info.pid())); 167 | if (applicationNiceName.isEmpty()) { 168 | applicationNiceName = QFileInfo(pathForWId(id)).fileName(); 169 | } 170 | return applicationNiceName; 171 | } 172 | -------------------------------------------------------------------------------- /src/ApplicationInfo.h: -------------------------------------------------------------------------------- 1 | #ifndef APPLICATIONINFO_H 2 | #define APPLICATIONINFO_H 3 | 4 | #include 5 | 6 | /* 7 | * https://en.wikipedia.org/wiki/Rule_of_three_(computer_programming) 8 | * Currently being used in: 9 | * Menu (master) 10 | * launch (copy) 11 | */ 12 | 13 | class ApplicationInfo 14 | { 15 | public: 16 | /** 17 | * Constructor. 18 | * 19 | * Creates an instance of the ApplicationInfo class. 20 | */ 21 | explicit ApplicationInfo(); 22 | 23 | /** 24 | * Destructor. 25 | * 26 | * Cleans up resources associated with the ApplicationInfo instance. 27 | */ 28 | ~ApplicationInfo(); 29 | 30 | /** 31 | * Get the most nested bundle path of a file. 32 | * 33 | * This function returns the name of the most nested bundle a file is in, 34 | * or an empty string if the file is not in a bundle. 35 | * 36 | * @param path The path of the file to check. 37 | * @return The bundle path or an empty string. 38 | */ 39 | static QString bundlePath(const QString &path); 40 | 41 | /** 42 | * Get a human-readable application name for a given path. 43 | * 44 | * This function returns a nice name for the application based on its path. 45 | * 46 | * @param path The path of the application. 47 | * @return The application nice name. 48 | */ 49 | static QString applicationNiceNameForPath(const QString &path); 50 | 51 | /** 52 | * Get the bundle path for a given process ID. 53 | * 54 | * This function returns the bundle path for a process ID, based on the 55 | * LAUNCHED_BUNDLE environment variable set by the 'launch' command. 56 | * 57 | * @param pid The process ID. 58 | * @return The bundle path. 59 | */ 60 | static QString bundlePathForPId(unsigned int pid); 61 | 62 | /** 63 | * Get the bundle path for a given window ID. 64 | * 65 | * This function returns the bundle path associated with a window ID. 66 | * 67 | * @param id The window ID. 68 | * @return The bundle path. 69 | */ 70 | static QString bundlePathForWId(unsigned long long id); 71 | 72 | /** 73 | * Get the path for a given window ID. 74 | * 75 | * This function returns the path associated with a window ID. 76 | * 77 | * @param id The window ID. 78 | * @return The path. 79 | */ 80 | static QString pathForWId(unsigned long long id); 81 | 82 | /** 83 | * Get a human-readable application name for a given window ID. 84 | * 85 | * This function returns a nice name for the application associated with 86 | * a window ID. 87 | * 88 | * @param id The window ID. 89 | * @return The application nice name. 90 | */ 91 | static QString applicationNiceNameForWId(unsigned long long id); 92 | }; 93 | 94 | #endif // APPLICATIONINFO_H 95 | -------------------------------------------------------------------------------- /src/ApplicationSelectionDialog.cpp: -------------------------------------------------------------------------------- 1 | #include "ApplicationSelectionDialog.h" 2 | #include "ui_ApplicationSelectionDialog.h" 3 | #include 4 | 5 | #include "extattrs.h" 6 | #include 7 | #include 8 | #include 9 | #include "launcher.h" 10 | #include "DbManager.h" 11 | #include 12 | 13 | 14 | ApplicationSelectionDialog::ApplicationSelectionDialog(QString *fileOrProtocol, QString *mimeType, 15 | bool showAlsoLegacyCandidates, bool showAllCandidates, 16 | QWidget *parent) 17 | : QDialog(parent), ui(new Ui::ApplicationSelectionDialog) 18 | { 19 | 20 | this->fileOrProtocol = fileOrProtocol; 21 | this->mimeType = mimeType; 22 | this->showAlsoLegacyCandidates = showAlsoLegacyCandidates; 23 | 24 | QString selectedApplication; 25 | 26 | ui->setupUi(this); 27 | ui->label->setText(QString("Please choose an application to open \n'%1'\nof type '%2':") 28 | .arg(*fileOrProtocol) 29 | .arg(*mimeType).replace("_", "/")); 30 | 31 | connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); 32 | connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); 33 | connect(ui->listWidget, &QListWidget::doubleClicked, this, &QDialog::accept); 34 | 35 | // Add a "Cancel" button 36 | QPushButton *cancelButton = new QPushButton(tr("Cancel")); 37 | cancelButton->setCheckable(true); 38 | cancelButton->setAutoExclusive(true); 39 | cancelButton->setAutoDefault(false); 40 | ui->buttonBox->addButton(cancelButton, QDialogButtonBox::RejectRole); 41 | 42 | // Add a button "Other..." that lets the user select an application not in the list 43 | QPushButton *openWithButton = new QPushButton(tr("Other...")); 44 | openWithButton->setCheckable(true); 45 | openWithButton->setAutoExclusive(true); 46 | openWithButton->setAutoDefault(false); 47 | ui->buttonBox->addButton(openWithButton, QDialogButtonBox::ActionRole); 48 | connect(openWithButton, &QPushButton::clicked, [=]() { 49 | QFileDialog fileDialog; 50 | fileDialog.setFileMode(QFileDialog::Directory); 51 | fileDialog.setFileMode(QFileDialog::AnyFile); 52 | fileDialog.setDirectory(DbManager::localShareLaunchApplicationsPath); 53 | if (fileDialog.exec()) { 54 | // Get the selected file or directory 55 | QStringList selectedFiles = fileDialog.selectedFiles(); 56 | if (selectedFiles.size() == 1) { 57 | QString selectedFile = selectedFiles.at(0); 58 | QStringList args; 59 | args << QFileInfo(selectedFile).absoluteFilePath(); 60 | args << *fileOrProtocol; 61 | 62 | // Symlink the chosen application to the launch "database" 63 | // so that it can be set as the default application later on 64 | QString symlinkPath = QString("%1/%2").arg(DbManager::localShareLaunchApplicationsPath).arg(QFileInfo(selectedFile).fileName()); 65 | if (!QDir(DbManager::localShareLaunchApplicationsPath).exists()) { 66 | QDir().mkdir(DbManager::localShareLaunchApplicationsPath); 67 | } 68 | if (!QFile::exists(symlinkPath) 69 | && !QFile::link(selectedFile, symlinkPath)) { 70 | QMessageBox::critical(this, tr("Error"), tr("Could not create symlink from %1 to %2").arg(selectedFile).arg(symlinkPath)); 71 | } 72 | // Symlink the chosen application from the symlink we just created to the MIME type path 73 | QString mimePath = QString("%1/%2") 74 | .arg(DbManager::localShareLaunchMimePath) 75 | .arg(QString(*mimeType).replace("/", "_")); 76 | if (!QDir(mimePath).exists()) { 77 | QDir().mkdir(mimePath); 78 | } 79 | QString mimeSymlinkPath = QString("%1/%2").arg(mimePath).arg(QFileInfo(selectedFile).fileName()); 80 | if (!QFile::exists(mimeSymlinkPath) 81 | && !QFile::link(symlinkPath, mimeSymlinkPath)) { 82 | QMessageBox::critical(this, tr("Error"), tr("Could not create symlink from %1 to %2").arg(symlinkPath).arg(mimeSymlinkPath)); 83 | } 84 | 85 | this->hide(); 86 | Launcher launcher; 87 | launcher.launch(args); 88 | } 89 | } 90 | 91 | }); 92 | 93 | this->setWindowTitle(tr("Open With")); 94 | 95 | ui->checkBoxAlwaysOpenThis->setEnabled(false); 96 | ui->checkBoxAlwaysOpenAll->setEnabled(false); 97 | 98 | QStringList *appCandidates; 99 | DbManager *db; 100 | 101 | // When selection changes and something is selected, enable the checkboxes 102 | // For URL scheme handlers, setting the default application "for this file" is not possible 103 | connect(ui->listWidget, &QListWidget::itemSelectionChanged, [=]() { 104 | if (ui->listWidget->selectedItems().length() > 0) { 105 | if (!mimeType->startsWith("x-scheme-handler")) 106 | ui->checkBoxAlwaysOpenThis->setEnabled(true); 107 | ui->checkBoxAlwaysOpenAll->setEnabled(true); 108 | } else { 109 | ui->checkBoxAlwaysOpenThis->setEnabled(false); 110 | ui->checkBoxAlwaysOpenAll->setEnabled(false); 111 | } 112 | }); 113 | 114 | // Construct the path to the MIME type in question 115 | QString mimePath = QString("%1/%2") 116 | .arg(DbManager::localShareLaunchMimePath) 117 | .arg(QString(*mimeType).replace("/", "_")); 118 | 119 | // Create the directory if it doesn't exist so that it is easier to 120 | // manually add applications to it 121 | QDir dir(mimePath); 122 | if (!dir.exists()) { 123 | dir.mkpath("."); 124 | } 125 | 126 | if (showAllCandidates == true) { 127 | qDebug() << "Control modifier pressed, showing all applications"; 128 | // Get all applications known to the system 129 | db = new DbManager(); 130 | appCandidates = new QStringList(db->allApplications()); 131 | delete db; 132 | showAlsoLegacyCandidates = true; 133 | } else { 134 | // Normal operation 135 | // Populate appCandidates with the syminks at mimePath 136 | qDebug() << "Normal operation (no modifier key is pressed) showing only applications for" << *mimeType; 137 | appCandidates = 138 | new QStringList(QDir(mimePath).entryList(QDir::NoDotAndDotDot | QDir::AllEntries)); 139 | // Prepend each candidate with the path at which it was found 140 | for (auto r = 0; r < appCandidates->length(); r++) { 141 | appCandidates->replace(r, QString("%1/%2").arg(mimePath).arg(appCandidates->at(r))); 142 | } 143 | 144 | } 145 | 146 | // Order apppCandidates by name and put ones ending in .desktop last 147 | std::sort(appCandidates->begin(), appCandidates->end()); 148 | std::stable_sort(appCandidates->begin(), appCandidates->end(), 149 | [](const QString &a, const QString &b) { 150 | return a.endsWith(".desktop") < b.endsWith(".desktop"); 151 | }); 152 | 153 | // If there are no appCandidates, then search for applications that can open the MIME type 154 | // up to the "_" part; e.g., if we have no candidates for "text_plain", then search for 155 | // candidates for "text" instead 156 | int appCandidatesCount = appCandidates->length(); 157 | if (appCandidatesCount == 0) { 158 | qDebug() << "No candidates found for" << *mimeType; 159 | qDebug() << "Hence looking for candidates for" 160 | << mimeType->replace("/", "_").split("_").first(); 161 | // Get the parent of mimePath 162 | QString mimePathParent = QFileInfo(mimePath).path(); 163 | // Make a mimePathDirs list of all directories in localShareLaunchMimePath that start with 164 | // e.g., "text_" 165 | QStringList *mimePathDirs = new QStringList( 166 | QDir(DbManager::localShareLaunchMimePath) 167 | .entryList(QDir::NoDotAndDotDot | QDir::AllDirs | QDir::NoSymLinks)); 168 | for (auto r = 0; r < mimePathDirs->length(); r++) { 169 | if (!mimePathDirs->at(r).startsWith( 170 | QString(*mimeType).replace("/", "_").split("_").first() + "_")) { 171 | mimePathDirs->removeAt(r); 172 | r--; 173 | } 174 | } 175 | // Prepend each entry in mimePathDirs with their path 176 | for (auto r = 0; r < mimePathDirs->length(); r++) { 177 | mimePathDirs->replace(r, 178 | QString("%1/%2") 179 | .arg(DbManager::localShareLaunchMimePath) 180 | .arg(mimePathDirs->at(r))); 181 | } 182 | 183 | // In each of the mimePathDirs, look for candidates 184 | for (auto r = 0; r < mimePathDirs->length(); r++) { 185 | qDebug() << "Looking for candidates in" << mimePathDirs->at(r); 186 | QDir dir(mimePathDirs->at(r)); 187 | dir.setFilter(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); 188 | dir.setSorting(QDir::Name); 189 | QStringList *dirEntries = new QStringList(dir.entryList()); 190 | for (auto s = 0; s < dirEntries->length(); s++) { 191 | // Construct path from dirEntries->at(s) and mimePathDirs->at(r) 192 | QString candPath = QString("%1/%2").arg(mimePathDirs->at(r)).arg(dirEntries->at(s)); 193 | appCandidates->append(candPath); 194 | } 195 | } 196 | } 197 | 198 | // Order apppCandidates by name and put ones ending in .desktop last 199 | std::sort(appCandidates->begin(), appCandidates->end()); 200 | std::stable_sort(appCandidates->begin(), appCandidates->end(), 201 | [](const QString &a, const QString &b) { 202 | return a.endsWith(".desktop") < b.endsWith(".desktop"); 203 | }); 204 | 205 | // Remove entries with the filename "Default" from appCandidates 206 | for (auto r = 0; r < appCandidates->length(); r++) { 207 | if (QFileInfo(appCandidates->at(r)).fileName() == "Default") { 208 | appCandidates->removeAt(r); 209 | r--; 210 | } 211 | } 212 | 213 | // Print number of appCandidates 214 | qDebug() << "Found" << appCandidates->length() << "candidates for" << *mimeType; 215 | 216 | // Remove duplicates from appCandidates. For each candidate, resolve the symlink (if it is one) 217 | // Once we have resolved all symlinks, we can remove duplicates by comparing the resolved 218 | // paths 219 | QStringList *appCandidatesResolved = new QStringList(); 220 | for (auto r = 0; r < appCandidates->length(); r++) { 221 | // Get canonical path of appCandidates->at(r) and if it is a symlink, resolve it 222 | QString appCandidateResolved = QFileInfo(appCandidates->at(r)).canonicalFilePath(); 223 | appCandidatesResolved->append(appCandidateResolved); 224 | } 225 | *appCandidates = appCandidatesResolved->toSet().toList(); 226 | 227 | // Print number of appCandidates 228 | qDebug() << "Found" << appCandidates->length() << "candidates for" << *mimeType; 229 | 230 | // Print appCandidates, each on a new line 231 | qDebug() << "appCandidates:"; 232 | for (auto r = 0; r < appCandidates->length(); r++) { 233 | qDebug() << appCandidates->at(r); 234 | } 235 | 236 | // Let's see if we have at least one application candidate that is not a desktop file; 237 | // only fall back to showing desktop files if we don't. Those are second-class citizens 238 | // only supported to a minimum extent for compatibility with legacy applications 239 | 240 | QStringList *preferredAppCandidates; 241 | preferredAppCandidates = new QStringList(); 242 | for (auto r = 0; r < appCandidates->length(); r++) { 243 | if (!appCandidates->at(r).endsWith(".desktop")) { 244 | preferredAppCandidates->append(appCandidates->at(r)); 245 | } 246 | } 247 | 248 | // Print how many desktop files we have 249 | int desktopFilesCount = appCandidates->length() - preferredAppCandidates->length(); 250 | // Print how many non-desktop files we have 251 | qDebug() << "Found" << preferredAppCandidates->length() << "non-desktop files"; 252 | qDebug() << "Found" << desktopFilesCount << "desktop files"; 253 | // Print whether showAlsoLegacyCandidates is true or false 254 | qDebug() << "showAlsoLegacyCandidates:" << showAlsoLegacyCandidates; 255 | qDebug() << "showAllCandidates:" << showAllCandidates; 256 | 257 | if (!showAlsoLegacyCandidates && preferredAppCandidates->length() > 0) { 258 | // Use preferredAppCandidates instead of appCandidates 259 | appCandidates = preferredAppCandidates; 260 | } 261 | 262 | for (auto r = 0; r < appCandidates->length(); r++) { 263 | QListWidgetItem *item = new QListWidgetItem(appCandidates->at(r)); 264 | item->setData(Qt::UserRole, QDir(appCandidates->at(r)).canonicalPath()); 265 | item->setToolTip(QDir(appCandidates->at(r)).canonicalPath()); 266 | QString completeBaseName = QFileInfo(item->text()).completeBaseName(); 267 | item->setText(completeBaseName); 268 | ui->listWidget->addItem(item); 269 | } 270 | } 271 | 272 | ApplicationSelectionDialog::~ApplicationSelectionDialog() 273 | { 274 | delete ui; 275 | } 276 | 277 | QString ApplicationSelectionDialog::getSelectedApplication() 278 | { 279 | QString appPath = ui->listWidget->selectedItems().first()->data(Qt::UserRole).toString(); 280 | if (ui->checkBoxAlwaysOpenThis->isChecked()) { 281 | qDebug() << "Writing open-with extended attribute"; 282 | // Get the path from the selected item 283 | 284 | // Write the open-with extended attribute 285 | bool ok = false; 286 | ok = Fm::setAttributeValueQString(*fileOrProtocol, "open-with", appPath); 287 | if (!ok) { 288 | QMessageBox msgBox; 289 | msgBox.setIcon(QMessageBox::Critical); 290 | msgBox.setText("Could not write the 'open-with' extended attribute"); 291 | msgBox.exec(); 292 | } 293 | 294 | } else if (ui->checkBoxAlwaysOpenAll->isChecked()) { 295 | 296 | // Clear the open-with extended attribute if it exists by setting it to NULL 297 | bool ok = false; 298 | ok = Fm::setAttributeValueQString(*fileOrProtocol, "open-with", NULL); 299 | /* 300 | if (!ok) { 301 | QMessageBox msgBox; 302 | msgBox.setIcon(QMessageBox::Critical); 303 | msgBox.setText("Could not clear the 'open-with' extended attribute"); 304 | msgBox.exec(); 305 | } 306 | */ 307 | 308 | qDebug() << "Creating default symlink for this MIME type"; 309 | // Use dbmanager to create a symlink in ~/.local/share/launch/MIME/<...>/Default to the 310 | // selected application 311 | 312 | QString mimePath = QString("%1/%2") 313 | .arg(DbManager::localShareLaunchMimePath) 314 | .arg(QString(*mimeType).replace("/", "_")); 315 | QString defaultPath = QString("%1/Default").arg(mimePath); 316 | qDebug() << "mimePath:" << mimePath; 317 | 318 | // Check if the MIME path exists and is a directory; if not, create it 319 | if (!QFile::exists(mimePath)) { 320 | qDebug() << "Creating directory for this MIME type"; 321 | bool ok = false; 322 | ok = QDir().mkpath(mimePath); 323 | if (!ok) { 324 | QMessageBox msgBox; 325 | msgBox.setIcon(QMessageBox::Critical); 326 | msgBox.setText("Could not create the directory for this MIME type"); 327 | msgBox.exec(); 328 | } 329 | } 330 | 331 | qDebug() << "Removing existing default symlink if it exists"; 332 | 333 | // Remove the symlink if it exists 334 | if (QFile::exists(defaultPath)) { 335 | bool ok = false; 336 | ok = QFile::remove(defaultPath); 337 | if (!ok) { 338 | QMessageBox msgBox; 339 | msgBox.setIcon(QMessageBox::Critical); 340 | msgBox.setText("Could not remove the existing default symlink"); 341 | msgBox.exec(); 342 | } 343 | } 344 | 345 | qDebug() << "Creating default symlink from %s to %s" << appPath << defaultPath; 346 | 347 | // Create the symlink 348 | ok = false; 349 | ok = QFile::link(appPath, defaultPath); 350 | if (!ok) { 351 | QMessageBox msgBox; 352 | msgBox.setIcon(QMessageBox::Critical); 353 | msgBox.setText("Could not create the default symlink"); 354 | msgBox.setInformativeText(QString("From: %1\nTo: %2").arg(appPath).arg(defaultPath)); 355 | msgBox.exec(); 356 | } 357 | 358 | } 359 | 360 | // Touch the file and its parent directory so that Filer updates its icon 361 | QFileInfo fileInfo(*fileOrProtocol); 362 | if (fileInfo.exists()) { 363 | QProcess process; 364 | qDebug() << "Touching parent directory: " << fileInfo.dir().path(); 365 | process.start("touch", QStringList() << fileInfo.dir().path()); 366 | process.waitForFinished(); 367 | } 368 | 369 | return ui->listWidget->selectedItems().first()->text(); 370 | } 371 | -------------------------------------------------------------------------------- /src/ApplicationSelectionDialog.h: -------------------------------------------------------------------------------- 1 | #ifndef APPLICATIONSELECTIONDIALOG_H 2 | #define APPLICATIONSELECTIONDIALOG_H 3 | 4 | #include 5 | #include 6 | #include "DbManager.h" 7 | 8 | namespace Ui { 9 | class ApplicationSelectionDialog; 10 | } 11 | 12 | class ApplicationSelectionDialog : public QDialog 13 | { 14 | Q_OBJECT 15 | 16 | public: 17 | explicit ApplicationSelectionDialog(QString *fileOrProtocol, QString *mimeType, 18 | bool showAlsoLegacyCandidates = false, 19 | bool showAllCandidates = false, 20 | QWidget *parent = nullptr); 21 | ~ApplicationSelectionDialog(); 22 | QString getSelectedApplication(); 23 | 24 | private: 25 | QString *fileOrProtocol; 26 | QString *mimeType; 27 | bool showAlsoLegacyCandidates; 28 | Ui::ApplicationSelectionDialog *ui; 29 | DbManager *db; 30 | }; 31 | 32 | #endif // APPLICATIONSELECTIONDIALOG_H 33 | -------------------------------------------------------------------------------- /src/ApplicationSelectionDialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ApplicationSelectionDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 300 11 | 12 | 13 | 14 | 15 | 16 | 17 | Qt::Horizontal 18 | 19 | 20 | QDialogButtonBox::Ok 21 | 22 | 23 | 24 | 25 | 26 | 27 | true 28 | 29 | 30 | Always open this document with this application 31 | 32 | 33 | 34 | 35 | 36 | 37 | Qt::Vertical 38 | 39 | 40 | 41 | 20 42 | 40 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | true 51 | 52 | 53 | 54 | 55 | 56 | 57 | true 58 | 59 | 60 | Open all documents of this type with this application 61 | 62 | 63 | 64 | 65 | 66 | 67 | TextLabel 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | buttonBox 77 | accepted() 78 | ApplicationSelectionDialog 79 | accept() 80 | 81 | 82 | 248 83 | 254 84 | 85 | 86 | 157 87 | 274 88 | 89 | 90 | 91 | 92 | buttonBox 93 | rejected() 94 | ApplicationSelectionDialog 95 | reject() 96 | 97 | 98 | 316 99 | 260 100 | 101 | 102 | 286 103 | 274 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /src/DbManager.cpp: -------------------------------------------------------------------------------- 1 | #include "DbManager.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "extattrs.h" 8 | 9 | 10 | // Make localShareLaunchApplicationsPath available to other classes 11 | const QString DbManager::localShareLaunchApplicationsPath = 12 | QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) 13 | + "/launch/Applications/"; 14 | 15 | // Make localShareLaunchMimePath available to other classes 16 | const QString DbManager::localShareLaunchMimePath = 17 | QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/launch/MIME/"; 18 | 19 | DbManager::DbManager() : filesystemSupportsExtattr(false) 20 | { 21 | 22 | qDebug() << "DbManager::DbManager()"; 23 | 24 | // In order to find out whether it is worth doing costly operations regarding 25 | // extattrs we check whether the filesystem supports them and only use them if 26 | // it does. This should help speed up things on Live ISOs where extattrs don't 27 | // seem to be supported. 28 | bool ok = false; 29 | ok = Fm::setAttributeValueInt("/usr", "filesystemSupportsExtattr", true); 30 | if (ok) { 31 | filesystemSupportsExtattr = true; 32 | qDebug() << "Extended attributes are supported on /usr; using them"; 33 | } else { 34 | qDebug() << "Extended attributes are not supported on /usr\n" 35 | "or the command to set them needs 'chmod +s'; system will be slower"; 36 | } 37 | 38 | // Create localShareLaunchMimePath and localShareLaunchApplicationsPath 39 | QDir dir; 40 | dir.mkpath(localShareLaunchMimePath); 41 | dir.mkpath(localShareLaunchApplicationsPath); 42 | 43 | // Check all symlinks in ~/.local/share/launch/ and remove any 44 | // that point to non-existent files. 45 | // TODO: Move to a location where it is 46 | // only run periodically, e.g., when the application starts, or run delayed 47 | // after an application is added 48 | QDirIterator it(localShareLaunchApplicationsPath, 49 | QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); 50 | while (it.hasNext()) { 51 | QString symlinkPath = it.next(); 52 | if (QFileInfo(symlinkPath).isSymLink() 53 | && !QFileInfo(QFileInfo(symlinkPath).symLinkTarget()).exists()) { 54 | handleNonExistingApplicationSymlink(symlinkPath); 55 | } 56 | } 57 | 58 | // Check all symlinks in ~/.local/share/launch/MIME/ and remove any 59 | // that point to non-existent files. 60 | // after an application is added 61 | QDirIterator it2(localShareLaunchMimePath, QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); 62 | while (it2.hasNext()) { 63 | QString symlinkPath = it2.next(); 64 | if (QFileInfo(symlinkPath).isSymLink() 65 | && !QFileInfo(QFileInfo(symlinkPath).symLinkTarget()).exists()) { 66 | handleNonExistingApplicationSymlink(symlinkPath); 67 | } 68 | } 69 | } 70 | 71 | DbManager::~DbManager() 72 | { 73 | qDebug() << "DbManager::~DbManager()"; 74 | } 75 | 76 | // Read "can-open" file and return its contents as a QString; 77 | // this is used e.g., when the system encounters application bundles 78 | // for the first time, or when the "open" command wants to open 79 | // documents but the filesystem doesn't support extended attributes 80 | // Returns nullptr if no "can-open" file is found in the application bundle 81 | QString DbManager::getCanOpenFromFile(QString canonicalPath) 82 | { 83 | if (canonicalPath.endsWith(".app")) { 84 | QString canOpenFilePath = canonicalPath + "/Resources/can-open"; 85 | if (!QFileInfo(canOpenFilePath).isFile()) 86 | return QString(); 87 | QFile f(canOpenFilePath); 88 | if (!f.open(QFile::ReadOnly | QFile::Text)) 89 | return QString(); 90 | QTextStream in(&f); 91 | return in.readAll(); 92 | } else if (canonicalPath.endsWith(".desktop")) { 93 | bool ok = false; 94 | QString canOpenFromExtAttr = Fm::getAttributeValueQString(canonicalPath, "can-open", ok); 95 | if (ok) 96 | return canOpenFromExtAttr; // extattr is already set 97 | // The following removes everything after a ';' which XDG loves to use 98 | // even though they are comments in .ini files... 99 | // QSettings desktopFile(canonicalPath, QSettings::IniFormat); 100 | // QString mime = desktopFile.value("Desktop Entry/MimeType").toString(); 101 | // Hence we have to do it the hard way. Yet another example of why XDG is 102 | // too complex 103 | QFile f(canonicalPath); 104 | QString mime = ""; 105 | f.open(QIODevice::ReadOnly | QIODevice::Text); 106 | if (f.isOpen()) { 107 | QTextStream in(&f); 108 | while (!in.atEnd()) { 109 | QString getLine = in.readLine().trimmed(); 110 | if (getLine.startsWith("MimeType=")) { 111 | mime = getLine.remove(0, 9); 112 | mime = mime.trimmed(); 113 | break; 114 | } 115 | } 116 | } 117 | f.close(); 118 | 119 | return mime; 120 | } else { 121 | // TODO: AppDir 122 | return QString(); 123 | } 124 | } 125 | 126 | void DbManager::handleApplication(QString path) 127 | { 128 | QString canonicalPath = QDir(path).canonicalPath(); 129 | 130 | // If it is a symlink, check whether it points to an existing file 131 | bool symlinkTargetExists = true; 132 | if (QFileInfo(canonicalPath).isSymLink()) { 133 | if (!QFileInfo(QFileInfo(canonicalPath).symLinkTarget()).exists()) { 134 | symlinkTargetExists = false; 135 | } 136 | } 137 | 138 | if (! symlinkTargetExists || !(QFileInfo(canonicalPath).isDir() || QFileInfo(canonicalPath).isFile())) { 139 | qDebug() << canonicalPath << "does not exist, removing from launch.db"; 140 | _removeApplication(canonicalPath); 141 | } else { 142 | // qDebug() << "Adding" << canonicalPath << "to launch.db"; 143 | _addApplication(canonicalPath); 144 | 145 | QString mime = getCanOpenFromFile(canonicalPath); 146 | if (mime.isEmpty()) { 147 | qDebug() << "No MIME types found in" << canonicalPath; 148 | return; 149 | } else if (mime == "") { 150 | qDebug() << "Empty MIME types found in" << canonicalPath; 151 | return; 152 | } 153 | 154 | // Split mime types into a QStringList 155 | QStringList mimeList = mime.split(";"); 156 | // Remove entries that consist of only whitespace 157 | mimeList.removeAll(""); 158 | // Trim whitespace from each entry; this is needed because 159 | // otherwise we get, e.g., "text/plain\n" instead of "text/plain" 160 | for (int i = 0; i < mimeList.size(); ++i) { 161 | mimeList[i] = mimeList[i].trimmed(); 162 | } 163 | 164 | for (QString mime : mimeList) { 165 | 166 | if (mime.isEmpty()) { 167 | continue; 168 | } 169 | 170 | QString mimeDir = localShareLaunchMimePath + "/" + mime.replace("/", "_"); 171 | if (!QFileInfo(mimeDir).isDir()) { 172 | QDir dir; 173 | dir.mkpath(mimeDir); 174 | } 175 | 176 | QString link = mimeDir + "/" + QFileInfo(canonicalPath).fileName(); 177 | 178 | if (QFileInfo(link).isSymLink()) { 179 | // qDebug() << "Not creating symlink for" << mime << "because it already" 180 | // << "exists"; 181 | continue; 182 | } 183 | bool ok = QFile::link(canonicalPath, link); 184 | if (ok) { 185 | qDebug() << "Created symlink for" << mime << "in" << localShareLaunchMimePath; 186 | } else { 187 | qDebug() << "Cannot create symlink for" << mime << "in" << localShareLaunchMimePath; 188 | } 189 | } 190 | 191 | // If extended attributes are not supported, there is nothing else to be 192 | // done here 193 | if (!filesystemSupportsExtattr) { 194 | return; 195 | } 196 | 197 | // Set 'can-open' extattr if 'can-open' extattr doesn't already exist but 198 | // 'can-open' file exists 199 | bool ok = false; 200 | Fm::getAttributeValueQString(canonicalPath, "can-open", ok); 201 | if (ok) 202 | return; // extattr is already set 203 | 204 | // Set 'can-open' extattr on the application 205 | ok = Fm::setAttributeValueQString(canonicalPath, "can-open", mime); 206 | if (ok) { 207 | qDebug() << "Set xattr 'can-open' on" << canonicalPath; 208 | } else { 209 | qDebug() << "Cannot set xattr 'can-open' on" << canonicalPath; 210 | } 211 | } 212 | } 213 | 214 | bool DbManager::_addApplication(const QString &path) 215 | { 216 | 217 | bool success = false; 218 | 219 | if (path.isEmpty()) 220 | return false; 221 | 222 | // Prevent recursion by checking if the path starts with the 223 | // localShareLaunchPath 224 | if (path.startsWith(localShareLaunchApplicationsPath) 225 | || path.startsWith(localShareLaunchMimePath)) { 226 | qDebug() << "Not creating symlink for" << path << "because it is in" 227 | << localShareLaunchApplicationsPath << "or" << localShareLaunchMimePath; 228 | return success; 229 | } 230 | 231 | // Check if a symlink to the target already exists in the directory 232 | // ~/.local/share/launch/Applications under any name that starts 233 | // with the name of the target sans extension 234 | // If it does, do nothing 235 | // If it doesn't, create a symlink to the target in the directory 236 | 237 | // Get name of target sans extension 238 | QString targetName = QFileInfo(path).fileName(); 239 | targetName = targetName.left(targetName.lastIndexOf(".")); 240 | QString targetCompleteSuffix = QFileInfo(path).completeSuffix(); 241 | 242 | // Check for existing symlinks to the target 243 | 244 | bool found = false; 245 | 246 | // Check for symlinks that start with the name of the target sans extension 247 | // to also catch -2, -3, etc. 248 | QDirIterator it2(localShareLaunchApplicationsPath, 249 | QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); 250 | while (it2.hasNext()) { 251 | QString symlinkPath = it2.next(); 252 | if (QFileInfo(symlinkPath).symLinkTarget() == path) { 253 | found = true; 254 | break; 255 | } 256 | } 257 | 258 | if (!found) { 259 | QString linkPath = localShareLaunchApplicationsPath + QFileInfo(path).fileName(); 260 | int i = 2; 261 | while (QFileInfo(linkPath).exists()) { 262 | linkPath = localShareLaunchApplicationsPath + targetName + "-" + QString::number(i) 263 | + "." + targetCompleteSuffix; 264 | i++; 265 | } 266 | if (QFile::link(path, linkPath)) { 267 | qDebug() << "Created symlink:" << linkPath; 268 | success = true; 269 | } else { 270 | qDebug() << "Failed to create symlink:" << linkPath; 271 | } 272 | } 273 | 274 | return success; 275 | } 276 | 277 | bool DbManager::_removeApplication(const QString &path) 278 | { 279 | bool success = false; 280 | 281 | // Remove all symlinks from ~/.local/share/launch/Applications that point to 282 | // the target 283 | QDirIterator it(localShareLaunchApplicationsPath, 284 | QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); 285 | while (it.hasNext()) { 286 | QString symlinkPath = it.next(); 287 | if (QFileInfo(symlinkPath).isSymLink() 288 | && QFileInfo(QFileInfo(symlinkPath).symLinkTarget()).canonicalFilePath() 289 | == QFileInfo(path).canonicalFilePath()) { 290 | if (QFile::remove(symlinkPath)) { 291 | qDebug() << "Removed symlink:" << symlinkPath; 292 | success = true; 293 | } else { 294 | qDebug() << "Failed to remove symlink:" << symlinkPath; 295 | QMessageBox msgBox; 296 | msgBox.setIcon(QMessageBox::Critical); 297 | msgBox.setText("Failed to remove symlink:" + symlinkPath); 298 | msgBox.exec(); 299 | } 300 | } 301 | } 302 | 303 | // Also remove it from all subdirectories of ~/.local/share/launch/MIME 304 | // that contain a symlink to the target 305 | QDirIterator it2(localShareLaunchMimePath, 306 | QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); 307 | while (it2.hasNext()) { 308 | QString mimeDir = it2.next(); 309 | if (QFileInfo(mimeDir).isDir()) { 310 | QDirIterator it3(mimeDir, 311 | QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); 312 | while (it3.hasNext()) { 313 | QString symlinkPath = it3.next(); 314 | if (QFileInfo(symlinkPath).isSymLink() 315 | && QFileInfo(QFileInfo(symlinkPath).symLinkTarget()).canonicalFilePath() 316 | == QFileInfo(path).canonicalFilePath()) { 317 | if (QFile::remove(symlinkPath)) { 318 | qDebug() << "Removed symlink:" << symlinkPath; 319 | success = true; 320 | } else { 321 | qDebug() << "Failed to remove symlink:" << symlinkPath; 322 | QMessageBox msgBox; 323 | msgBox.setIcon(QMessageBox::Critical); 324 | msgBox.setText("Failed to remove symlink:" + symlinkPath); 325 | msgBox.exec(); 326 | } 327 | } 328 | } 329 | } 330 | } 331 | 332 | return success; 333 | } 334 | 335 | QStringList DbManager::allApplications() const 336 | { 337 | 338 | QStringList results; 339 | 340 | // Check all symlinks in ~/.local/share/launch/Applications and get their 341 | // targets if they exist, delete them otherwise 342 | QDirIterator it(localShareLaunchApplicationsPath, 343 | QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); 344 | while (it.hasNext()) { 345 | QString symlinkPath = it.next(); 346 | if (QFileInfo(symlinkPath).isSymLink()) { 347 | QString target = QFileInfo(symlinkPath).symLinkTarget(); 348 | if (QFileInfo(target).exists()) { 349 | results.append(target); 350 | } else { 351 | handleNonExistingApplicationSymlink(symlinkPath); 352 | } 353 | } 354 | } 355 | 356 | // Sort the results so that they are in alphabetical order and .desktop files 357 | // are at the end 358 | std::sort(results.begin(), results.end(), [](const QString &a, const QString &b) { 359 | if (a.endsWith(".desktop") && !b.endsWith(".desktop")) { 360 | return false; 361 | } else if (!a.endsWith(".desktop") && b.endsWith(".desktop")) { 362 | return true; 363 | } else { 364 | return a < b; 365 | } 366 | }); 367 | 368 | return results; 369 | } 370 | 371 | unsigned int DbManager::_numberOfApplications() const 372 | { 373 | // Count the number of valid symlinks in 374 | // ~/.local/share/launch/Applications 375 | unsigned int count = 0; 376 | QDirIterator it(localShareLaunchApplicationsPath, 377 | QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); 378 | while (it.hasNext()) { 379 | QString symlinkPath = it.next(); 380 | if (QFileInfo(symlinkPath).isSymLink()) { 381 | QString target = QFileInfo(symlinkPath).symLinkTarget(); 382 | if (QFileInfo(target).exists()) { 383 | count++; 384 | } else { 385 | handleNonExistingApplicationSymlink(symlinkPath); 386 | } 387 | } 388 | } 389 | return count; 390 | } 391 | 392 | bool DbManager::handleNonExistingApplicationSymlink(const QString &symlinkPath) const 393 | { 394 | // Exit if symlinkPath is not a symlink 395 | if (!QFileInfo(symlinkPath).isSymLink()) { 396 | return false; 397 | } 398 | qDebug() << "Removing symlink to non-existent file:" << symlinkPath; 399 | // TODO: We could get fancy here and check whether similar applications exist 400 | // at the target path (e.g., newer versions) and if so, ask the user whether 401 | // they want to create a new symlink to the new location 402 | QFile::remove(symlinkPath); 403 | return true; 404 | } 405 | 406 | bool DbManager::applicationExists(const QString &path) const 407 | { 408 | bool exists = false; 409 | // Check all symlinks in ~/.local/share/launch/Applications and get their 410 | // targets if they exist, delete them otherwise. If the target of a symlink 411 | // matches the path, the application exists 412 | QDirIterator it(localShareLaunchApplicationsPath, 413 | QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); 414 | while (it.hasNext()) { 415 | QString symlinkPath = it.next(); 416 | if (QFileInfo(symlinkPath).isSymLink()) { 417 | QString target = QFileInfo(symlinkPath).symLinkTarget(); 418 | if (QFileInfo(target).exists()) { 419 | if (target == path) { 420 | exists = true; 421 | break; 422 | } 423 | } else { 424 | handleNonExistingApplicationSymlink(symlinkPath); 425 | } 426 | } 427 | } 428 | return exists; 429 | } 430 | 431 | bool DbManager::removeAllApplications() 432 | { 433 | bool success = false; 434 | 435 | // Delete all symlinks in ~/.local/share/launch/Applications 436 | QDirIterator it(localShareLaunchApplicationsPath, 437 | QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); 438 | while (it.hasNext()) { 439 | QString symlinkPath = it.next(); 440 | if (QFileInfo(symlinkPath).isSymLink()) { 441 | qDebug() << "Removing symlink:" << symlinkPath; 442 | QFile::remove(symlinkPath); 443 | } 444 | } 445 | return success; 446 | } 447 | -------------------------------------------------------------------------------- /src/DbManager.h: -------------------------------------------------------------------------------- 1 | #ifndef DBMANAGER_H 2 | #define DBMANAGER_H 3 | 4 | #include 5 | 6 | class DbManager 7 | { 8 | public: 9 | DbManager(); 10 | ~DbManager(); 11 | void handleApplication(QString canonicalPath); 12 | QStringList allApplications() const; 13 | bool removeAllApplications(); 14 | bool handleNonExistingApplicationSymlink(const QString &symlinkPath) const; 15 | bool applicationExists(const QString &name) const; 16 | QString getCanOpenFromFile(QString canonicalPath); 17 | bool filesystemSupportsExtattr; 18 | static const QString localShareLaunchApplicationsPath; 19 | static const QString localShareLaunchMimePath; 20 | 21 | private: 22 | bool _createTable(); 23 | bool _addApplication(const QString &name); 24 | bool _removeApplication(const QString &name); 25 | 26 | unsigned int _numberOfApplications() const; 27 | }; 28 | 29 | #endif // DBMANAGER_H 30 | -------------------------------------------------------------------------------- /src/Executable.cpp: -------------------------------------------------------------------------------- 1 | #include "Executable.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | bool Executable::isExecutable(const QString& path) { 11 | QFileInfo fileInfo(path); 12 | return fileInfo.isExecutable(); 13 | } 14 | 15 | bool Executable::hasShebang(const QString& path) { 16 | QFile file(path); 17 | QFileInfo fileInfo(path); 18 | 19 | // If it is a directory, it cannot have a shebang, so return false 20 | if (fileInfo.isDir()) { 21 | qDebug() << "File is a directory, so it cannot have a shebang."; 22 | return false; 23 | } 24 | 25 | // If it is a symlink, we need to check the target 26 | if (fileInfo.isSymLink()) { 27 | qDebug() << "File is a symlink, so we need to check the target."; 28 | QString target = fileInfo.symLinkTarget(); 29 | qDebug() << "Target:" << target; 30 | return hasShebang(target); 31 | } 32 | 33 | qDebug() << "Checking file:" << path; 34 | if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { 35 | qWarning() << "Failed to open file:" << path; 36 | return false; 37 | } 38 | 39 | // Check if the file starts with the shebang sequence 40 | QByteArray firstTwoBytes = file.read(2); 41 | qDebug() << "First two bytes:" << firstTwoBytes; 42 | if (firstTwoBytes == "#!") { 43 | qDebug() << "File has a shebang."; 44 | // Exception: If the MIME type is e.g., "application/x-raw-disk-image", 45 | // then we ignore the shebang 46 | if (QMimeDatabase().mimeTypeForFile(path).name().contains("disk-image")) { 47 | qDebug() << "File is a disk image, so we ignore the shebang."; 48 | return false; 49 | } 50 | return true; 51 | } else { 52 | qDebug() << "File does not have a shebang."; 53 | return false; 54 | } 55 | } 56 | 57 | bool Executable::isElf(const QString& path) { 58 | QMimeDatabase mimeDatabase; 59 | QString mimeType = mimeDatabase.mimeTypeForFile(path).name(); 60 | // NOTE: Not all "application/..." mime types are ELF executables, e.g., disk images 61 | // have "application/..." mime types, too. 62 | if (mimeType == "application/x-executable" || \ 63 | mimeType == "application/x-pie-executable" || \ 64 | mimeType == "application/vnd-appimage") { 65 | qDebug() << "File is an ELF executable."; 66 | return true; 67 | } else { 68 | qDebug() << "File is not an ELF executable."; 69 | return false; 70 | } 71 | } 72 | 73 | bool Executable::askUserToMakeExecutable(const QString& path) { 74 | if (!isExecutable(path)) { 75 | QString message = tr("The file is not executable:\n%1\n\nDo you want to make it executable?\n\nYou should only do this if you trust this file.") 76 | .arg(path); 77 | QMessageBox::StandardButton response = QMessageBox::question(nullptr, tr("Make Executable"), message, 78 | QMessageBox::Yes | QMessageBox::No); 79 | 80 | if (response == QMessageBox::Yes) { 81 | 82 | QProcess process; 83 | QStringList arguments; 84 | arguments << "+x" << path; 85 | 86 | process.setProgram("chmod"); 87 | process.setArguments(arguments); 88 | 89 | process.start(); 90 | if (process.waitForFinished() && process.exitCode() == 0) { 91 | // QMessageBox::information(nullptr, tr("Success"), tr("File is now executable.")); 92 | return true; 93 | } else { 94 | QMessageBox::warning(nullptr, tr("Error"), tr("Failed to make the file executable.")); 95 | return false; 96 | } 97 | } else { 98 | // QMessageBox::information(nullptr, tr("Info"), tr("File was not made executable.")); 99 | return false; 100 | } 101 | } else { 102 | // QMessageBox::information(nullptr, tr("Info"), tr("The file is already executable.")); 103 | return true; 104 | } 105 | } 106 | 107 | bool Executable::hasShebangOrIsElf(const QString& path) { 108 | if (hasShebang(path)) { 109 | qDebug() << tr("File has a shebang."); 110 | return true; 111 | } else if (isElf(path)) { 112 | qDebug() << tr("File is an ELF."); 113 | return true; 114 | } else { 115 | qDebug() << tr("File does not have a shebang or is not an ELF."); 116 | return false; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Executable.h: -------------------------------------------------------------------------------- 1 | #ifndef EXECUTABLE_H 2 | #define EXECUTABLE_H 3 | 4 | #include 5 | #include 6 | 7 | /** 8 | * @file Executable.h 9 | * @class Executable 10 | * @brief A class to provide utility methods related to ELF executables and interpreted scripts. 11 | */ 12 | class Executable : public QObject { 13 | Q_OBJECT 14 | public: 15 | /** 16 | * Check if a file is executable. 17 | * 18 | * @param path The path to the file. 19 | * @return True if the file is executable, false otherwise. 20 | */ 21 | static bool isExecutable(const QString& path); 22 | 23 | /** 24 | * Check if a file has a shebang line. 25 | * 26 | * @param path The path to the file. 27 | * @return True if the file has a shebang, false otherwise. 28 | */ 29 | static bool hasShebang(const QString& path); 30 | 31 | /** 32 | * Check if a file has a shebang line or is an ELF executable. 33 | * 34 | * @param path The path to the file. 35 | * @return True if the file has a shebang or is an ELF, false otherwise. 36 | */ 37 | static bool hasShebangOrIsElf(const QString& path); 38 | 39 | /** 40 | * Check if a file is an ELF executable. 41 | * 42 | * @param path The path to the file. 43 | * @return True if the file is an ELF, false otherwise. 44 | */ 45 | static bool isElf(const QString& path); 46 | 47 | /** 48 | * @brief Ask the user if they want to make a file executable and perform the action if requested. 49 | * 50 | * This method displays a dialog asking the user if they want to make the specified file executable. 51 | * If the user agrees, the file's permissions are modified accordingly. 52 | * 53 | * @param path The path to the file. 54 | * @return True if the file is now executable or already executable, false otherwise. 55 | * If an error occurs during permission modification, false is returned as well. 56 | */ 57 | static bool askUserToMakeExecutable(const QString& path); 58 | }; 59 | 60 | #endif // EXECUTABLE_H 61 | -------------------------------------------------------------------------------- /src/bundle-thumbnailer.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "DbManager.h" 7 | 8 | int main(int argc, char *argv[]) 9 | { 10 | QCoreApplication a(argc, argv); 11 | 12 | if (argc < 2) { 13 | qCritical() << "USAGE:" << argv[0] << "-p\nto print all known applications on this system"; 14 | return 1; 15 | } 16 | 17 | QString path = argv[1]; 18 | QString canonicalPath = QDir(path).canonicalPath(); 19 | 20 | // For speed reasons, exit as fast as possible if we are not working on an 21 | // application 22 | if (!(path == "-p" || canonicalPath.endsWith(".app") || canonicalPath.endsWith(".AppDir") 23 | || canonicalPath.endsWith(".AppImage"))) 24 | return 0; 25 | 26 | DbManager db; 27 | 28 | if (path == "-p") { 29 | const QStringList allApps = db.allApplications(); 30 | for (QString app : allApps) { 31 | qWarning() << app; 32 | } 33 | return 0; 34 | } 35 | 36 | db.handleApplication(canonicalPath); 37 | 38 | return 0; 39 | } 40 | -------------------------------------------------------------------------------- /src/extattrs.cpp: -------------------------------------------------------------------------------- 1 | #include "extattrs.h" 2 | 3 | #include // for checking BSD definition 4 | #if defined(BSD) 5 | # include 6 | #else 7 | # include 8 | # include 9 | #endif 10 | 11 | #include 12 | // #include 13 | #include 14 | 15 | #define XATTR_NAMESPACE "user" 16 | 17 | using namespace Fm; 18 | 19 | namespace Fm { 20 | 21 | static const int ATTR_VAL_SIZE = 20480; // FIXME: Can we do without a predetermined size? 22 | // ANSWER: Yes, by calling extattr_get_file twice; see 23 | // https://github.com/probonopd/Filer/blob/da3499361150215f9b5dc6cd3d21165a9025e5f5/src/ExtendedAttributes.cpp#L110-L149 24 | // TODO: Use that 25 | // 26 | // If this size is too small, then reading extattr fails, leading to strange unexpected errors 27 | // including segfaults of 'launch', preventing the desktop from starting up 28 | // 256 was not enough to read, e.g., 'can-open' containing many MIME types; 29 | // 2048 was still not enough to handle, e.g., org.shotcut.Shotcut.desktop 30 | 31 | /* 32 | * get the attibute value from the extended attribute for the path as int 33 | */ 34 | int getAttributeValueInt(const QString &path, const QString &attribute, bool &ok) 35 | { 36 | int value = 0; 37 | 38 | // get the value from the extended attribute for the path 39 | char data[ATTR_VAL_SIZE]; 40 | #if defined(BSD) 41 | ssize_t bytesRetrieved = extattr_get_file(path.toLatin1().data(), EXTATTR_NAMESPACE_USER, 42 | attribute.toLatin1().data(), data, ATTR_VAL_SIZE); 43 | #else 44 | QString namespacedAttr; 45 | namespacedAttr.append(XATTR_NAMESPACE).append(".").append(attribute); 46 | ssize_t bytesRetrieved = 47 | getxattr(path.toLatin1().data(), namespacedAttr.toLatin1().data(), data, ATTR_VAL_SIZE); 48 | #endif 49 | // check if we got the attribute value 50 | if (bytesRetrieved <= 0) 51 | ok = false; 52 | else { 53 | // convert the value to int via QString 54 | QString strValue(data); 55 | bool intOK; 56 | int val = strValue.toInt(&intOK); 57 | if (intOK) { 58 | ok = true; 59 | value = val; 60 | } 61 | } 62 | return value; 63 | } 64 | 65 | /* 66 | * set the attibute value in the extended attribute for the path as int 67 | */ 68 | bool setAttributeValueInt(const QString &path, const QString &attribute, int value) 69 | { 70 | // set the value from the extended attribute for the path 71 | const QString data = QString::number(value); 72 | return setAttributeValueQString(path, attribute, data); 73 | } 74 | 75 | /* 76 | * get the attibute value from the extended attribute for the path as QString 77 | */ 78 | QString getAttributeValueQString(const QString &path, const QString &attribute, bool &ok) 79 | { 80 | // get the value from the extended attribute for the path 81 | char data[ATTR_VAL_SIZE]; 82 | #if defined(BSD) 83 | ssize_t bytesRetrieved = extattr_get_file(path.toLatin1().data(), EXTATTR_NAMESPACE_USER, 84 | attribute.toLatin1().data(), data, ATTR_VAL_SIZE); 85 | #else 86 | QString namespacedAttr; 87 | namespacedAttr.append(XATTR_NAMESPACE).append(".").append(attribute); 88 | ssize_t bytesRetrieved = 89 | getxattr(path.toLatin1().data(), namespacedAttr.toLatin1().data(), data, ATTR_VAL_SIZE); 90 | #endif 91 | // check if we got the attribute value 92 | if (bytesRetrieved < 0) // If this is 0, then the value is empty but the extattr is set. If this 93 | // is < 0, extattr is not set 94 | ok = false; 95 | else { 96 | // convert the value to QString 97 | data[bytesRetrieved] = 0; 98 | QString strValue; 99 | strValue = QString::fromStdString(data); 100 | strValue = strValue.trimmed(); 101 | ok = true; 102 | return strValue; 103 | } 104 | return nullptr; 105 | } 106 | 107 | /* 108 | * set the attibute value in the extended attribute for the path as QString 109 | */ 110 | bool setAttributeValueQString(const QString &path, const QString &attribute, const QString &value) 111 | { 112 | // set the value from the extended attribute for the path 113 | /* 114 | QString candidateProgram = QStandardPaths::findExecutable("setextattr"); // FreeBSD 115 | if (candidateProgram.isEmpty()) 116 | QStandardPaths::findExecutable("setxattr"); // Linux 117 | if (candidateProgram.isEmpty()) { 118 | qCritical() << "Did not find setextattr nor setxattr, cannot set extended attribute"; 119 | return false; 120 | } 121 | QProcess p; 122 | p.setProgram(QStandardPaths::findExecutable(candidateProgram)); 123 | p.setArguments({ "user", attribute, value, path }); 124 | p.start(); 125 | p.waitForFinished(); 126 | if (p.exitCode() != 0) { 127 | qDebug() << "Failed to run command:" << p.program() << p.arguments(); 128 | return false; 129 | } 130 | return true; 131 | */ 132 | // The following does not work on read-only files, e.g., at /usr 133 | #if defined(BSD) 134 | ssize_t bytesSet = extattr_set_file(path.toLatin1().data(), EXTATTR_NAMESPACE_USER, 135 | attribute.toLatin1().data(), value.toLatin1().data(), 136 | value.length() + 1); // include \0 termination char 137 | // check if we set the attribute value 138 | return (bytesSet > 0); 139 | #else 140 | QString namespacedAttr; 141 | namespacedAttr.append(XATTR_NAMESPACE).append(".").append(attribute); 142 | int success = setxattr(path.toLatin1().data(), 143 | namespacedAttr.toLatin1().data(), 144 | value.toLatin1().data(), value.length() + 1, 0); // include \0 termination char 145 | // check if we set the attribute value 146 | return (success == 0); 147 | #endif 148 | } 149 | 150 | } // namespace Fm 151 | -------------------------------------------------------------------------------- /src/extattrs.h: -------------------------------------------------------------------------------- 1 | // File added by probono 2 | 3 | #ifndef EXTATTRS_H 4 | #define EXTATTRS_H 5 | 6 | #include 7 | 8 | namespace Fm { 9 | int getAttributeValueInt(const QString &path, const QString &attribute, bool &ok); 10 | bool setAttributeValueInt(const QString &path, const QString &attribute, int value); 11 | QString getAttributeValueQString(const QString &path, const QString &attribute, bool &ok); 12 | bool setAttributeValueQString(const QString &path, const QString &attribute, const QString &value); 13 | } // namespace Fm 14 | 15 | #endif // EXTATTRS_H 16 | -------------------------------------------------------------------------------- /src/launch.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "launcher.h" 4 | 5 | /* 6 | * All documents shall be opened through this tool on helloDesktop 7 | * 8 | * This tool handles four types of applications: 9 | * 1. Executables 10 | * 2. Simplified .app bundles (Convention: AppName.app/AppName is an executable) 11 | * 3. Simplified .AppDir directories (Convention: AppName[.AppDir]/AppRun is an executable) 12 | * 4. AppName.desktop files 13 | * 14 | * The applications are searched 15 | * 1. At the path given as the first argument 16 | * 2. On the $PATH 17 | * 3. In launch.db 18 | * 4. As a fallback, via Baloo? (not implemented yet) 19 | * 20 | * launch.db is populated 21 | * 1. By this tool (currently each time it is invoked; could be optimized to: only if application is 22 | not found) 23 | * 2. By the file manager when one looks at applications (can be implemented natively or using 24 | bundle-thumbnailer) 25 | * 26 | * If an application bundle is being launched that has existing windows and no arguments are passed 27 | in, 28 | * the existing windows are brought to the front instead of starting a new process. 29 | * 30 | * The environment variable LAUNCHED_EXECUTABLE is populated, and for bundles, the LAUNCHED_BUNDLE 31 | * environment variable is also polulated. 32 | * 33 | * Usage: 34 | * launch [] Launch the specified application 35 | 36 | Similar to https://github.com/probonopd/appwrapper and GNUstep openapp 37 | 38 | TODO: 39 | * Possibly optimize for launch speed by only populating launch.db if the application is not found 40 | * Make the behavior resemble /usr/local/GNUstep/System/Tools/openapp (a bash script) 41 | 42 | user@FreeBSD$ /usr/local/GNUstep/System/Tools/openapp --help 43 | usage: openapp [--find] [--debug] application [arguments...] 44 | 45 | application is the complete or relative name of the application 46 | program with or without the .app extension, like Ink.app. 47 | 48 | [arguments...] are the arguments to the application. 49 | 50 | If --find is used, openapp prints out the full path of the application 51 | executable which would be executed, without actually executing it. It 52 | will also list all paths that are attempted. 53 | 54 | */ 55 | 56 | int main(int argc, char *argv[]) 57 | { 58 | 59 | QApplication app(argc, argv); 60 | 61 | Launcher *launcher = new Launcher(); 62 | 63 | // Setting a busy cursor in this way seems only to affect the own application's windows 64 | // rather than the full screen, which is why it is not suitable for this tool 65 | // QApplication::setOverrideCursor(Qt::WaitCursor); 66 | 67 | // Launch an application but initially watch for errors and display a Qt error message 68 | // if needed. After some timeout, detach the process of the application, and exit this helper 69 | 70 | QStringList args = app.arguments(); 71 | 72 | args.pop_front(); 73 | 74 | launcher->discoverApplications(); 75 | 76 | if (QFileInfo(argv[0]).fileName() == "launch") { 77 | if (args.isEmpty()) { 78 | qCritical() << "USAGE:" << argv[0] << " []"; 79 | exit(1); 80 | } 81 | return launcher->launch(args); 82 | } 83 | 84 | if (QFileInfo(argv[0]).fileName().endsWith("open")) { 85 | if (args.isEmpty()) { 86 | qCritical() << "USAGE:" << argv[0] << ""; 87 | exit(1); 88 | } 89 | return launcher->open(args); 90 | } 91 | 92 | return 1; 93 | } 94 | -------------------------------------------------------------------------------- /src/launcher.cpp: -------------------------------------------------------------------------------- 1 | #include "launcher.h" 2 | #include "ApplicationSelectionDialog.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "Executable.h" 9 | #include 10 | 11 | Launcher::Launcher() : db(new DbManager()) { } 12 | 13 | Launcher::~Launcher() 14 | { 15 | db->~DbManager(); 16 | } 17 | 18 | // If a package needs to be updated, tell the user how to do this, 19 | // or even offer to do it 20 | QString Launcher::getPackageUpdateCommand(QString pathToInstalledFile) 21 | { 22 | QString candidate = QStandardPaths::findExecutable("pkg"); 23 | if (candidate != "") { 24 | // We are on FreeBSD 25 | QProcess p; 26 | QString exe = "pkg which " + pathToInstalledFile; 27 | p.start(exe); 28 | p.waitForFinished(); 29 | QString output(p.readAllStandardOutput()); 30 | if (output != "") { 31 | qDebug() << output; 32 | QRegExp rx(".* was installed by package (.*)-(.*)\n"); 33 | if (rx.indexIn(output) == 0) { 34 | return QString("sudo pkg install %1").arg(rx.cap(1)); 35 | } 36 | } 37 | } 38 | // TODO: Implement the same for deb and rpm... 39 | // In all other cases, return a blank string 40 | return ""; 41 | } 42 | 43 | // Translate cryptic errors into clear text, and possibly even offer buttons to 44 | // take action 45 | void Launcher::handleError(QDetachableProcess *p, QString errorString) 46 | { 47 | QMessageBox qmesg; 48 | 49 | QFileInfo fi(p->program()); 50 | QString title = fi.completeBaseName(); // https://doc.qt.io/qt-5/qfileinfo.html#completeBaseName 51 | 52 | // Make this error message not appear in the Dock // FIXME: Does not work, 53 | // why? 54 | qmesg.setWindowFlag(Qt::SubWindow); 55 | 56 | // If we can't make the icon go away in the Dock, at least make it a 57 | // non-placeholder icon Not sure if the Dock is already reflecting the 58 | // WindowIcon correctly 59 | // FIXME: Does not work, why? 60 | QIcon icon = QApplication::style()->standardIcon(QStyle::SP_MessageBoxWarning); 61 | qmesg.setWindowIcon(icon); 62 | 63 | QRegExp rx(".*ld-elf.so.1: (.*): version (.*) required by (.*) not found.*"); 64 | QRegExp rxPy(".*ModuleNotFoundError: No module named '(.*)'.*"); 65 | 66 | if (errorString.contains("FATAL: kernel too old")) { 67 | QString cleartextString = 68 | "The Linux compatibility layer reports an older kernel version than " 69 | "what is required to run this application.\n\n" 70 | "Please run\nsudo sysctl compat.linux.osrelease=5.0.0\nand try again."; 71 | qmesg.warning(nullptr, title, cleartextString); 72 | } else if (errorString.contains("setuid_sandbox_host.cc")) { 73 | QString cleartextString = "Cannot run Chromium-based applications with a sandbox.\n" 74 | "Please try running it with the --no-sandbox argument."; 75 | qmesg.warning(nullptr, title, cleartextString); 76 | } else if (rx.indexIn(errorString) == 0) { 77 | QString outdatedLib = rx.cap(1); 78 | QString versionNeeded = rx.cap(2); 79 | QFile f(outdatedLib); 80 | QFileInfo fileInfo(f.fileName()); 81 | QString outdatedLibShort(fileInfo.fileName()); 82 | QString cleartextString = 83 | QString("%1 application requires at least version %2 of %3 to run.") 84 | .arg(title) 85 | .arg(versionNeeded) 86 | .arg(outdatedLibShort); 87 | if (getPackageUpdateCommand(outdatedLib) != "") { 88 | cleartextString.append(QString("\n\nPlease update it with\n%1\nand try again.") 89 | .arg(getPackageUpdateCommand(outdatedLib))); 90 | } else { 91 | cleartextString.append(QString("\n\nPlease update it and try again.") 92 | .arg(getPackageUpdateCommand(outdatedLib))); 93 | } 94 | qmesg.warning(nullptr, title, cleartextString); 95 | } else if (rxPy.indexIn(errorString) == 0) { 96 | QString missingPyModule = rxPy.cap(1); 97 | QString cleartextString = QString("%1 requires the Python module %2 to " 98 | "run.\n\nPlease install it and try again.") 99 | .arg(title) 100 | .arg(missingPyModule); 101 | qmesg.warning(nullptr, title, cleartextString); 102 | } else { 103 | 104 | QStringList lines = errorString.split("\n"); 105 | 106 | // Remove all lines from QStringList lines that contain "from LD_PRELOAD 107 | // cannot be preloaded" or "no version information available as these most 108 | // likely are not relevant to the user 109 | for (QStringList::iterator it = lines.begin(); it != lines.end();) { 110 | if ((*it).contains("from LD_PRELOAD cannot be preloaded") 111 | || (*it).contains("no version information available (required by")) { 112 | it = lines.erase(it); 113 | } else { 114 | ++it; 115 | } 116 | } 117 | 118 | QString cleartextString = lines.join("\n"); 119 | if (lines.length() > 10) { 120 | 121 | QString text = QObject::tr(QString("%1 has quit unexpectedly.\n\n\n").arg(title).toUtf8()); 122 | // Append non-breaking spaces to the text to increase width 123 | text.append(QString(100, QChar::Nbsp)); 124 | qmesg.setText(text); 125 | QString informativeText = QObject::tr("Error message:"); 126 | qmesg.setWindowTitle(title); 127 | qmesg.setDetailedText(cleartextString); 128 | qmesg.setIcon(QMessageBox::Warning); 129 | qmesg.setSizeGripEnabled(true); 130 | qmesg.exec(); 131 | } else { 132 | qmesg.warning(nullptr, title, cleartextString); 133 | } 134 | 135 | } 136 | } 137 | 138 | // Find apps on well-known paths and put them into launch.db 139 | void Launcher::discoverApplications() 140 | { 141 | // Measure the time it takes to look up candidates 142 | QElapsedTimer timer; 143 | timer.start(); 144 | AppDiscovery *ad = new AppDiscovery(db); 145 | QStringList wellKnownLocs = ad->wellKnownApplicationLocations(); 146 | ad->findAppsInside(wellKnownLocs); 147 | // Print to stdout how long it took to discover applications 148 | qDebug() << "Took" << timer.elapsed() 149 | << "milliseconds to discover applications and add them to " 150 | "launch.db, part of which was logging"; 151 | // ad->~AppDiscovery(); // FIXME: Doing this here would lead to a crash; why? 152 | } 153 | 154 | QStringList Launcher::executableForBundleOrExecutablePath(QString bundleOrExecutablePath) 155 | { 156 | QStringList executableAndArgs = {}; 157 | if (QFile::exists(bundleOrExecutablePath)) { 158 | QFileInfo info = QFileInfo(bundleOrExecutablePath); 159 | if (bundleOrExecutablePath.endsWith(".AppDir") || bundleOrExecutablePath.endsWith(".app")) { 160 | qDebug() << "# Found" << bundleOrExecutablePath; 161 | QString executable_candidate; 162 | if (bundleOrExecutablePath.endsWith(".AppDir")) { 163 | executable_candidate = bundleOrExecutablePath + "/AppRun"; 164 | } else { 165 | // The .app could be a symlink, so we need to determine the 166 | // nameWithoutSuffix from its target 167 | QString nameWithoutSuffix = QFileInfo(bundleOrExecutablePath).completeBaseName(); 168 | executable_candidate = bundleOrExecutablePath + "/" + nameWithoutSuffix; 169 | } 170 | QFileInfo candinfo = QFileInfo(executable_candidate); 171 | if (candinfo.isExecutable()) { 172 | executableAndArgs = QStringList({ executable_candidate }); 173 | } 174 | 175 | } else if (bundleOrExecutablePath.endsWith(".AppImage") 176 | || bundleOrExecutablePath.endsWith(".appimage")) { 177 | qDebug() << "# Found non-executable AppImage" << bundleOrExecutablePath; 178 | executableAndArgs = QStringList({ bundleOrExecutablePath }); 179 | } else if (bundleOrExecutablePath.endsWith(".desktop")) { 180 | qDebug() << "# Found .desktop file" << bundleOrExecutablePath; 181 | QSettings desktopFile(bundleOrExecutablePath, QSettings::IniFormat); 182 | QString s = desktopFile.value("Desktop Entry/Exec").toString(); 183 | QStringList execStringAndArgs = QProcess::splitCommand( 184 | s); // This should hopefully treat quoted strings halfway correctly 185 | if (execStringAndArgs.first().count(QLatin1Char('\\')) > 0) { 186 | QMessageBox::warning(nullptr, " ", 187 | "Launching such complex .desktop files is not supported yet.\n" 188 | + bundleOrExecutablePath); 189 | exit(1); 190 | } else { 191 | // Get the first element of the list, which is the executable, and look it up on the $PATH 192 | QString executable = execStringAndArgs.first(); 193 | if (! executable.contains("/")) { 194 | QString executablePath = QStandardPaths::findExecutable(executable); 195 | if (executablePath == "") { 196 | QMessageBox::warning(nullptr, 197 | QApplication::tr("Executable not found"), 198 | QApplication::tr("Could not find executable %1 on $PATH.\n%2") 199 | .arg(executable, bundleOrExecutablePath)); 200 | 201 | exit(1); 202 | } 203 | // Replace the first element of the list with the full path to the executable 204 | execStringAndArgs.replace(0, executablePath); 205 | } 206 | executableAndArgs = execStringAndArgs; 207 | } 208 | } else if (!info.isDir()) { 209 | if(Executable::hasShebangOrIsElf(bundleOrExecutablePath)) { 210 | if(info.isExecutable()) { 211 | qDebug() << "# Found executable" << bundleOrExecutablePath; 212 | executableAndArgs = QStringList({ bundleOrExecutablePath }); 213 | } else { 214 | qDebug() << "# Found non-executable" << bundleOrExecutablePath; 215 | bool success = Executable::askUserToMakeExecutable(bundleOrExecutablePath); 216 | if (!success) { 217 | exit(1); 218 | } else { 219 | executableAndArgs = QStringList({ bundleOrExecutablePath }); 220 | } 221 | } 222 | } 223 | } 224 | } 225 | return executableAndArgs; 226 | } 227 | 228 | QString Launcher::pathWithoutBundleSuffix(QString path) 229 | { 230 | // List of common bundle suffixes to remove 231 | QStringList bundleSuffixes = { ".AppDir", ".app", ".desktop", ".AppImage", ".appimage" }; 232 | 233 | QString cleanedPath = path; 234 | for (const QString &suffix : bundleSuffixes) { 235 | cleanedPath = cleanedPath.remove(QRegularExpression(suffix, QRegularExpression::CaseInsensitiveOption)); 236 | } 237 | 238 | return cleanedPath; 239 | } 240 | 241 | int Launcher::launch(QStringList args) 242 | { 243 | QDetachableProcess p; 244 | 245 | QString executable = nullptr; 246 | QString firstArg = args.first(); 247 | qDebug() << "launch firstArg:" << firstArg; 248 | 249 | QFileInfo fileInfo = QFileInfo(firstArg); 250 | QString nameWithoutSuffix = QFileInfo(fileInfo.completeBaseName()).fileName(); 251 | 252 | // Remove trailing slashes 253 | while (firstArg.endsWith("/")) { 254 | firstArg.remove(firstArg.length() - 1, 1); 255 | } 256 | 257 | args.pop_front(); 258 | 259 | // First, try to find something we can launch at the path that was supplied as 260 | // an argument, Examples: 261 | // /Applications/LibreOffice.app 262 | // /Applications/LibreOffice.AppDir 263 | // /Applications/LibreOffice.AppImage 264 | // /Applications/libreoffice 265 | 266 | QStringList e = executableForBundleOrExecutablePath(firstArg); 267 | if (e.length() > 0) { 268 | executable = e.first(); 269 | 270 | // Non-executable files should be handled by the open command, not the launch command. 271 | // But just in case, we check whether the file is lacking the executable bit. 272 | if(Executable::hasShebangOrIsElf(executable)) { 273 | QFileInfo info = QFileInfo(executable); 274 | if(! info.isExecutable()) { 275 | qDebug() << "# Found non-executable" << executable; 276 | bool success = Executable::askUserToMakeExecutable(executable); 277 | if (!success) { 278 | exit(1); 279 | } 280 | } 281 | } 282 | } 283 | 284 | // Second, try to find an executable file on the $PATH 285 | if (executable == nullptr) { 286 | QString candidate = QStandardPaths::findExecutable(firstArg); 287 | if (candidate != "") { 288 | qDebug() << "Found" << candidate << "on the $PATH"; 289 | executable = candidate; // Returns the absolute file path to the 290 | // executable, or an empty string if not found 291 | } 292 | } 293 | 294 | // Third, try to find an executable from the applications in launch.db 295 | 296 | QString selectedBundle = ""; 297 | if (executable == nullptr) { 298 | 299 | // Measure the time it takes to look up candidates 300 | QElapsedTimer timer; 301 | timer.start(); 302 | 303 | // Iterate recursively through locationsContainingApps searching for AppRun 304 | // files in matchingly named AppDirs 305 | 306 | QFileInfoList candidates; 307 | 308 | QStringList allAppsFromDb = db->allApplications(); 309 | 310 | for (QString appBundleCandidate : allAppsFromDb) { 311 | // Now that we may have collected different candidates, decide on which 312 | // one to use e.g., the one with the highest self-declared version number. 313 | // Also we need to check whether the appBundleCandidate exist 314 | // For now, just use the first one 315 | if (pathWithoutBundleSuffix(appBundleCandidate).endsWith(firstArg)) { 316 | if (QFileInfo(appBundleCandidate).exists()) { 317 | qDebug() << "Selected from launch.db:" << appBundleCandidate; 318 | selectedBundle = appBundleCandidate; 319 | break; 320 | } else { 321 | db->handleApplication(appBundleCandidate); // Remove from launch.db it 322 | // if it does not exist 323 | } 324 | } 325 | } 326 | 327 | // For the selectedBundle, get the launchable executable 328 | if (selectedBundle == "") { 329 | QMessageBox::warning(nullptr, " ", 330 | QString("The application '%1'\ncan't be launched " 331 | "because it can't be found.") 332 | .arg(firstArg)); 333 | // Remove the application from launch.db if the symlink points to a non-existing file 334 | db->handleApplication(firstArg); 335 | 336 | exit(1); 337 | } else { 338 | QStringList e = executableForBundleOrExecutablePath(selectedBundle); 339 | if (e.length() > 0) 340 | executable = e.first(); 341 | } 342 | } 343 | 344 | // .desktop files can have arguments in them, and we need to insert the 345 | // arguments given to launch on the command line into the arguments coming 346 | // from the desktop file. So we have to construct arguments from the desktop 347 | // file and from the command line Things like this make XDG overly complex! 348 | QStringList constructedArgs = {}; 349 | 350 | QStringList execLinePartsFromDesktopFile = executableForBundleOrExecutablePath(firstArg); 351 | if (execLinePartsFromDesktopFile.length() > 1) { 352 | execLinePartsFromDesktopFile.pop_front(); 353 | for (const QString &execLinePartFromDesktopFile : execLinePartsFromDesktopFile) { 354 | if (execLinePartFromDesktopFile == "%f" || execLinePartFromDesktopFile == "%u") { 355 | if (args.length() > 0) { 356 | constructedArgs.append(args[0]); 357 | } 358 | } else if (execLinePartFromDesktopFile == "%F" || execLinePartFromDesktopFile == "%U") { 359 | if (args.length() > 0) { 360 | constructedArgs.append(args); 361 | } 362 | } else { 363 | constructedArgs.append(execLinePartFromDesktopFile); 364 | } 365 | } 366 | args = constructedArgs; 367 | } 368 | 369 | // Proceed to launch application 370 | p.setProgram(executable); 371 | 372 | QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); 373 | qDebug() << "# Setting LAUNCHED_EXECUTABLE environment variable to" << executable; 374 | env.insert("LAUNCHED_EXECUTABLE", executable); 375 | QFileInfo info = QFileInfo(executable); 376 | 377 | p.setArguments(args); 378 | 379 | // Hint: LAUNCHED_EXECUTABLE and LAUNCHED_BUNDLE environment variables 380 | // can be gotten from X11 windows on FreeBSD with 381 | // procstat -e $(xprop | grep PID | cut -d " " -f 3) 382 | qDebug() << "info.canonicalFilePath():" << info.canonicalFilePath(); 383 | qDebug() << "executable:" << executable; 384 | env.remove("LAUNCHED_BUNDLE"); // So that nested launches won't leak LAUNCHED_BUNDLE 385 | // from parent to child application; works 386 | if (info.dir().absolutePath().toLower().endsWith(".appdir") 387 | || info.dir().absolutePath().toLower().endsWith(".app")) { 388 | qDebug() << "# Bundle directory (.app, .AppDir)" << info.dir().canonicalPath(); 389 | qDebug() << "# Setting LAUNCHED_BUNDLE environment variable to it"; 390 | env.insert("LAUNCHED_BUNDLE", 391 | info.dir().canonicalPath()); // Resolve symlinks so as to show 392 | // the real location 393 | } else if (fileInfo.canonicalFilePath().toLower().endsWith(".appimage")) { 394 | qDebug() << "# Bundle file (.AppImage)" << fileInfo.canonicalFilePath(); 395 | qDebug() << "# Setting LAUNCHED_BUNDLE environment variable to it"; 396 | env.insert("LAUNCHED_BUNDLE", 397 | fileInfo.canonicalFilePath()); // Resolve symlinks so as to show 398 | // the real location 399 | } else if (fileInfo.canonicalFilePath().endsWith(".desktop")) { 400 | qDebug() << "# Bundle file (.desktop)" << fileInfo.canonicalFilePath(); 401 | qDebug() << "# Setting LAUNCHED_BUNDLE environment variable to it"; 402 | env.insert("LAUNCHED_BUNDLE", 403 | fileInfo.canonicalFilePath()); // Resolve symlinks so as to show 404 | // the real location 405 | } 406 | // qDebug() << "# env" << env.toStringList(); 407 | p.setProcessEnvironment(env); 408 | qDebug() << "# p.processEnvironment():" << p.processEnvironment().toStringList(); 409 | 410 | p.setProcessChannelMode( 411 | QProcess::ForwardedOutputChannel); // Forward stdout onto the main process 412 | qDebug() << "# program:" << p.program(); 413 | qDebug() << "# args:" << args; 414 | 415 | // If user launches an application bundle that has existing windows, bring 416 | // those to the front instead of launching a second instance. 417 | // FIXME: Currently we are doing this only if launch has been invoked without 418 | // parameters for the application to be launched, so as to not break opening 419 | // documents with applications; we can only hope that such applications are 420 | // smart enough to open the supplied document using an already-running process 421 | // instance. This is clumsy; how to do it better? We could check whethe the 422 | // same application we are about to launch has windows open with the name/path 423 | // of the file about to be opened in _NET_WM_NAME (e.g., FeatherPad) but that 424 | // does not work reliably for all applictions, e.g.,"launch.html - Falkon" 425 | // TODO: Remove Menu special case here as soon as we can bring up its Search 426 | // box with D-Bus 427 | if (args.length() < 1 && env.contains("LAUNCHED_BUNDLE") && (firstArg != "Menu")) { 428 | qDebug() << "# Checking for existing windows"; 429 | const auto &windows = KWindowSystem::windows(); 430 | bool foundExistingWindow = false; 431 | for (WId wid : windows) { 432 | 433 | QString runningBundle = ApplicationInfo::bundlePathForWId(wid); 434 | if (runningBundle == env.value("LAUNCHED_BUNDLE")) { 435 | // Check if the user ID which the application is running under is the same user ID as is the current user 436 | // This is to avoid bringing to the front windows of other users (e.g., if we want to run as root) 437 | // FIXME: Find a way that works on all platforms and takes ~3 lines of code instead of ~20 438 | int pid = 0; 439 | Display *display = XOpenDisplay(nullptr); 440 | Atom type; 441 | int format; 442 | unsigned long nitems, bytes_after; 443 | unsigned char *prop; 444 | int status = XGetWindowProperty(display, wid, XInternAtom(display, "_NET_WM_PID", False), 0, 1, False, XA_CARDINAL, &type, &format, &nitems, &bytes_after, &prop); 445 | if (status == Success && prop) { 446 | pid = *(unsigned long *)prop; 447 | XFree(prop); 448 | } 449 | XCloseDisplay(display); 450 | qDebug() << "# _NET_WM_PID:" << pid; 451 | QProcess process; 452 | process.start("ps", QStringList() << "-p" << QString::number(pid) << "-o" << "user"); 453 | process.waitForFinished(-1); 454 | QString processOutput = process.readAllStandardOutput(); 455 | if (processOutput.split("\n").at(1) != qgetenv("USER")) { 456 | qDebug() << "# Not activating window" << wid << "because it is running under a different user ID"; 457 | continue; 458 | } 459 | 460 | foundExistingWindow = true; 461 | KWindowSystem::forceActiveWindow(wid); 462 | } 463 | } 464 | if (foundExistingWindow) { 465 | qDebug() << "# Activated existing windows instead of launching a new instance"; 466 | 467 | // We can't exit immediately or else te windows won't become active; 468 | // FIXME: Do this properly 469 | QTime dieTime = QTime::currentTime().addSecs(1); 470 | while (QTime::currentTime() < dieTime) 471 | QCoreApplication::processEvents(QEventLoop::AllEvents, 100); 472 | 473 | exit(0); 474 | } else { 475 | qDebug() << "# Did not find existing windows for LAUNCHED_BUNDLE" 476 | << env.value("LAUNCHED_BUNDLE"); 477 | } 478 | } else if (args.length() > 1) { 479 | qDebug() << "# Not checking for existing windows because arguments were " 480 | "passed to the application"; 481 | } else { 482 | qDebug() << "# Not checking for existing windows"; 483 | } 484 | 485 | // Start new process 486 | p.start(); 487 | 488 | // Tell Menu that an application is being launched 489 | QString bPath = ApplicationInfo::bundlePath(p.program()); 490 | 491 | // Blocks until process has started 492 | if (!p.waitForStarted()) { 493 | // The reason we ended up here may well be that the file has executable permissions despite 494 | // it not being an executable file, hence we can't launch it. So we try to open it with its 495 | // default application 496 | qDebug() << "# Failed to start process; trying to open it with its default application"; 497 | QStringList completeArgs = { firstArg }; 498 | completeArgs.append(args); 499 | open(completeArgs); 500 | exit(0); 501 | } 502 | 503 | if (env.value("LAUNCHED_BUNDLE") != "") { 504 | QString stringToBeDisplayed = QFileInfo(env.value("LAUNCHED_BUNDLE")).completeBaseName(); 505 | // For desktop files, we need to parse them... 506 | if (env.value("LAUNCHED_BUNDLE").endsWith(".desktop")) { 507 | QSettings desktopFile(env.value("LAUNCHED_BUNDLE"), QSettings::IniFormat); 508 | stringToBeDisplayed = desktopFile.value("Desktop Entry/Name").toString(); 509 | } 510 | 511 | if (QDBusConnection::sessionBus().isConnected()) { 512 | QDBusInterface iface("local.Menu", "/", "", QDBusConnection::sessionBus()); 513 | if (!iface.isValid()) { 514 | qDebug() << "D-Bus interface not valid"; 515 | } else { 516 | QDBusReply reply = iface.call("showApplicationName", stringToBeDisplayed); 517 | if (!reply.isValid()) { 518 | qDebug() << "D-Bus reply not valid"; 519 | } else { 520 | qDebug() << QString("D-Bus reply: %1\n").arg(reply.value()); 521 | } 522 | } 523 | } 524 | } 525 | 526 | p.waitForFinished(10 527 | * 1000); // Blocks until process has finished or timeout occured (x seconds) 528 | // Errors occuring thereafter will not be reported to the user in a message 529 | // box anymore. This should cover most errors like missing libraries, missing 530 | // interpreters, etc. 531 | 532 | if (p.state() == 0 and p.exitCode() != 0) { 533 | qDebug("Process is not running anymore and exit code was not 0"); 534 | QString error = p.readAllStandardError(); 535 | if (error.isEmpty()) { 536 | error = QString("%1 exited unexpectedly\nwith exit code %2") 537 | .arg(nameWithoutSuffix) 538 | .arg(p.exitCode()); 539 | } 540 | 541 | qDebug() << error; 542 | handleError(&p, error); 543 | 544 | // Tell Menu that an application is no more being launched 545 | if (QDBusConnection::sessionBus().isConnected()) { 546 | QDBusInterface iface("local.Menu", "/", "", QDBusConnection::sessionBus()); 547 | if (!iface.isValid()) { 548 | qDebug() << "D-Bus interface not valid"; 549 | } else { 550 | QDBusReply reply = iface.call("hideApplicationName"); 551 | if (!reply.isValid()) { 552 | qDebug() << "D-Bus reply not valid"; 553 | } else { 554 | qDebug() << QString("D-Bus reply: %1\n").arg(reply.value()); 555 | } 556 | } 557 | } 558 | 559 | exit(p.exitCode()); 560 | } 561 | 562 | // When we have made it all the way to here, add our application to the 563 | // launch.db 564 | // TODO: Similarly, when we are trying to launch the bundle but it is not 565 | // there anymore, then remove it from the launch.db 566 | if (env.contains("LAUNCHED_BUNDLE")) { 567 | db->handleApplication(env.value("LAUNCHED_BUNDLE")); 568 | } 569 | 570 | db->~DbManager(); 571 | 572 | p.waitForFinished(-1); 573 | 574 | // Is this a way to p.detach(); and return(0) 575 | // without crashing the payload application 576 | // when it writes to stderr? 577 | // https://github.com/helloSystem/launch/issues/4 578 | // TODO: Make these picked up by a crash reporter; maybe name them with the 579 | // pid? p.setStandardErrorFile("/tmp/launch_error"); 580 | // p.setStandardOutputFile("/tmp/launch_output"); 581 | // p.setInputChannelMode(QProcess::ManagedInputChannel); // "Unforward" 582 | 583 | // NOTE: 'daemon launch ...' will lead to '' in 'ps ax' output 584 | // after the following runs; does this have any negative impact other than 585 | // cosmetics? 586 | // p.detach(); 587 | return (0); 588 | } 589 | 590 | int Launcher::open(QStringList args) 591 | { 592 | 593 | bool showChooserRequested = false; 594 | if (args.first() == "--chooser") { 595 | args.pop_front(); 596 | showChooserRequested = true; 597 | } 598 | 599 | QString firstArg = args.first(); 600 | qDebug() << "open firstArg:" << firstArg; 601 | 602 | // Workaround for FreeBSD not being able to properly mount all AppImages 603 | // by using the "runappimage" helper that mounts the AppImage and then 604 | // executes its payload. 605 | // FIXME: This should go away as soon as possible. 606 | 607 | if (QSysInfo::kernelType() == "freebsd") { 608 | // Check if we have the "runappimage" helper 609 | QString runappimage = QStandardPaths::findExecutable("runappimage"); 610 | qDebug() << "runappimage:" << runappimage; 611 | if (! runappimage.isEmpty()) { 612 | if (firstArg.toLower().endsWith(".appimage")) { 613 | QFileInfo info = QFileInfo(firstArg); 614 | if (!info.isExecutable()) { 615 | args.insert(0, runappimage); 616 | firstArg = args.first(); 617 | } 618 | } 619 | } 620 | } 621 | 622 | QString appToBeLaunched = nullptr; 623 | QStringList 624 | removalCandidates = {}; // For applications that possibly don't exist on disk anymore 625 | 626 | // NOTE: magnet:?xt=urn:btih:... URLs do not contain ":/" 627 | if (!showChooserRequested && (!QFileInfo::exists(firstArg)) && (!firstArg.contains(":/")) 628 | && (!firstArg.contains(":?"))) { 629 | if (QFileInfo(firstArg).isSymLink()) { 630 | // Broken symlink 631 | // TODO: Offer to delete or fix broken symlinks 632 | QMessageBox::warning(nullptr, " ", 633 | QString("The symlink '%1'\ncan't be opened " 634 | "because\nthe target '%2'\ncan't be found.") 635 | .arg(firstArg) 636 | .arg(QFileInfo(firstArg).symLinkTarget())); 637 | } else { 638 | // File not found 639 | QMessageBox::warning( 640 | nullptr, " ", 641 | QString("'%1'\ncan't be opened because it can't be found.").arg(firstArg)); 642 | } 643 | exit(1); 644 | } 645 | 646 | // Check whether the file to be opened is an ELF executable or a script missing the executable bit 647 | if(!showChooserRequested && Executable::hasShebangOrIsElf(firstArg)) { 648 | QStringList executableAndArgs; 649 | QFileInfo info = QFileInfo(firstArg); 650 | if(info.isExecutable()) { 651 | qDebug() << "# Found executable" << firstArg; 652 | exit(launch(args)); 653 | } else { 654 | qDebug() << "# Found non-executable" << firstArg; 655 | bool success = Executable::askUserToMakeExecutable(firstArg); 656 | if (!success) { 657 | exit(1); 658 | } else { 659 | exit(launch(args)); 660 | } 661 | } 662 | } 663 | 664 | // Check whether the file to be opened specifies an application it wants to be 665 | // opened with 666 | bool ok = false; 667 | QString openWith = Fm::getAttributeValueQString(firstArg, "open-with", ok); 668 | if (ok && !showChooserRequested) { 669 | // NOTE: For security reasons, the application must be known to the system 670 | // so that totally random commands won't get executed. 671 | // This could 672 | // possibly be made more sophisticated by allowing the open-with 673 | // value to any kind of string that 'launch' knows to open; 674 | // to be decided. Behavior might change in the future. 675 | if (db->applicationExists(openWith)) { 676 | appToBeLaunched = openWith; 677 | } 678 | } 679 | 680 | QString mimeType; 681 | if (appToBeLaunched.isNull()) { 682 | // Get MIME type of file to be opened 683 | mimeType = QMimeDatabase().mimeTypeForFile(firstArg).name(); 684 | 685 | // Handle legacy XDG style "file:///..." URIs 686 | // by converting them to sane "/...". Example: Falkon downloads being 687 | // double-clicked 688 | if (firstArg.startsWith("file://")) { 689 | firstArg = QUrl::fromEncoded(firstArg.toUtf8()).toLocalFile(); 690 | } 691 | 692 | // Handle legacy XDG style "computer:///LIVE.mount" mount points 693 | // by converting them to sane "/media/LIVE". For legacy compatibility 694 | // reasons only 695 | if ((firstArg.startsWith("computer://")) && (firstArg.endsWith(".mount"))) { 696 | appToBeLaunched = "Filer"; 697 | firstArg.replace("computer://", "").replace(".mount", ""); 698 | firstArg = "/media" + firstArg; 699 | } 700 | 701 | // Do this AFTER the special cases like "file://" and "computer://" have 702 | // already been handled 703 | // NOTE: magnet:?xt=urn:btih:... URLs do not contain ":/" 704 | if (firstArg.contains(":/") || firstArg.contains(":?")) { 705 | QUrl url = QUrl(firstArg); 706 | qDebug() << "Protocol" << url.scheme(); 707 | mimeType = "x-scheme-handler/" + url.scheme(); 708 | } 709 | 710 | // Stop stealing applications (like code-oss) from claiming folders 711 | if (mimeType.startsWith("inode/")) { 712 | appToBeLaunched = "Filer"; 713 | } 714 | 715 | // Empty files are reported as 'application/x-zerosize' here, but as 716 | // "inode/x-empty" by 'file' so treat them as empty text files; TODO: Better 717 | // ideas, anyone? 718 | if (mimeType == "application/x-zerosize" || mimeType == "inode/x-empty") { 719 | mimeType = "text/plain"; 720 | } 721 | 722 | qDebug() << "File to be opened has MIME type:" << mimeType; 723 | 724 | // Do not attempt to open file types which are known to have no useful 725 | // applications; please let us know if you have better ideas for what to do 726 | // with those 727 | QStringList blacklistedMimeTypes = { "application/octet-stream" }; 728 | for (const QString blacklistedMimeType : blacklistedMimeTypes) { 729 | if ((mimeType == blacklistedMimeType) && (!firstArg.contains(":/"))) { 730 | QMessageBox::warning( 731 | nullptr, " ", 732 | QString("Cannot open %1\nof MIME type '%2'.").arg(firstArg, mimeType)); 733 | exit(1); 734 | } 735 | } 736 | 737 | // Do not open .desktop files; instead, launch them 738 | if (mimeType == "application/x-desktop") { 739 | QStringList argsForLaunch = { firstArg }; 740 | argsForLaunch.append(args); 741 | return launch(argsForLaunch); 742 | } 743 | 744 | // Check whether there is a symlink in ~/.local/share/launch/MIME/<...>/Default 745 | // pointing to an application that exists on disk; if yes, then use that 746 | if (!showChooserRequested) { 747 | QString mimePath = QString("%1/%2") 748 | .arg(db->localShareLaunchMimePath) 749 | .arg(mimeType.replace("/", "_")); 750 | QString defaultPath = QString("%1/Default").arg(mimePath); 751 | if (QFileInfo::exists(defaultPath)) { 752 | QString defaultApp = QFileInfo(defaultPath).symLinkTarget(); 753 | if (QFileInfo::exists(defaultApp)) { 754 | appToBeLaunched = defaultApp; 755 | } else { 756 | // The symlink is broken 757 | removalCandidates.append(defaultApp); 758 | } 759 | } 760 | // If there is no Default symlink, check how many symlinks there are in 761 | // the directory and if there is only one, use that 762 | else { 763 | QDirIterator it(mimePath, QDir::Files | QDir::NoDotAndDotDot); 764 | int count = 0; 765 | while (it.hasNext()) { 766 | it.next(); 767 | count++; 768 | if (count > 1) 769 | break; 770 | } 771 | if (count == 1) { 772 | // Check whether the only file in the directory is a symlink and whether it 773 | // points to an application that exists on disk 774 | QString onlyFile = it.filePath(); 775 | if (QFileInfo(onlyFile).isSymLink()) { 776 | QString onlyApp = QFileInfo(onlyFile).symLinkTarget(); 777 | if (QFileInfo::exists(onlyApp)) { 778 | appToBeLaunched = onlyApp; 779 | } else { 780 | // The symlink is broken 781 | removalCandidates.append(onlyApp); 782 | } 783 | } 784 | } 785 | } 786 | } 787 | 788 | if (appToBeLaunched.isNull()) { 789 | QStringList appCandidates; 790 | QStringList fallbackAppCandidates; // Those where only the first part of 791 | // the MIME type before the "/" matches 792 | const QStringList allApps = db->allApplications(); 793 | for (const QString &app : allApps) { 794 | 795 | QStringList canOpens; 796 | if (db->filesystemSupportsExtattr) { 797 | bool ok = false; 798 | canOpens = Fm::getAttributeValueQString(app, "can-open", ok).split(";"); 799 | if (!ok) { 800 | if (!removalCandidates.contains(app)) 801 | removalCandidates.append(app); 802 | continue; 803 | } 804 | } else { 805 | canOpens = db->getCanOpenFromFile(app).split(";"); 806 | } 807 | 808 | for (const QString &canOpen : canOpens) { 809 | if (canOpen == mimeType) { 810 | qDebug() << app << "can open" << canOpen; 811 | if (!appCandidates.contains(app)) 812 | appCandidates.append(app); 813 | } 814 | if (canOpen.split("/").first() == mimeType.split("/").first()) { 815 | qDebug() << app << "can open" << canOpen.split("/").first(); 816 | if (!fallbackAppCandidates.contains(app)) 817 | fallbackAppCandidates.append(app); 818 | } 819 | } 820 | } 821 | 822 | qDebug() << "appCandidates:" << appCandidates; 823 | 824 | // Falling back to applications where only the first part of the MIME type 825 | // before the "/" matches; this does not make sense for x-scheme-handler 826 | // though 827 | if ((appCandidates.length() < 1) 828 | && (mimeType.split("/").first() != "x-scheme-handler")) { 829 | qDebug() << "fallbackAppCandidates:" << fallbackAppCandidates; 830 | appCandidates = fallbackAppCandidates; 831 | } 832 | 833 | QString fileOrProtocol = QFileInfo(firstArg).canonicalFilePath(); 834 | if (firstArg.contains(":/")) { 835 | fileOrProtocol = firstArg; 836 | } 837 | 838 | if (showChooserRequested || appCandidates.length() < 1) { 839 | ApplicationSelectionDialog *dlg = 840 | new ApplicationSelectionDialog(&fileOrProtocol, &mimeType, true, false, nullptr); 841 | auto result = dlg->exec(); 842 | if (result == QDialog::Accepted) 843 | appToBeLaunched = dlg->getSelectedApplication(); 844 | else 845 | exit(0); 846 | } else { 847 | appToBeLaunched = appCandidates[0]; 848 | } 849 | qDebug() << "appToBeLaunched" << appToBeLaunched; 850 | } 851 | } 852 | // Garbage collect launch.db: Remove applications that are no longer on the 853 | // filesystem 854 | for (const QString removalCandidate : removalCandidates) { 855 | db->handleApplication(removalCandidate); 856 | } 857 | 858 | // TODO: Prioritize which of the applications that can handle this 859 | // file should get to open it. For now we ust just the first one we find 860 | // const QStringList arguments = QStringList({appCandidates[0], path}); 861 | return launch({ appToBeLaunched, firstArg }); 862 | } -------------------------------------------------------------------------------- /src/launcher.h: -------------------------------------------------------------------------------- 1 | #ifndef LAUNCHER_H 2 | #define LAUNCHER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | #include 30 | 31 | #include "DbManager.h" 32 | #include "ApplicationInfo.h" 33 | #include "AppDiscovery.h" 34 | #include "extattrs.h" 35 | 36 | class QDetachableProcess : public QProcess 37 | 38 | { 39 | public: 40 | QDetachableProcess(QObject *parent = 0) : QProcess(parent) { } 41 | void detach() 42 | { 43 | this->waitForStarted(); 44 | setProcessState(QProcess::NotRunning); 45 | // qDebug() << "Detaching process"; 46 | } 47 | }; 48 | 49 | class Launcher 50 | { 51 | public: 52 | Launcher(); 53 | ~Launcher(); 54 | 55 | void discoverApplications(); 56 | int launch(QStringList args); 57 | int open(const QStringList args); 58 | 59 | private: 60 | DbManager *db; 61 | void handleError(QDetachableProcess *p, QString errorString); 62 | QString getPackageUpdateCommand(QString pathToInstalledFile); 63 | QStringList executableForBundleOrExecutablePath(QString bundleOrExecutablePath); 64 | QString pathWithoutBundleSuffix(QString path); 65 | }; 66 | 67 | #endif // LAUNCHER_H 68 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.15) 2 | 3 | project("test") 4 | 5 | # Find the Qt5 package 6 | find_package(Qt5 REQUIRED COMPONENTS Test Gui Widgets) 7 | 8 | set(CMAKE_AUTOMOC ON) 9 | set(CMAKE_AUTORCC ON) 10 | set(CMAKE_AUTOUIC ON) 11 | 12 | include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../src/) 13 | 14 | # Find the Qt5 package and its components 15 | find_package(Qt5 REQUIRED COMPONENTS Test Gui Widgets) 16 | 17 | # Set up your project sources and headers 18 | set(SOURCES 19 | testExecutable.cpp 20 | ${CMAKE_CURRENT_SOURCE_DIR}/../src/Executable.h 21 | ${CMAKE_CURRENT_SOURCE_DIR}/../src/Executable.cpp 22 | ) 23 | 24 | # Add the executable for your tests 25 | add_executable(${PROJECT_NAME} ${SOURCES}) 26 | 27 | # Link against Qt libraries 28 | target_link_libraries(${PROJECT_NAME} PRIVATE Qt5::Test Qt5::Gui Qt5::Widgets) 29 | 30 | # Define a CTest test 31 | add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}_tests) -------------------------------------------------------------------------------- /tests/CTestTestfile.cmake: -------------------------------------------------------------------------------- 1 | # Define a CTest test 2 | add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) 3 | -------------------------------------------------------------------------------- /tests/errortest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | 5 | for i in range(50): 6 | print("error output", i, file=sys.stderr) 7 | 8 | sys.exit(1) -------------------------------------------------------------------------------- /tests/testExecutable.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "Executable.h" 5 | 6 | class TestExecutable : public QObject { 7 | Q_OBJECT 8 | 9 | private slots: 10 | void testIsExecutable() { 11 | QVERIFY(Executable::isExecutable("/usr/bin/env")); 12 | QVERIFY(!Executable::isExecutable("/etc/os-release")); 13 | } 14 | 15 | void testHasShebang() { 16 | QVERIFY(Executable::hasShebang("/usr/bin/bg")); 17 | QVERIFY(!Executable::hasShebang("/etc/os-release")); 18 | } 19 | 20 | void testHasShebangOrIsElf() { 21 | QVERIFY(Executable::hasShebangOrIsElf("/usr/bin/bg")); 22 | QVERIFY(Executable::hasShebangOrIsElf("/usr/bin/env")); 23 | QVERIFY(!Executable::hasShebangOrIsElf("/etc/os-release")); 24 | } 25 | 26 | }; 27 | 28 | QTEST_APPLESS_MAIN(TestExecutable) 29 | 30 | #include "testExecutable.moc" 31 | --------------------------------------------------------------------------------