├── include ├── filechooser.h ├── config.h ├── logger.h └── xdpw.h ├── contrib ├── config.sample ├── systemd │ └── xdg-desktop-portal-termfilechooser.service.in ├── fzf-wrapper.sh ├── lf-wrapper.sh ├── yazi-wrapper.sh ├── vifm-wrapper.sh ├── ranger-wrapper.sh └── nnn-wrapper.sh ├── org.freedesktop.impl.portal.desktop.termfilechooser.service.in ├── termfilechooser.portal ├── .editorconfig ├── meson_options.txt ├── remove_legacy_file.sh ├── .gitignore ├── LICENSE ├── src ├── core │ ├── request.c │ ├── logger.c │ ├── main.c │ └── config.c └── filechooser │ └── filechooser.c ├── xdg-desktop-portal-termfilechooser.5.scd ├── meson.build └── README.md /include/filechooser.h: -------------------------------------------------------------------------------- 1 | #ifndef FILECHOOSER_H 2 | #define FILECHOOSER_H 3 | 4 | 5 | #endif 6 | -------------------------------------------------------------------------------- /contrib/config.sample: -------------------------------------------------------------------------------- 1 | [filechooser] 2 | cmd=/usr/share/xdg-desktop-portal-termfilechooser/yazi-wrapper.sh 3 | -------------------------------------------------------------------------------- /org.freedesktop.impl.portal.desktop.termfilechooser.service.in: -------------------------------------------------------------------------------- 1 | [D-BUS Service] 2 | Name=org.freedesktop.impl.portal.desktop.termfilechooser 3 | Exec=@libexecdir@/xdg-desktop-portal-termfilechooser 4 | @systemd_service@ 5 | -------------------------------------------------------------------------------- /termfilechooser.portal: -------------------------------------------------------------------------------- 1 | [portal] 2 | DBusName=org.freedesktop.impl.portal.desktop.termfilechooser 3 | Interfaces=org.freedesktop.impl.portal.FileChooser; 4 | UseIn=i3;wlroots;sway;Hyprland;Wayfire;river;mate;lxde;openbox;unity;pantheon 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | indent_style = space 9 | indent_size = 2 10 | 11 | [*.scd] 12 | indent_style=tab 13 | 14 | [*.{xml,yml}] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option('sd-bus-provider', type: 'combo', choices: ['auto', 'libsystemd', 'libelogind', 'basu'], value: 'auto', description: 'Provider of the sd-bus library') 2 | option('systemd', type: 'feature', value: 'auto', description: 'Install systemd user service unit') 3 | option('man-pages', type: 'feature', value: 'auto', description: 'Generate and install man pages') 4 | -------------------------------------------------------------------------------- /contrib/systemd/xdg-desktop-portal-termfilechooser.service.in: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Portal service (terminal file chooser implementation) 3 | PartOf=graphical-session.target 4 | After=graphical-session.target 5 | 6 | [Service] 7 | Type=dbus 8 | BusName=org.freedesktop.impl.portal.desktop.termfilechooser 9 | ExecStart=@libexecdir@/xdg-desktop-portal-termfilechooser 10 | Restart=on-failure 11 | -------------------------------------------------------------------------------- /remove_legacy_file.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | sudo rm -f "/usr/local/lib/systemd/user/xdg-desktop-portal-termfilechooser.service" 3 | sudo rm -f "/usr/local/lib64/systemd/user/xdg-desktop-portal-termfilechooser.service" 4 | sudo rm -f "/usr/local/libexec/xdg-desktop-portal-termfilechooser" 5 | sudo rm -f "/usr/local/share/dbus-1/services/org.freedesktop.impl.portal.desktop.termfilechooser.service" 6 | sudo rm -rf "/usr/local/share/xdg-desktop-portal-termfilechooser/" 7 | -------------------------------------------------------------------------------- /include/config.h: -------------------------------------------------------------------------------- 1 | #ifndef CONFIG_H 2 | #define CONFIG_H 3 | 4 | #include "logger.h" 5 | 6 | struct config_filechooser { 7 | char *cmd; 8 | char *default_dir; 9 | }; 10 | 11 | struct xdpw_config { 12 | struct config_filechooser filechooser_conf; 13 | }; 14 | 15 | void print_config(enum LOGLEVEL loglevel, struct xdpw_config *config); 16 | void finish_config(struct xdpw_config *config); 17 | void init_config(char **const configfile, struct xdpw_config *config); 18 | 19 | #endif 20 | -------------------------------------------------------------------------------- /include/logger.h: -------------------------------------------------------------------------------- 1 | #ifndef LOGGER_H 2 | #define LOGGER_H 3 | 4 | #include 5 | 6 | #define DEFAULT_LOGLEVEL ERROR 7 | 8 | enum LOGLEVEL { QUIET, ERROR, WARN, INFO, DEBUG, TRACE }; 9 | 10 | struct logger_properties { 11 | enum LOGLEVEL level; 12 | FILE *dst; 13 | }; 14 | 15 | enum LOGLEVEL get_logger_level(void); 16 | void init_logger(FILE *dst, enum LOGLEVEL level); 17 | enum LOGLEVEL get_loglevel(const char *level); 18 | void logprint(enum LOGLEVEL level, char *msg, ...); 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | 54 | # build folder 55 | build/ 56 | build-*/ 57 | .cache/ 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 emersion 4 | Copyright (c) 2021 Germain Z. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /include/xdpw.h: -------------------------------------------------------------------------------- 1 | #ifndef XDPW_H 2 | #define XDPW_H 3 | 4 | #ifdef HAVE_LIBSYSTEMD 5 | #include 6 | #elif HAVE_LIBELOGIND 7 | #include 8 | #elif HAVE_BASU 9 | #include 10 | #endif 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | #include "config.h" 17 | 18 | struct xdpw_state { 19 | sd_bus *bus; 20 | struct xdpw_config *config; 21 | }; 22 | 23 | struct xdpw_request { 24 | sd_bus_slot *slot; 25 | }; 26 | 27 | struct xdpw_session { 28 | sd_bus_slot *slot; 29 | char *session_handle; 30 | }; 31 | 32 | typedef void (*xdpw_event_loop_timer_func_t)(void *data); 33 | 34 | enum { 35 | PORTAL_RESPONSE_SUCCESS = 0, 36 | PORTAL_RESPONSE_CANCELLED = 1, 37 | PORTAL_RESPONSE_ENDED = 2 38 | }; 39 | 40 | int xdpw_filechooser_init(struct xdpw_state *state); 41 | 42 | struct xdpw_request *xdpw_request_create(sd_bus *bus, const char *object_path); 43 | void xdpw_request_destroy(struct xdpw_request *req); 44 | 45 | struct xdpw_session *xdpw_session_create(struct xdpw_state *state, sd_bus *bus, char *object_path); 46 | void xdpw_session_destroy(struct xdpw_session *req); 47 | 48 | struct xdpw_timer *xdpw_add_timer(struct xdpw_state *state, 49 | uint64_t delay_ns, xdpw_event_loop_timer_func_t func, void *data); 50 | 51 | void xdpw_destroy_timer(struct xdpw_timer *timer); 52 | 53 | #endif 54 | -------------------------------------------------------------------------------- /src/core/request.c: -------------------------------------------------------------------------------- 1 | #include "logger.h" 2 | #include "xdpw.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | static const char interface_name[] = "org.freedesktop.impl.portal.Request"; 9 | 10 | static int method_close(sd_bus_message *msg, void *data, 11 | sd_bus_error *ret_error) { 12 | struct xdpw_request *req = data; 13 | int ret = 0; 14 | logprint(INFO, "dbus: request closed"); 15 | 16 | sd_bus_message *reply = NULL; 17 | ret = sd_bus_message_new_method_return(msg, &reply); 18 | if (ret < 0) { 19 | return ret; 20 | } 21 | 22 | ret = sd_bus_send(NULL, reply, NULL); 23 | if (ret < 0) { 24 | return ret; 25 | } 26 | 27 | sd_bus_message_unref(reply); 28 | 29 | xdpw_request_destroy(req); 30 | 31 | return 0; 32 | } 33 | 34 | static const sd_bus_vtable request_vtable[] = { 35 | SD_BUS_VTABLE_START(0), 36 | SD_BUS_METHOD("Close", "", "", method_close, SD_BUS_VTABLE_UNPRIVILEGED), 37 | SD_BUS_VTABLE_END}; 38 | 39 | struct xdpw_request *xdpw_request_create(sd_bus *bus, const char *object_path) { 40 | struct xdpw_request *req = calloc(1, sizeof(struct xdpw_request)); 41 | 42 | if (sd_bus_add_object_vtable(bus, &req->slot, object_path, interface_name, 43 | request_vtable, NULL) < 0) { 44 | free(req); 45 | logprint(ERROR, "dbus: sd_bus_add_object_vtable failed: %s", 46 | strerror(-errno)); 47 | return NULL; 48 | } 49 | 50 | return req; 51 | } 52 | 53 | void xdpw_request_destroy(struct xdpw_request *req) { 54 | if (req == NULL) { 55 | return; 56 | } 57 | sd_bus_slot_unref(req->slot); 58 | free(req); 59 | } 60 | -------------------------------------------------------------------------------- /contrib/fzf-wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ "$6" == "1" ]]; then 4 | set -x 5 | fi 6 | 7 | # This wrapper script is invoked by xdg-desktop-portal-termfilechooser. 8 | # 9 | # Inputs: 10 | # 1. "1" if multiple files can be chosen, "0" otherwise. 11 | # 2. "1" if a directory should be chosen, "0" otherwise. 12 | # 3. "0" if opening files was requested, "1" if writing to a file was 13 | # requested. For example, when uploading files in Firefox, this will be "0". 14 | # When saving a web page in Firefox, this will be "1". 15 | # 4. If writing to a file, this is recommended path provided by the caller. For 16 | # example, when saving a web page in Firefox, this will be the recommended 17 | # path Firefox provided, such as "~/Downloads/webpage_title.html". 18 | # Note that if the path already exists, we keep appending "_" to it until we 19 | # get a path that does not exist. 20 | # 5. The output path, to which results should be written. 21 | # 6. "1" if log level >= DEBUG, "0" otherwise. 22 | # 23 | # Output: 24 | # The script should print the selected paths to the output path (argument #5), 25 | # one path per line. 26 | # If nothing is printed, then the operation is assumed to have been canceled. 27 | 28 | multiple="$1" 29 | directory="$2" 30 | save="$3" 31 | path="$4" 32 | out="$5" 33 | 34 | termcmd="${TERMCMD:-/usr/bin/kitty}" 35 | 36 | if [ "$save" = "1" ]; then 37 | cmd="dialog --yesno \"Save to \"$path\"?\" 0 0 && ( printf '%s' \"$path\" > $out ) || ( printf '%s' 'Input path to write to: ' && read input && printf '%s' \"\$input\" > $out)" 38 | elif [ "$directory" = "1" ]; then 39 | cmd="fd -a --base-directory=$HOME -td | fzf +m --prompt 'Select directory > ' > $out" 40 | elif [ "$multiple" = "1" ]; then 41 | cmd="fd -a --base-directory=$HOME | fzf -m --prompt 'Select files > ' > $out" 42 | else 43 | cmd="fd -a --base-directory=$HOME | fzf +m --prompt 'Select file > ' > $out" 44 | fi 45 | 46 | "$termcmd" sh -c "$cmd" 47 | -------------------------------------------------------------------------------- /xdg-desktop-portal-termfilechooser.5.scd: -------------------------------------------------------------------------------- 1 | xdg-desktop-portal-termfilechooser(5) 2 | 3 | # NAME 4 | 5 | xdg-desktop-portal-termfilechooser - an xdg-desktop-portal backend to choose 6 | files with your favorite terminal file manager. 7 | 8 | # DESCRIPTION 9 | 10 | xdg-desktop-portal-termfilechooser (or xdptf for short) allows applications to 11 | choose files via xdg-desktop-portal using your favorite terminal filechooser. 12 | 13 | xdptf will try to load the configuration file from these locations: 14 | 15 | - $XDG_CONFIG_HOME/xdg-desktop-portal-termfilechooser/$XDG_CURRENT_DESKTOP 16 | - $XDG_CONFIG_HOME/xdg-desktop-portal-termfilechooser/config 17 | - /etc/xdg/xdg-desktop-portal-termfilechooser/$XDG_CURRENT_DESKTOP 18 | - /etc/xdg/xdg-desktop-portal-termfilechooser/config 19 | 20 | _$XDG_CONFIG_HOME_ defaults to _~/.config_. 21 | _$XDG_CURRENT_DESKTOP_ can be a colon seperated list. Each element of that list will be tried. 22 | 23 | The configuration files use the INI file format. Example: 24 | 25 | ``` 26 | [filechooser] 27 | cmd=/usr/share/xdg-desktop-portal-termfilechooser/ranger-wrapper.sh 28 | ``` 29 | 30 | # FILECHOOSER OPTIONS 31 | 32 | These options need to be placed under the **[filechooser]** section. 33 | 34 | **cmd** = _command_ 35 | Command to execute. For invocation details, please refer to the default 36 | wrapper script. 37 | 38 | The default wrapper script is provided at 39 | /usr/share/xdg-desktop-portal-termfilechooser/ranger-wrapper.sh, and is 40 | configured to launch the ranger file manager to select files. 41 | By default, the wrapper script will use $TERMCMD if available. Otherwise, 42 | it will fallback to "/usr/bin/kitty". 43 | You can copy and edit the script to change this. 44 | 45 | An example wrapper script which uses fd and fzf can also be found at 46 | /usr/share/xdg-desktop-portal-termfilechooser/fzf-wrapper.sh. 47 | 48 | **default_dir** = _directory_ 49 | Default directory to save files in, if the invoking program does not 50 | provide one. 51 | 52 | The default value is /tmp. 53 | 54 | # SEE ALSO 55 | 56 | **ranger**(1) 57 | -------------------------------------------------------------------------------- /src/core/logger.c: -------------------------------------------------------------------------------- 1 | #include "logger.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | static struct logger_properties logprops; 9 | 10 | void init_logger(FILE *dst, enum LOGLEVEL level) { 11 | logprops.dst = dst; 12 | logprops.level = level; 13 | } 14 | 15 | enum LOGLEVEL get_logger_level(void) { return logprops.level; } 16 | 17 | enum LOGLEVEL get_loglevel(const char *level) { 18 | if (strcmp(level, "QUIET") == 0) { 19 | return QUIET; 20 | } else if (strcmp(level, "ERROR") == 0) { 21 | return ERROR; 22 | } else if (strcmp(level, "WARN") == 0) { 23 | return WARN; 24 | } else if (strcmp(level, "INFO") == 0) { 25 | return INFO; 26 | } else if (strcmp(level, "DEBUG") == 0) { 27 | return DEBUG; 28 | } else if (strcmp(level, "TRACE") == 0) { 29 | return TRACE; 30 | } 31 | 32 | fprintf(stderr, "Could not understand log level %s\n", level); 33 | exit(1); 34 | } 35 | 36 | static const char *print_loglevel(enum LOGLEVEL loglevel) { 37 | switch (loglevel) { 38 | case QUIET: 39 | return "QUIET"; 40 | case ERROR: 41 | return "ERROR"; 42 | case WARN: 43 | return "WARN"; 44 | case INFO: 45 | return "INFO"; 46 | case DEBUG: 47 | return "DEBUG"; 48 | case TRACE: 49 | return "TRACE"; 50 | } 51 | fprintf(stderr, "Could not find log level %d\n", loglevel); 52 | abort(); 53 | } 54 | 55 | void logprint(enum LOGLEVEL level, char *msg, ...) { 56 | if (!logprops.dst) { 57 | fprintf(stderr, "Logger has been called, but was not initialized\n"); 58 | abort(); 59 | } 60 | 61 | if (level > logprops.level || level == QUIET) { 62 | return; 63 | } 64 | va_list args; 65 | 66 | char timestr[200]; 67 | time_t t = time(NULL); 68 | struct tm *tmp = localtime(&t); 69 | 70 | if (strftime(timestr, sizeof(timestr), "%Y/%m/%d %H:%M:%S", tmp) == 0) { 71 | fprintf(stderr, "strftime returned 0"); 72 | abort(); 73 | } 74 | 75 | fprintf(logprops.dst, "%s", timestr); 76 | fprintf(logprops.dst, " "); 77 | fprintf(logprops.dst, "[%s]", print_loglevel(level)); 78 | fprintf(logprops.dst, " - "); 79 | 80 | va_start(args, msg); 81 | vfprintf(logprops.dst, msg, args); 82 | va_end(args); 83 | 84 | fprintf(logprops.dst, "\n"); 85 | fflush(logprops.dst); 86 | } 87 | -------------------------------------------------------------------------------- /contrib/lf-wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ "$6" == "1" ]]; then 4 | set -x 5 | fi 6 | # This wrapper script is invoked by xdg-desktop-portal-termfilechooser. 7 | # 8 | # Inputs: 9 | # 1. "1" if multiple files can be chosen, "0" otherwise. 10 | # 2. "1" if a directory should be chosen, "0" otherwise. 11 | # 3. "0" if opening files was requested, "1" if writing to a file was 12 | # requested. For example, when uploading files in Firefox, this will be "0". 13 | # When saving a web page in Firefox, this will be "1". 14 | # 4. If writing to a file, this is recommended path provided by the caller. For 15 | # example, when saving a web page in Firefox, this will be the recommended 16 | # path Firefox provided, such as "~/Downloads/webpage_title.html". 17 | # Note that if the path already exists, we keep appending "_" to it until we 18 | # get a path that does not exist. 19 | # 5. The output path, to which results should be written. 20 | # 6. "1" if log level >= DEBUG, "0" otherwise. 21 | # 22 | # Output: 23 | # The script should print the selected paths to the output path (argument #5), 24 | # one path per line. 25 | # If nothing is printed, then the operation is assumed to have been canceled. 26 | 27 | multiple="$1" 28 | directory="$2" 29 | save="$3" 30 | path="$4" 31 | out="$5" 32 | cmd="/usr/bin/lf" 33 | if [ "$save" = "1" ]; then 34 | TITLE="Save File:" 35 | elif [ "$directory" = "1" ]; then 36 | TITLE="Select Directory:" 37 | else 38 | TITLE="Select File:" 39 | fi 40 | 41 | quote_string() { 42 | local input="$1" 43 | echo "'${input//\'/\'\\\'\'}'" 44 | } 45 | 46 | termcmd="${TERMCMD:-/usr/bin/kitty --title $(quote_string "$TITLE")}" 47 | 48 | cleanup() { 49 | if [ -f "$tmpfile" ]; then 50 | /usr/bin/rm "$tmpfile" || : 51 | fi 52 | if [ "$save" = "1" ] && [ ! -s "$out" ]; then 53 | /usr/bin/rm "$path" || : || n 54 | fi 55 | } 56 | 57 | trap cleanup EXIT HUP INT QUIT ABRT TERM 58 | 59 | if [ "$save" = "1" ]; then 60 | tmpfile=$(/usr/bin/mktemp) 61 | /usr/bin/printf '%s' 'xdg-desktop-portal-termfilechooser saving files tutorial 62 | 63 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 64 | !!! === WARNING! === !!! 65 | !!! The contents of *whatever* file you open last in !!! 66 | !!! ranger will be *overwritten*! !!! 67 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 68 | 69 | Instructions: 70 | 1) Move this file wherever you want. 71 | 2) Rename the file if needed. 72 | 3) Confirm your selection by opening the file, for 73 | example by pressing . 74 | Notes: 75 | 1) This file is provided for your convenience. You can 76 | only choose this placeholder file otherwise the save operation aborted. 77 | 2) If you quit ranger without opening a file, this file 78 | will be removed and the save operation aborted. 79 | ' >"$path" 80 | set -- -selection-path "$(quote_string "$tmpfile")" "$(quote_string "$path")" 81 | elif [ "$directory" = "1" ]; then 82 | set -- -last-dir-path "$(quote_string "$out")" "$(quote_string "$path")" 83 | elif [ "$multiple" = "1" ]; then 84 | set -- -selection-path "$(quote_string "$out")" "$(quote_string "$path")" 85 | else 86 | set -- -selection-path "$(quote_string "$out")" "$(quote_string "$path")" 87 | fi 88 | 89 | eval "$termcmd -- $cmd $@" 90 | 91 | # case save file 92 | if [ "$save" = "1" ] && [ -s "$tmpfile" ]; then 93 | selected_file=$(/usr/bin/head -n 1 "$tmpfile") 94 | # Check if selected file is placeholder file 95 | if [ -f "$selected_file" ] && /usr/bin/grep -qi "^xdg-desktop-portal-termfilechooser saving files tutorial" "$selected_file"; then 96 | /usr/bin/echo "$selected_file" >"$out" 97 | path="$selected_file" 98 | fi 99 | fi 100 | -------------------------------------------------------------------------------- /contrib/yazi-wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ "$6" == "1" ]]; then 4 | set -x 5 | fi 6 | 7 | # This wrapper script is invoked by xdg-desktop-portal-termfilechooser. 8 | # 9 | # Inputs: 10 | # 1. "1" if multiple files can be chosen, "0" otherwise. 11 | # 2. "1" if a directory should be chosen, "0" otherwise. 12 | # 3. "0" if opening files was requested, "1" if writing to a file was 13 | # requested. For example, when uploading files in Firefox, this will be "0". 14 | # When saving a web page in Firefox, this will be "1". 15 | # 4. If writing to a file, this is recommended path provided by the caller. For 16 | # example, when saving a web page in Firefox, this will be the recommended 17 | # path Firefox provided, such as "~/Downloads/webpage_title.html". 18 | # Note that if the path already exists, we keep appending "_" to it until we 19 | # get a path that does not exist. 20 | # 5. The output path, to which results should be written. 21 | # 6. "1" if log level >= DEBUG, "0" otherwise. 22 | # 23 | # Output: 24 | # The script should print the selected paths to the output path (argument #5), 25 | # one path per line. 26 | # If nothing is printed, then the operation is assumed to have been canceled. 27 | 28 | multiple="$1" 29 | directory="$2" 30 | save="$3" 31 | path="$4" 32 | out="$5" 33 | cmd="/usr/bin/yazi" 34 | # "wezterm start --always-new-process" if you use wezterm 35 | if [ "$save" = "1" ]; then 36 | TITLE="Save File:" 37 | elif [ "$directory" = "1" ]; then 38 | TITLE="Select Directory:" 39 | else 40 | TITLE="Select File:" 41 | fi 42 | 43 | quote_string() { 44 | local input="$1" 45 | echo "'${input//\'/\'\\\'\'}'" 46 | } 47 | 48 | termcmd="${TERMCMD:-/usr/bin/kitty --title $(quote_string "$TITLE")}" 49 | 50 | cleanup() { 51 | if [ -f "$tmpfile" ]; then 52 | /usr/bin/rm "$tmpfile" || : 53 | fi 54 | if [ "$save" = "1" ] && [ ! -s "$out" ]; then 55 | /usr/bin/rm "$path" || : 56 | fi 57 | } 58 | 59 | trap cleanup EXIT HUP INT QUIT ABRT TERM 60 | 61 | if [ "$save" = "1" ]; then 62 | tmpfile=$(/usr/bin/mktemp) 63 | 64 | # Save/download file 65 | /usr/bin/printf '%s' 'xdg-desktop-portal-termfilechooser saving files tutorial 66 | 67 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 68 | !!! === WARNING! === !!! 69 | !!! The contents of *whatever* file you open last in !!! 70 | !!! yazi will be *overwritten*! !!! 71 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 72 | 73 | Instructions: 74 | 1) Move this file wherever you want. 75 | 2) Rename the file if needed. 76 | 3) Confirm your selection by opening the file, for 77 | example by pressing . 78 | 79 | Notes: 80 | 1) This file is provided for your convenience. You can 81 | only choose this placeholder file otherwise the save operation aborted. 82 | 2) If you quit yazi without opening a file, this file 83 | will be removed and the save operation aborted. 84 | ' >"$path" 85 | set -- --chooser-file="$(quote_string "$tmpfile")" "$(quote_string "$path")" 86 | elif [ "$directory" = "1" ]; then 87 | # upload files from a directory 88 | # Use this if you want to select folder by 'quit' function in yazi. 89 | set -- --cwd-file="$(quote_string "$out")" "$(quote_string "$path")" 90 | # NOTE: Use this if you want to select folder by enter a.k.a yazi keybind for 'open' funtion ('run = "open") . 91 | # set -- --chooser-file="$out" "$path" 92 | elif [ "$multiple" = "1" ]; then 93 | # upload multiple files 94 | set -- --chooser-file="$(quote_string "$out")" "$(quote_string "$path")" 95 | else 96 | # upload only 1 file 97 | set -- --chooser-file="$(quote_string "$out")" "$(quote_string "$path")" 98 | fi 99 | 100 | eval "$termcmd -- $cmd $@" 101 | 102 | # case save file 103 | if [ "$save" = "1" ] && [ -s "$tmpfile" ]; then 104 | selected_file=$(/usr/bin/head -n 1 "$tmpfile") 105 | # Check if selected file is placeholder file 106 | if [ -f "$selected_file" ] && /usr/bin/grep -qi "^xdg-desktop-portal-termfilechooser saving files tutorial" "$selected_file"; then 107 | /usr/bin/echo "$selected_file" >"$out" 108 | path="$selected_file" 109 | fi 110 | fi 111 | -------------------------------------------------------------------------------- /contrib/vifm-wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ "$6" == "1" ]]; then 4 | set -x 5 | fi 6 | 7 | # This wrapper script is invoked by xdg-desktop-portal-termfilechooser. 8 | # 9 | # Inputs: 10 | # 1. "1" if multiple files can be chosen, "0" otherwise. 11 | # 2. "1" if a directory should be chosen, "0" otherwise. 12 | # 3. "0" if opening files was requested, "1" if writing to a file was 13 | # requested. For example, when uploading files in Firefox, this will be "0". 14 | # When saving a web page in Firefox, this will be "1". 15 | # 4. If writing to a file, this is recommended path provided by the caller. For 16 | # example, when saving a web page in Firefox, this will be the recommended 17 | # path Firefox provided, such as "~/Downloads/webpage_title.html". 18 | # Note that if the path already exists, we keep appending "_" to it until we 19 | # get a path that does not exist. 20 | # 5. The output path, to which results should be written. 21 | # 6. "1" if log level >= DEBUG, "0" otherwise. 22 | # 23 | # Output: 24 | # The script should print the selected paths to the output path (argument #5), 25 | # one path per line. 26 | # If nothing is printed, then the operation is assumed to have been canceled. 27 | 28 | multiple="$1" 29 | directory="$2" 30 | save="$3" 31 | path="$4" 32 | out="$5" 33 | cmd="/usr/bin/vifm" 34 | # "wezterm start --always-new-process" if you use wezterm 35 | if [ "$save" = "1" ]; then 36 | TITLE="Save File:" 37 | elif [ "$directory" = "1" ]; then 38 | TITLE="Select Directory:" 39 | else 40 | TITLE="Select File:" 41 | fi 42 | 43 | quote_string() { 44 | local input="$1" 45 | echo "'${input//\'/\'\\\'\'}'" 46 | } 47 | 48 | termcmd="${TERMCMD:-/usr/bin/kitty --title $(quote_string "$TITLE")}" 49 | 50 | cleanup() { 51 | if [ -f "$tmpfile" ]; then 52 | /usr/bin/rm "$tmpfile" || : 53 | fi 54 | if [ "$save" = "1" ] && [ ! -s "$out" ]; then 55 | /usr/bin/rm "$path" || : 56 | fi 57 | } 58 | 59 | trap cleanup EXIT HUP INT QUIT ABRT TERM 60 | 61 | if [ "$save" = "1" ]; then 62 | tmpfile=$(/usr/bin/mktemp) 63 | # Save/download file 64 | /usr/bin/printf '%s' 'xdg-desktop-portal-termfilechooser saving files tutorial 65 | 66 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 67 | !!! === WARNING! === !!! 68 | !!! The contents of *whatever* file you open last in !!! 69 | !!! ranger will be *overwritten*! !!! 70 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 71 | 72 | Instructions: 73 | 1) Move this file wherever you want. 74 | 2) Rename the file if needed. 75 | 3) Confirm your selection by opening the file, for 76 | example by pressing . 77 | 78 | Notes: 79 | 1) This file is provided for your convenience. You can 80 | only choose this placeholder file otherwise the save operation aborted. 81 | 2) If you quit ranger without opening a file, this file 82 | will be removed and the save operation aborted. 83 | ' >"$path" 84 | set -- --choose-files "$(quote_string "$tmpfile")" -c "$(quote_string "set statusline='Select save path (see tutorial in preview pane; try pressing key if no preview)'")" --select "$(quote_string "$path")" 85 | elif [ "$directory" = "1" ]; then 86 | # upload files from a directory 87 | set -- --choose-dir "$(quote_string "$out")" -c "$(quote_string "set statusline='Select directory (quit in dir to select it)'")" "$(quote_string "$path")" 88 | elif [ "$multiple" = "1" ]; then 89 | # upload multiple files 90 | set -- --choose-files "$(quote_string "$out")" -c "$(quote_string "set statusline='Select file(s) (press key to select multiple)'")" "$(quote_string "$path")" 91 | else 92 | # upload only 1 file 93 | set -- --choose-files "$(quote_string "$out")" -c "$(quote_string "set statusline='Select file (open file to select it)'")" "$(quote_string "$path")" 94 | fi 95 | eval "$termcmd -- $cmd $@" 96 | 97 | # case save file 98 | if [ "$save" = "1" ] && [ -s "$tmpfile" ]; then 99 | selected_file=$(/usr/bin/head -n 1 "$tmpfile") 100 | # Check if selected file is placeholder file 101 | if [ -f "$selected_file" ] && /usr/bin/grep -qi "^xdg-desktop-portal-termfilechooser saving files tutorial" "$selected_file"; then 102 | /usr/bin/echo "$selected_file" >"$out" 103 | path="$selected_file" 104 | fi 105 | fi 106 | -------------------------------------------------------------------------------- /contrib/ranger-wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ "$6" == "1" ]]; then 4 | set -x 5 | fi 6 | 7 | # This wrapper script is invoked by xdg-desktop-portal-termfilechooser. 8 | # 9 | # Inputs: 10 | # 1. "1" if multiple files can be chosen, "0" otherwise. 11 | # 2. "1" if a directory should be chosen, "0" otherwise. 12 | # 3. "0" if opening files was requested, "1" if writing to a file was 13 | # requested. For example, when uploading files in Firefox, this will be "0". 14 | # When saving a web page in Firefox, this will be "1". 15 | # 4. If writing to a file, this is recommended path provided by the caller. For 16 | # example, when saving a web page in Firefox, this will be the recommended 17 | # path Firefox provided, such as "~/Downloads/webpage_title.html". 18 | # Note that if the path already exists, we keep appending "_" to it until we 19 | # get a path that does not exist. 20 | # 5. The output path, to which results should be written. 21 | # 6. "1" if log level >= DEBUG, "0" otherwise. 22 | # 23 | # Output: 24 | # The script should print the selected paths to the output path (argument #5), 25 | # one path per line. 26 | # If nothing is printed, then the operation is assumed to have been canceled. 27 | 28 | multiple="$1" 29 | directory="$2" 30 | save="$3" 31 | path="$4" 32 | out="$5" 33 | cmd="/usr/bin/ranger" 34 | # "wezterm start --always-new-process" if you use wezterm 35 | if [ "$save" = "1" ]; then 36 | TITLE="Save File:" 37 | elif [ "$directory" = "1" ]; then 38 | TITLE="Select Directory:" 39 | else 40 | TITLE="Select File:" 41 | fi 42 | 43 | quote_string() { 44 | local input="$1" 45 | echo "'${input//\'/\'\\\'\'}'" 46 | } 47 | 48 | termcmd="${TERMCMD:-/usr/bin/kitty --title $(quote_string "$TITLE")}" 49 | 50 | cleanup() { 51 | if [ -f "$tmpfile" ]; then 52 | /usr/bin/rm "$tmpfile" || : 53 | fi 54 | if [ "$save" = "1" ] && [ ! -s "$out" ]; then 55 | /usr/bin/rm "$path" || : 56 | fi 57 | } 58 | 59 | trap cleanup EXIT HUP INT QUIT ABRT TERM 60 | 61 | if [ "$save" = "1" ]; then 62 | tmpfile=$(/usr/bin/mktemp) 63 | # Save/download file 64 | /usr/bin/printf '%s' 'xdg-desktop-portal-termfilechooser saving files tutorial 65 | 66 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 67 | !!! === WARNING! === !!! 68 | !!! The contents of *whatever* file you open last in !!! 69 | !!! ranger will be *overwritten*! !!! 70 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 71 | 72 | Instructions: 73 | 1) Move this file wherever you want. 74 | 2) Rename the file if needed. 75 | 3) Confirm your selection by opening the file, for 76 | example by pressing . 77 | 78 | Notes: 79 | 1) This file is provided for your convenience. You can 80 | only choose this placeholder file otherwise the save operation aborted. 81 | 2) If you quit ranger without opening a file, this file 82 | will be removed and the save operation aborted. 83 | ' >"$path" 84 | set -- --choosefile="$(quote_string "$tmpfile")" --cmd="$(quote_string "echo Select save path (see tutorial in preview pane; try pressing zv or zp if no preview)")" --selectfile="$(quote_string "$path")" 85 | elif [ "$directory" = "1" ]; then 86 | # upload files from a directory 87 | set -- --choosedir="$(quote_string "$out")" --show-only-dirs --cmd="$(quote_string "echo Select directory (quit in dir to select it)")" "$(quote_string "$path")" 88 | elif [ "$multiple" = "1" ]; then 89 | # upload multiple files 90 | set -- --choosefiles="$(quote_string "$out")" --cmd="$(quote_string "echo Select file(s) (open file to select it; to select multiple)")" "$(quote_string "$path")" 91 | else 92 | # upload only 1 file 93 | set -- --choosefile="$(quote_string "$out")" --cmd="$(quote_string "echo Select file (open file to select it)")" "$(quote_string "$path")" 94 | fi 95 | eval "$termcmd -- $cmd $@" 96 | 97 | # case save file 98 | if [ "$save" = "1" ] && [ -s "$tmpfile" ]; then 99 | selected_file=$(/usr/bin/head -n 1 "$tmpfile") 100 | # Check if selected file is placeholder file 101 | if [ -f "$selected_file" ] && /usr/bin/grep -qi "^xdg-desktop-portal-termfilechooser saving files tutorial" "$selected_file"; then 102 | /usr/bin/echo "$selected_file" >"$out" 103 | path="$selected_file" 104 | fi 105 | fi 106 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'xdg-desktop-portal-termfilechooser', 3 | 'c', 4 | version: '1.1.0', 5 | license: 'MIT', 6 | meson_version: '>=0.50.0', 7 | default_options: ['c_std=c11', 'warning_level=2', 'werror=true'], 8 | ) 9 | 10 | cc = meson.get_compiler('c') 11 | 12 | add_project_arguments(cc.get_supported_arguments([ 13 | '-Wno-missing-braces', 14 | '-Wno-missing-field-initializers', 15 | '-Wno-unused-parameter', 16 | '-D_POSIX_C_SOURCE=200809L', 17 | ]), language: 'c') 18 | 19 | prefix = get_option('prefix') 20 | libexecdir = get_option('libexecdir') 21 | sysconfdir = get_option('sysconfdir') 22 | datadir = get_option('datadir') 23 | add_project_arguments('-DSYSCONFDIR="@0@"'.format(join_paths(prefix, sysconfdir)), language : 'c') 24 | 25 | inc = include_directories('include') 26 | 27 | rt = cc.find_library('rt') 28 | iniparser = dependency('inih') 29 | 30 | if get_option('sd-bus-provider') == 'auto' 31 | assert(get_option('auto_features').auto(), 'sd-bus-provider must not be set to auto since auto_features != auto') 32 | sdbus = dependency('libsystemd', 33 | required: false, 34 | not_found_message: 'libsystemd not found, trying libelogind', 35 | ) 36 | if not sdbus.found() 37 | sdbus = dependency('libelogind', 38 | required: false, 39 | not_found_message: 'libelogind not found, trying basu', 40 | ) 41 | endif 42 | if not sdbus.found() 43 | sdbus = dependency('basu', 44 | required: false, 45 | ) 46 | endif 47 | if not sdbus.found() 48 | error('Neither libsystemd, nor libelogind, nor basu was found') 49 | endif 50 | else 51 | sdbus = dependency(get_option('sd-bus-provider')) 52 | endif 53 | add_project_arguments('-DHAVE_' + sdbus.name().to_upper() + '=1', language: 'c') 54 | 55 | xdpw_files = files([ 56 | 'src/core/main.c', 57 | 'src/core/logger.c', 58 | 'src/core/config.c', 59 | 'src/core/request.c', 60 | 'src/filechooser/filechooser.c', 61 | ]) 62 | 63 | executable( 64 | 'xdg-desktop-portal-termfilechooser', 65 | [xdpw_files], 66 | dependencies: [ 67 | sdbus, 68 | rt, 69 | iniparser, 70 | ], 71 | include_directories: [inc], 72 | install: true, 73 | install_dir: libexecdir, 74 | ) 75 | 76 | conf_data = configuration_data() 77 | conf_data.set('libexecdir', 78 | join_paths(prefix, libexecdir)) 79 | conf_data.set('systemd_service', '') 80 | 81 | systemd = dependency('systemd', required: get_option('systemd')) 82 | 83 | if systemd.found() 84 | systemd_service_file = 'xdg-desktop-portal-termfilechooser.service' 85 | user_unit_dir = systemd.get_pkgconfig_variable('systemduserunitdir', 86 | define_variable: ['prefix', prefix]) 87 | conf_data.set('systemd_service', 'SystemdService=' + systemd_service_file) 88 | 89 | configure_file( 90 | configuration: conf_data, 91 | input: 'contrib/systemd/' + systemd_service_file + '.in', 92 | output: '@BASENAME@', 93 | install_dir: user_unit_dir, 94 | ) 95 | endif 96 | 97 | configure_file( 98 | configuration: conf_data, 99 | input: 'org.freedesktop.impl.portal.desktop.termfilechooser.service.in', 100 | output: '@BASENAME@', 101 | install_dir: join_paths(datadir, 'dbus-1', 'services'), 102 | ) 103 | 104 | install_data( 105 | 'termfilechooser.portal', 106 | install_dir: join_paths(datadir, 'xdg-desktop-portal', 'portals'), 107 | ) 108 | 109 | install_subdir( 110 | 'contrib', 111 | install_dir: join_paths(datadir, 'xdg-desktop-portal-termfilechooser'), 112 | strip_directory: true, 113 | ) 114 | 115 | scdoc = dependency('scdoc', required: get_option('man-pages'), version: '>= 1.9.7') 116 | if scdoc.found() 117 | man_pages = ['xdg-desktop-portal-termfilechooser.5.scd'] 118 | foreach src : man_pages 119 | topic = src.split('.')[0] 120 | section = src.split('.')[1] 121 | output = topic + '.' + section 122 | 123 | custom_target( 124 | output, 125 | input: files(src), 126 | output: output, 127 | command: [ 128 | 'sh', '-c', '@0@ < @INPUT@ > @1@'.format(scdoc.get_pkgconfig_variable('scdoc'), output) 129 | ], 130 | install: true, 131 | install_dir: join_paths(get_option('mandir'), 'man' + section), 132 | ) 133 | endforeach 134 | endif 135 | -------------------------------------------------------------------------------- /contrib/nnn-wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ "$6" == "1" ]]; then 4 | set -x 5 | fi 6 | # This wrapper script is invoked by xdg-desktop-portal-termfilechooser. 7 | # 8 | # Inputs: 9 | # 1. "1" if multiple files can be chosen, "0" otherwise. 10 | # 2. "1" if a directory should be chosen, "0" otherwise. 11 | # 3. "0" if opening files was requested, "1" if writing to a file was 12 | # requested. For example, when uploading files in Firefox, this will be "0". 13 | # When saving a web page in Firefox, this will be "1". 14 | # 4. If writing to a file, this is recommended path provided by the caller. For 15 | # example, when saving a web page in Firefox, this will be the recommended 16 | # path Firefox provided, such as "~/Downloads/webpage_title.html". 17 | # Note that if the path already exists, we keep appending "_" to it until we 18 | # get a path that does not exist. 19 | # 5. The output path, to which results should be written. 20 | # 6. "1" if log level >= DEBUG, "0" otherwise. 21 | # 22 | 23 | cleanup() { 24 | if [ -f "$tmpfile" ]; then 25 | /usr/bin/rm "$tmpfile" || : 26 | fi 27 | if [ "$save" = "1" ] && [ ! -s "$out" ]; then 28 | /usr/bin/rm "$path" || : 29 | fi 30 | } 31 | trap cleanup EXIT HUP INT QUIT ABRT TERM 32 | 33 | multiple="$1" 34 | directory="$2" 35 | save="$3" 36 | path="$4" 37 | out="$5" 38 | cmd="/usr/bin/nnn" 39 | if [ "$save" = "1" ]; then 40 | TITLE="Save File:" 41 | elif [ "$directory" = "1" ]; then 42 | TITLE="Select Directory:" 43 | else 44 | TITLE="Select File:" 45 | fi 46 | 47 | quote_string() { 48 | local input="$1" 49 | echo "'${input//\'/\'\\\'\'}'" 50 | } 51 | 52 | termcmd="${TERMCMD:-/usr/bin/kitty --title $(quote_string "$TITLE")}" 53 | # See also: https://github.com/jarun/nnn/wiki/Basic-use-cases#file-picker 54 | 55 | # nnn has no equivalent of ranger's: 56 | # `--cmd` 57 | # .. and no other way to show a message text on startup. So, no way to show instructions in nnn itself, like it is done in ranger-wrapper. 58 | # nnn also does not show previews (needs a plugin and a keypress). So, the save instructions in `$path` file are not shown automatically. 59 | # `--show-only-dirs` 60 | # `--choosedir` 61 | # - To select then upload all files in a folder: Go inside of the folder then quit. 62 | 63 | if [ "$save" = "1" ]; then 64 | tmpfile=$(/usr/bin/mktemp) 65 | 66 | # Save/download file 67 | /usr/bin/printf '%s' 'xdg-desktop-portal-termfilechooser saving files tutorial 68 | 69 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 70 | !!! === WARNING! === !!! 71 | !!! The contents of *whatever* file you open last in !!! 72 | !!! yazi will be *overwritten*! !!! 73 | !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 74 | 75 | Instructions: 76 | 1) Move this file wherever you want. 77 | 2) Rename the file if needed. 78 | 3) Confirm your selection by opening the file, for 79 | example by pressing . 80 | 81 | Notes: 82 | 1) This file is provided for your convenience. You can 83 | only choose this placeholder file otherwise the save operation aborted. 84 | 2) If you quit yazi without opening a file, this file 85 | will be removed and the save operation aborted. 86 | ' >"$path" 87 | # -a create new FIFO, -P to show preview if they exist 88 | # Ref: https://github.com/jarun/nnn/wiki/Live-previews 89 | set -- -p "$(quote_string "$tmpfile")" "$(quote_string "$path")" 90 | elif [ "$directory" = "1" ]; then 91 | # data will has the format: `cd '/absolute/path/to/folder'` 92 | tmpfile=$(/usr/bin/mktemp) 93 | set -- "$(quote_string "$path")" 94 | else 95 | set -- -p "$(quote_string "$out")" "$(quote_string "$path")" 96 | fi 97 | 98 | if [ "$directory" = "1" ]; then 99 | NNN_TMPFILE="$tmpfile" eval "$termcmd -- $cmd $@" 100 | else 101 | eval "$termcmd -- $cmd $@" 102 | fi 103 | 104 | if [ "$directory" = "1" ] && [ -s "$tmpfile" ]; then 105 | # convert from `cd '/absolute/path/to/folder'` to `/absolute/path/to/folder` 106 | selected_dir=$(/usr/bin/head -n 1 "$tmpfile") 107 | selected_dir="${selected_dir#cd \'}" 108 | selected_dir="${selected_dir%\'}" 109 | /usr/bin/echo "$selected_dir" >"$out" 110 | fi 111 | 112 | # case save file 113 | if [ "$save" = "1" ] && [ -s "$tmpfile" ]; then 114 | selected_file=$(/usr/bin/head -n 1 "$tmpfile") 115 | # Check if selected file is placeholder file 116 | if [ -f "$selected_file" ] && /usr/bin/grep -qi "^xdg-desktop-portal-termfilechooser saving files tutorial" "$selected_file"; then 117 | /usr/bin/echo "$selected_file" >"$out" 118 | path="$selected_file" 119 | fi 120 | fi 121 | -------------------------------------------------------------------------------- /src/core/main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "logger.h" 9 | #include "xdpw.h" 10 | 11 | enum event_loop_fd { 12 | EVENT_LOOP_DBUS, 13 | }; 14 | 15 | static volatile bool keep_running = true; 16 | 17 | void handle_sigterm(int sig) { keep_running = false; } 18 | 19 | static const char service_name[] = 20 | "org.freedesktop.impl.portal.desktop.termfilechooser"; 21 | 22 | static int xdpw_usage(FILE *stream, int rc) { 23 | static const char *usage = 24 | "Usage: xdg-desktop-portal-termfilechooser [options]\n" 25 | "\n" 26 | " -l, --loglevel= Select log level (default is " 27 | "ERROR).\n" 28 | " QUIET, ERROR, WARN, INFO, DEBUG, " 29 | "TRACE\n" 30 | " -c, --config= Select config file.\n" 31 | " (default is " 32 | "$XDG_CONFIG_HOME/xdg-desktop-portal-termfilechooser/config)\n" 33 | " -r, --replace Replace a running instance.\n" 34 | " -h, --help Get help (this text).\n" 35 | "\n"; 36 | 37 | fprintf(stream, "%s", usage); 38 | return rc; 39 | } 40 | 41 | static int handle_name_lost(sd_bus_message *m, void *userdata, 42 | sd_bus_error *ret_error) { 43 | logprint(INFO, "dbus: lost name, closing connection"); 44 | sd_bus_close(sd_bus_message_get_bus(m)); 45 | return 1; 46 | } 47 | 48 | int main(int argc, char *argv[]) { 49 | signal(SIGTERM, handle_sigterm); 50 | signal(SIGINT, handle_sigterm); 51 | 52 | struct xdpw_config config = {}; 53 | char *configfile = NULL; 54 | enum LOGLEVEL loglevel = DEFAULT_LOGLEVEL; 55 | bool replace = false; 56 | 57 | static const char *shortopts = "l:o:c:f:rh"; 58 | static const struct option longopts[] = { 59 | {"loglevel", required_argument, NULL, 'l'}, 60 | {"config", required_argument, NULL, 'c'}, 61 | {"replace", no_argument, NULL, 'r'}, 62 | {"help", no_argument, NULL, 'h'}, 63 | {NULL, 0, NULL, 0}}; 64 | 65 | while (1) { 66 | int c = getopt_long(argc, argv, shortopts, longopts, NULL); 67 | if (c < 0) { 68 | break; 69 | } 70 | 71 | switch (c) { 72 | case 'l': 73 | loglevel = get_loglevel(optarg); 74 | break; 75 | case 'c': 76 | configfile = strdup(optarg); 77 | break; 78 | case 'r': 79 | replace = true; 80 | break; 81 | case 'h': 82 | return xdpw_usage(stdout, EXIT_SUCCESS); 83 | default: 84 | return xdpw_usage(stderr, EXIT_FAILURE); 85 | } 86 | } 87 | 88 | init_logger(stderr, loglevel); 89 | init_config(&configfile, &config); 90 | print_config(DEBUG, &config); 91 | 92 | int ret = 0; 93 | 94 | sd_bus *bus = NULL; 95 | sd_bus_slot *slot = NULL; 96 | ret = sd_bus_open_user(&bus); 97 | if (ret < 0) { 98 | logprint(ERROR, "dbus: failed to connect to user bus: %s", strerror(-ret)); 99 | return EXIT_FAILURE; 100 | } 101 | logprint(DEBUG, "dbus: connected"); 102 | 103 | struct xdpw_state state = { 104 | .bus = bus, 105 | .config = &config, 106 | }; 107 | 108 | xdpw_filechooser_init(&state); 109 | 110 | uint64_t flags = SD_BUS_NAME_ALLOW_REPLACEMENT; 111 | if (replace) { 112 | flags |= SD_BUS_NAME_REPLACE_EXISTING; 113 | } 114 | 115 | ret = sd_bus_request_name(bus, service_name, flags); 116 | if (ret < 0) { 117 | logprint(ERROR, "dbus: failed to acquire service name: %s", strerror(-ret)); 118 | goto error; 119 | } 120 | 121 | const char *unique_name; 122 | ret = sd_bus_get_unique_name(bus, &unique_name); 123 | if (ret < 0) { 124 | logprint(ERROR, "dbus: failed to get unique bus name: %s", strerror(-ret)); 125 | goto error; 126 | } 127 | 128 | static char match[1024]; 129 | snprintf(match, sizeof(match), 130 | "sender='org.freedesktop.DBus'," 131 | "type='signal'," 132 | "interface='org.freedesktop.DBus'," 133 | "member='NameOwnerChanged'," 134 | "path='/org/freedesktop/DBus'," 135 | "arg0='%s'," 136 | "arg1='%s'", 137 | service_name, unique_name); 138 | 139 | ret = sd_bus_add_match(bus, &slot, match, handle_name_lost, NULL); 140 | if (ret < 0) { 141 | logprint(ERROR, "dbus: failed to add NameOwnerChanged signal match: %s", 142 | strerror(-ret)); 143 | goto error; 144 | } 145 | 146 | while (keep_running) { 147 | ret = sd_bus_process(state.bus, NULL); 148 | if (ret < 0) { 149 | logprint(ERROR, "sd_bus_process failed: %s", strerror(-ret)); 150 | break; 151 | } 152 | 153 | if (ret > 0) 154 | continue; 155 | 156 | ret = sd_bus_wait(state.bus, (uint64_t)-1); 157 | if (ret < 0) { 158 | logprint(ERROR, "Failed to wait: %s\n", strerror(-ret)); 159 | break; 160 | } 161 | 162 | logprint(TRACE, "flushing bus\n"); 163 | sd_bus_flush(state.bus); 164 | } 165 | 166 | finish_config(&config); 167 | free(configfile); 168 | 169 | return EXIT_SUCCESS; 170 | 171 | error: 172 | sd_bus_slot_unref(slot); 173 | sd_bus_unref(bus); 174 | finish_config(&config); 175 | return EXIT_FAILURE; 176 | } 177 | -------------------------------------------------------------------------------- /src/core/config.c: -------------------------------------------------------------------------------- 1 | #include "config.h" 2 | #include "logger.h" 3 | #include "xdpw.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define FILECHOOSER_DEFAULT_CMD \ 13 | "/usr/share/xdg-desktop-portal-termfilechooser/yazi-wrapper.sh" 14 | #define FILECHOOSER_DEFAULT_DIR "$HOME" 15 | 16 | static char *expand_env(const char *input) { 17 | wordexp_t p; 18 | if (wordexp(input, &p, 0) != 0) { 19 | return strdup(input); // Return original if expansion fails 20 | } 21 | char *expanded = strdup(p.we_wordv[0]); 22 | wordfree(&p); 23 | return expanded; 24 | } 25 | 26 | void print_config(enum LOGLEVEL loglevel, struct xdpw_config *config) { 27 | logprint(loglevel, "config: cmd: %s", config->filechooser_conf.cmd); 28 | logprint(loglevel, "config: default_dir: %s", 29 | config->filechooser_conf.default_dir); 30 | } 31 | 32 | // NOTE: calling finish_config won't prepare the config to be read again from 33 | // config file with init_config since to pointers and other values won't be 34 | // reset to NULL, or 0 35 | void finish_config(struct xdpw_config *config) { 36 | logprint(DEBUG, "config: destroying config"); 37 | free(config->filechooser_conf.cmd); 38 | free(config->filechooser_conf.default_dir); 39 | } 40 | 41 | static void parse_string(char **dest, const char *value) { 42 | if (value == NULL || *value == '\0') { 43 | logprint(TRACE, "config: skipping empty value in config file"); 44 | return; 45 | } 46 | free(*dest); 47 | char *expanded_cmd = expand_env(value); 48 | *dest = strdup(expanded_cmd); 49 | free(expanded_cmd); 50 | } 51 | 52 | static int handle_ini_filechooser(struct config_filechooser *filechooser_conf, 53 | const char *key, const char *value) { 54 | if (strcmp(key, "cmd") == 0) { 55 | parse_string(&filechooser_conf->cmd, value); 56 | } else if (strcmp(key, "default_dir") == 0) { 57 | parse_string(&filechooser_conf->default_dir, value); 58 | } else { 59 | logprint(TRACE, "config: skipping invalid key in config file"); 60 | return 0; 61 | } 62 | return 1; 63 | } 64 | 65 | static int handle_ini_config(void *data, const char *section, const char *key, 66 | const char *value) { 67 | struct xdpw_config *config = (struct xdpw_config *)data; 68 | logprint(TRACE, "config: parsing section %s, key %s, value %s", section, key, 69 | value); 70 | 71 | if (strcmp(section, "filechooser") == 0) { 72 | return handle_ini_filechooser(&config->filechooser_conf, key, value); 73 | } 74 | 75 | logprint(TRACE, "config: skipping invalid key in config file"); 76 | return 0; 77 | } 78 | 79 | static void default_config(struct xdpw_config *config) { 80 | char *expanded_cmd = expand_env(FILECHOOSER_DEFAULT_CMD); 81 | config->filechooser_conf.cmd = strdup(expanded_cmd); 82 | free(expanded_cmd); 83 | 84 | char *expanded_dir = expand_env(FILECHOOSER_DEFAULT_DIR); 85 | config->filechooser_conf.default_dir = strdup(expanded_dir); 86 | free(expanded_dir); 87 | } 88 | 89 | static bool file_exists(const char *path) { 90 | return path && access(path, R_OK) != -1; 91 | } 92 | 93 | static char *config_path(const char *prefix, const char *filename) { 94 | if (!prefix || !prefix[0] || !filename || !filename[0]) { 95 | return NULL; 96 | } 97 | 98 | char *config_folder = "xdg-desktop-portal-termfilechooser"; 99 | 100 | size_t size = 3 + strlen(prefix) + strlen(config_folder) + strlen(filename); 101 | char *path = calloc(size, sizeof(char)); 102 | snprintf(path, size, "%s/%s/%s", prefix, config_folder, filename); 103 | return path; 104 | } 105 | 106 | static char *get_config_path(void) { 107 | const char *home = getenv("HOME"); 108 | char *config_home_fallback = NULL; 109 | if (home != NULL && home[0] != '\0') { 110 | size_t size_fallback = 1 + strlen(home) + strlen("/.config"); 111 | config_home_fallback = calloc(size_fallback, sizeof(char)); 112 | snprintf(config_home_fallback, size_fallback, "%s/.config", home); 113 | } 114 | 115 | const char *config_home = getenv("XDG_CONFIG_HOME"); 116 | if (config_home == NULL || config_home[0] == '\0') { 117 | config_home = config_home_fallback; 118 | } 119 | 120 | const char *prefix[2]; 121 | prefix[0] = config_home; 122 | prefix[1] = SYSCONFDIR "/xdg"; 123 | 124 | const char *xdg_current_desktop = getenv("XDG_CURRENT_DESKTOP"); 125 | const char *config_fallback = "config"; 126 | 127 | char *config_list = NULL; 128 | for (size_t i = 0; i < 2; i++) { 129 | if (xdg_current_desktop) { 130 | config_list = strdup(xdg_current_desktop); 131 | char *config = strtok(config_list, ":"); 132 | while (config) { 133 | char *path = config_path(prefix[i], config); 134 | if (!path) { 135 | config = strtok(NULL, ":"); 136 | continue; 137 | } 138 | logprint(TRACE, "config: trying config file %s", path); 139 | if (file_exists(path)) { 140 | free(config_list); 141 | free(config_home_fallback); 142 | return path; 143 | } 144 | free(path); 145 | config = strtok(NULL, ":"); 146 | } 147 | free(config_list); 148 | } 149 | char *path = config_path(prefix[i], config_fallback); 150 | if (!path) { 151 | continue; 152 | } 153 | logprint(TRACE, "config: trying config file %s", path); 154 | if (file_exists(path)) { 155 | free(config_home_fallback); 156 | return path; 157 | } 158 | free(path); 159 | } 160 | 161 | free(config_home_fallback); 162 | return NULL; 163 | } 164 | 165 | void init_config(char **const configfile, struct xdpw_config *config) { 166 | if (*configfile == NULL) { 167 | *configfile = get_config_path(); 168 | } 169 | 170 | default_config(config); 171 | if (*configfile == NULL) { 172 | logprint(ERROR, "config: no config file found"); 173 | return; 174 | } 175 | if (ini_parse(*configfile, handle_ini_config, config) < 0) { 176 | logprint(ERROR, "config: unable to load config file %s", *configfile); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xdg-desktop-portal-termfilechooser (Fork) 2 | 3 | 4 | 5 | - [Work best with org.freedesktop.FileManager1.common](#work-best-with-orgfreedesktopfilemanager1common) 6 | - [Alternative](#alternative) 7 | - [Installation](#installation) 8 | - [Build using package manager](#build-using-package-manager) 9 | - [Build from source](#build-from-source) 10 | - [Dependencies](#dependencies) 11 | - [Download the source](#download-the-source) 12 | - [Build](#build) 13 | - [Configuration](#configuration) 14 | - [Disable the original file picker portal](#disable-the-original-file-picker-portal) 15 | - [Systemd service](#systemd-service) 16 | - [Test](#test) 17 | - [Troubleshooting](#troubleshooting) 18 | - [For developers](#for-developers) 19 | - [Usage](#usage) 20 | - [Tricks (Optional)](#tricks-optional) 21 | - [For yazi only: Hover over placeholder file after moving it](#for-yazi-only-hover-over-placeholder-file-after-moving-it) 22 | - [For wezterm only: Display file operation in window title](#for-wezterm-only-display-file-operation-in-window-title) 23 | - [Floating file selector with tiling window manager like (hyprland, sway, i3, etc):](#floating-file-selector-with-tiling-window-manager-like-hyprland-sway-i3-etc) 24 | - [Documentation](#documentation) 25 | - [License](#license) 26 | 27 | 28 | 29 | [xdg-desktop-portal] backend for choosing files with your favorite file chooser. 30 | By default, it will use the [yazi](https://github.com/sxyazi/yazi) file manager, but this is customizable. 31 | Based on [xdg-desktop-portal-wlr] (xdpw). 32 | 33 | ## Work best with org.freedesktop.FileManager1.common 34 | 35 | Having trouble with "Open in Folder" in your web browser’s download manager — where it doesn’t highlight the downloaded file or respect your file manager configuration? If so, check out this file manager D-Bus service. There's a preview section to help you understand what it does: 36 | https://github.com/boydaihungst/org.freedesktop.FileManager1.common 37 | 38 | By combining `xdg-desktop-portal-termfilechooser` with `org.freedesktop.FileManager1.common`, 39 | you'll get a file-opening experience similar to macOS and Windows. 40 | 41 | ## Alternative 42 | 43 | For NixOs, more configurations, and better support, see: 44 | 45 | [https://github.com/hunkyburrito/xdg-desktop-portal-termfilechooser](https://github.com/hunkyburrito/xdg-desktop-portal-termfilechooser) 46 | 47 | ## Installation 48 | 49 | ### Build using package manager 50 | 51 | For Arch: 52 | 53 | ```sh 54 | yay -S xdg-desktop-portal-termfilechooser-boydaihungst-git 55 | ``` 56 | 57 | ### Build from source 58 | 59 | #### Dependencies 60 | 61 | On apt-based systems: 62 | 63 | ```sh 64 | sudo apt install xdg-desktop-portal build-essential ninja-build meson libinih-dev libsystemd-dev scdoc 65 | ``` 66 | 67 | For Arch, see the dependencies in the [AUR package](https://aur.archlinux.org/packages/xdg-desktop-portal-termfilechooser-boydaihungst-git#pkgdeps). 68 | 69 | #### Download the source 70 | 71 | ```sh 72 | git clone https://github.com/boydaihungst/xdg-desktop-portal-termfilechooser 73 | ``` 74 | 75 | #### Build 76 | 77 | - Remove legacy files: 78 | 79 | ```sh 80 | cd xdg-desktop-portal-termfilechooser 81 | chmod +x remove_legacy_file.sh && sudo remove_legacy_file.sh 82 | ``` 83 | 84 | - Then check and update if you are using any old version of wrapper script: `$HOME/.config/xdg-desktop-portal-termfilechooser/ANY-wrapper.sh` 85 | If you use wrapper scripts from `/usr/share/xdg-desktop-portal-termfilechooser/ANY-wrapper.sh`, then it always up to date. 86 | 87 | - Build and install: 88 | 89 | ```sh 90 | meson build --prefix=/usr 91 | ninja -C build 92 | sudo ninja -C build install 93 | ``` 94 | 95 | ## Configuration 96 | 97 | Copy the `config` and any of the wrapper scripts in `contrib` dir to `~/.config/xdg-desktop-portal-termfilechooser`. 98 | Edit the `config` file to set your preferred terminal emulator and file manager applications. 99 | 100 | For terminal emulator. You can set the `TERMCMD` environment variable instead of edit wrapper file. 101 | So you only need to copy `config`. By default wrappers is placed at `/usr/share/xdg-desktop-portal-termfilechooser/` 102 | 103 | Example: 104 | 105 | - `$HOME/.profile` 106 | - `.bashrc` 107 | 108 | ```sh 109 | # use wezterm intead of kitty 110 | export TERMCMD="wezterm start --always-new-process" 111 | ``` 112 | 113 | - `$HOME/.config/xdg-desktop-portal-termfilechooser/config` or `$XDG_CONFIG_HOME/xdg-desktop-portal-termfilechooser/config` 114 | Use yazi wrapper instead of ranger wrapper: 115 | 116 | ```dosini 117 | [filechooser] 118 | cmd=/usr/share/xdg-desktop-portal-termfilechooser/yazi-wrapper.sh 119 | default_dir=$HOME 120 | ``` 121 | 122 | - Use custom yazi wrapper instead of default wrapper: 123 | 124 | ```dosini 125 | [filechooser] 126 | cmd=$HOME/.config/xdg-desktop-portal-termfilechooser/yazi-wrapper.sh 127 | default_dir=$HOME 128 | ``` 129 | 130 | - or 131 | 132 | ```dosini 133 | [filechooser] 134 | cmd=$XDG_CONFIG_HOME/xdg-desktop-portal-termfilechooser/yazi-wrapper.sh 135 | default_dir=$HOME 136 | ``` 137 | 138 | The `default_dir` is used in case the app, which triggers download/upload or select file/folder, doesn't suggested location to save/open file/directory or suggested a relative path to its CWD, which we can't access from dbus message. 139 | For example in firefox it's `$HOME` the first time, after successfully selected/saved file, it will remember the last selected location. 140 | This location is suggested by the app (e.g. firefox), not by xdg-desktop-portal-termfilechooser itself. Normally, it remember the last selected location based on file extension. Rare case like Obsidian, always suggest relative path = `.`, so we have to fallback to `default_dir`, because we don't know where the CWD of Obsidian is. 141 | 142 | ### Disable the original file picker portal 143 | 144 | - If your xdg-desktop-portal version is >= [`1.18.0`](https://github.com/flatpak/xdg-desktop-portal/releases/tag/1.18.0), then you can specify the portal for FileChooser in `~/.config/xdg-desktop-portal/portals.conf` file (see the [flatpak docs](https://flatpak.github.io/xdg-desktop-portal/docs/portals.conf.html) and [ArchWiki](https://wiki.archlinux.org/title/XDG_Desktop_Portal#Configuration)): 145 | 146 | ```sh 147 | # Get version of xdg-desktop-portal 148 | xdg-desktop-portal --version 149 | # If xdg-desktop-portal not on $PATH, try: 150 | /usr/libexec/xdg-desktop-portal --version 151 | ``` 152 | 153 | Or, if it says `No such file or directory`: 154 | 155 | ```sh 156 | # Run command below to get the location of xdg-desktop-portal, which is ExecStart=COMMAND 157 | systemctl cat --user xdg-desktop-portal.service 158 | /path/to/xdg-desktop-portal -l TRACE -r 159 | ``` 160 | 161 | ```dosini 162 | [preferred] 163 | org.freedesktop.impl.portal.FileChooser=termfilechooser 164 | ``` 165 | 166 | - If your `xdg-desktop-portal --version` is older, you can remove `FileChooser` from `Interfaces` of the `{gtk;kde;…}.portal` files: 167 | 168 | ```sh 169 | find /usr/share/xdg-desktop-portal/portals -name '*.portal' -not -name 'termfilechooser.portal' \ 170 | -exec grep -q 'FileChooser' '{}' \; \ 171 | -exec sudo sed -i'.bak' 's/org\.freedesktop\.impl\.portal\.FileChooser;\?//g' '{}' \; 172 | ``` 173 | 174 | ### Systemd service 175 | 176 | - Restart the portal service: 177 | 178 | ```sh 179 | systemctl --user restart xdg-desktop-portal.service 180 | ``` 181 | 182 | ## Test 183 | 184 | ```sh 185 | GTK_USE_PORTAL=1 zenity --file-selection 186 | GTK_USE_PORTAL=1 zenity --file-selection --directory 187 | GTK_USE_PORTAL=1 zenity --file-selection --multiple 188 | 189 | # Change `USER` to your `username`: 190 | GTK_USE_PORTAL=1 zenity --file-selection --save --filename='/home/USER/save_file_$.txt 191 | ``` 192 | 193 | and additional options: `--multiple`, `--directory`, `--save`. 194 | 195 | #### Troubleshooting 196 | 197 | - After editing termfilechooser's config, restart its service: 198 | 199 | ```sh 200 | systemctl --user restart xdg-desktop-portal-termfilechooser.service 201 | ``` 202 | 203 | - Debug the termfilechooser: 204 | 205 | ```sh 206 | systemctl --user stop xdg-desktop-portal-termfilechooser.service 207 | /usr/lib/xdg-desktop-portal-termfilechooser -l TRACE -r 208 | ``` 209 | 210 | Or, if it says `No such file or directory`: 211 | 212 | ```sh 213 | # Run command below to get the location of xdg-desktop-portal-termfilechooser, which is ExecStart=COMMAND 214 | systemctl --user cat xdg-desktop-portal-termfilechooser.service. 215 | /path/to/xdg-desktop-portal-termfilechooser -l TRACE -r 216 | ``` 217 | 218 | This way the output from the wrapper scripts (e.g. `yazi-wrapper.sh`) will be written to the same terminal. 219 | Then try to reproduce the bug. Finally, upload create an issue and upload logs, 220 | app use xdg-open to open/save file and the custom wrapper script if you use it. 221 | 222 | - Since [this merge request in GNOME](https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/4829), `GTK_USE_PORTAL=1` seems to be replaced with `GDK_DEBUG=portals`. 223 | 224 | - See also: [Troubleshooting section in ArchWiki](https://wiki.archlinux.org/title/XDG_Desktop_Portal#Troubleshooting). 225 | 226 | ## For developers 227 | 228 | - Stop service: `systemctl --user stop xdg-desktop-portal-termfilechooser.service` 229 | - Build and run in debug mode: `meson build --prefix=/usr --reconfigure && ninja -C build && ./build/xdg-desktop-portal-termfilechooser -l TRACE -r` 230 | - Monitor dbus message: `dbus-monitor --session "interface='org.freedesktop.portal.FileChooser'"` 231 | - Explain dbus message values meaning: https://github.com/flatpak/xdg-desktop-portal/blob/main/data/org.freedesktop.portal.FileChooser.xml 232 | 233 | ## Usage 234 | 235 | Firefox has a setting in its `about:config` to always use XDG desktop portal's file chooser: set `widget.use-xdg-desktop-portal.file-picker` to `1`. See [https://wiki.archlinux.org/title/Firefox#XDG_Desktop_Portal_integration](https://wiki.archlinux.org/title/Firefox#XDG_Desktop_Portal_integration). 236 | 237 | ## Tricks (Optional) 238 | 239 | ### For yazi only: Hover over placeholder file after moving it 240 | 241 | If you use yazi-wrapper.sh, you could install [boydaihungst/hover-after-moved.yazi](https://github.com/boydaihungst/hover-after-moved.yazi). 242 | So when you move placeholder file to other directory, yazi will auto hover over it. 243 | The placeholder file is created by termfilechooser when you save/download a file. 244 | 245 | ### For wezterm only: Display file operation in window title 246 | 247 | If you use any wrapper.sh with wezterm, you could add the following lua script to `wezterm.lua`. 248 | So when wezterm is open with your preferred file manager, it will display workspace name beside the cwd path: 249 | 250 | ```lua 251 | local wezterm = require("wezterm") 252 | wezterm.on("format-window-title", function(tab, pane, tabs, panes, config) 253 | local ws = wezterm.mux.get_active_workspace() 254 | local title = tab.active_pane.title 255 | if ws ~= "default" then 256 | return string.format("[%s] %s", ws, title) 257 | end 258 | return title 259 | end) 260 | ``` 261 | 262 | Also open your preferred wrapper.sh and edit the line `termcmd=` to `termcmd="${TERMCMD:-/usr/bin/wezterm start --class 'yazi' --workspace $(quote_string "$TITLE")}"` 263 | 264 | Results: 265 | 266 | image 267 | 268 | image 269 | 270 | image 271 | 272 | ### Floating file selector with tiling window manager like (hyprland, sway, i3, etc): 273 | 274 | Open your preferred wrapper.sh and edit `--class` or `--app-id` in the line `termcmd=` to whatever value you want. 275 | For example kitty + yazi: `termcmd="${TERMCMD:-kitty --app-id 'yazi-selector' --title $(quote_string "$TITLE")}"` 276 | 277 | hyprland.conf floating window with app-id/class = yazi-selector: 278 | 279 | ```dosini 280 | windowrulev2 = float, class:^yazi-selector$ 281 | windowrulev2 = size 80% 90%, class:^yazi-selector$ 282 | ``` 283 | 284 | ## Documentation 285 | 286 | See `man 5 xdg-desktop-portal-termfilechooser`. 287 | 288 | ## License 289 | 290 | MIT 291 | 292 | [xdg-desktop-portal](https://github.com/flatpak/xdg-desktop-portal) 293 | [original author: GermainZ](https://github.com/GermainZ/xdg-desktop-portal-termfilechooser) 294 | [xdg-desktop-portal-wlr](https://github.com/emersion/xdg-desktop-portal-wlr) 295 | [ranger](https://github.com/ranger/ranger/) 296 | [yazi](https://github.com/sxyazi/yazi/) 297 | [lf](https://github.com/gokcehan/lf) 298 | [fzf](https://github.com/junegunn/fzf) 299 | [nnn](https://github.com/jarun/nnn) 300 | [vifm](https://github.com/vifm/vifm) 301 | -------------------------------------------------------------------------------- /src/filechooser/filechooser.c: -------------------------------------------------------------------------------- 1 | #include "filechooser.h" 2 | #include "xdpw.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #define PATH_PREFIX "file://" 15 | #define PATH_PORTAL "/tmp/termfilechooser.portal" 16 | 17 | static const char object_path[] = "/org/freedesktop/portal/desktop"; 18 | static const char interface_name[] = "org.freedesktop.impl.portal.FileChooser"; 19 | 20 | // Helper function to URL encode a string 21 | static char *url_encode(const char *s) { 22 | const char *hex = "0123456789abcdef"; 23 | size_t len = strlen(s); 24 | unsigned char *encoded = 25 | malloc(len * 3 + 1); // Worst case: all chars need encoding 26 | unsigned char *p = encoded; 27 | for (size_t i = 0; i < len; i++) { 28 | unsigned char c = s[i]; 29 | if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~' || 30 | c == '/') { 31 | *p++ = c; 32 | } else { 33 | *p++ = '%'; 34 | *p++ = hex[c >> 4]; 35 | *p++ = hex[c & 15]; 36 | } 37 | } 38 | *p = '\0'; 39 | return (char *)encoded; 40 | } 41 | 42 | static int exec_filechooser(void *data, bool writing, bool multiple, 43 | bool directory, char *path, char ***selected_files, 44 | size_t *num_selected_files) { 45 | struct xdpw_state *state = data; 46 | 47 | char *cmd_script = state->config->filechooser_conf.cmd; 48 | 49 | if (!cmd_script) { 50 | logprint(ERROR, "cmd not specified"); 51 | return -1; 52 | } 53 | 54 | logprint(DEBUG, "Command script path: %s", cmd_script); 55 | if (access(cmd_script, X_OK) != 0) { 56 | logprint(ERROR, "Command script is not executable: %s", cmd_script); 57 | return -1; 58 | } 59 | 60 | const char *args[] = {cmd_script, 61 | multiple ? "1" : "0", 62 | directory ? "1" : "0", 63 | writing ? "1" : "0", 64 | path ? path : "", 65 | PATH_PORTAL, 66 | get_logger_level() >= DEBUG ? "1" : "0", 67 | NULL}; 68 | 69 | // Check if the portal file exists and have read write permission 70 | if (access(PATH_PORTAL, F_OK) == 0) { 71 | if (access(PATH_PORTAL, R_OK | W_OK) != 0) { 72 | logprint(ERROR, 73 | "failed to start portal, make sure you have permission to read " 74 | "and write %s", 75 | PATH_PORTAL); 76 | return -1; 77 | } 78 | } 79 | remove(PATH_PORTAL); 80 | 81 | pid_t pid = fork(); 82 | if (pid == -1) { 83 | logprint(ERROR, "fork failed: %d", strerror(errno)); 84 | return -1; 85 | } else if (pid == 0) { 86 | // Child process 87 | execvp(cmd_script, (char *const *)args); 88 | logprint(ERROR, "execvp failed: %s", strerror(errno)); 89 | _exit(EXIT_FAILURE); 90 | } else { 91 | // Parent process 92 | int status; 93 | waitpid(pid, &status, 0); 94 | if (WIFEXITED(status) && WEXITSTATUS(status) != 0) { 95 | logprint(ERROR, "command failed with status %d", WEXITSTATUS(status)); 96 | return -1; 97 | } 98 | } 99 | 100 | FILE *fp = fopen(PATH_PORTAL, "r+"); 101 | if (fp == NULL) { 102 | logprint(DEBUG, "Aborted"); 103 | *selected_files = NULL; 104 | *num_selected_files = 0; 105 | return -1; 106 | } 107 | 108 | size_t num_lines = 0; 109 | int cr; 110 | 111 | // NOTE: Some file manager like lf doesn't add newline at the end of file 112 | 113 | // Go to the end to check size 114 | if (fseek(fp, 0, SEEK_END) != 0) { 115 | fclose(fp); 116 | *selected_files = NULL; 117 | *num_selected_files = 0; 118 | return -1; 119 | } 120 | 121 | long size = ftell(fp); 122 | if (size == 0) { 123 | // Empty file, do nothing 124 | fclose(fp); 125 | return 0; 126 | } 127 | 128 | // Check last character 129 | if (fseek(fp, -1, SEEK_END) != 0) { 130 | fclose(fp); 131 | *selected_files = NULL; 132 | *num_selected_files = 0; 133 | return -1; 134 | } 135 | 136 | int last = fgetc(fp); 137 | if (last != '\n') { 138 | fseek(fp, 0, SEEK_END); // move to EOF again 139 | fputc('\n', fp); // append newline 140 | } 141 | fseek(fp, 0, SEEK_SET); 142 | 143 | // Count lines 144 | while ((cr = getc(fp)) != EOF) { 145 | if (cr == '\n') { 146 | num_lines++; 147 | } 148 | } 149 | if (ferror(fp)) { 150 | fclose(fp); 151 | *selected_files = NULL; 152 | *num_selected_files = 0; 153 | return 1; 154 | } 155 | rewind(fp); 156 | 157 | if (num_lines == 0) { 158 | num_lines = 1; 159 | } 160 | 161 | *num_selected_files = num_lines; 162 | *selected_files = malloc((num_lines + 1) * sizeof(char *)); 163 | if (*selected_files == NULL) { 164 | fclose(fp); 165 | *num_selected_files = 0; 166 | return -1; 167 | } 168 | 169 | for (size_t i = 0; i < num_lines; i++) { 170 | size_t n = 0; 171 | char *line = NULL; 172 | ssize_t nread = getline(&line, &n, fp); 173 | if (ferror(fp)) { 174 | free(line); 175 | for (size_t j = 0; j < i; j++) { 176 | free((*selected_files)[j]); 177 | } 178 | free(*selected_files); 179 | fclose(fp); 180 | *selected_files = NULL; 181 | *num_selected_files = 0; 182 | return 1; 183 | } 184 | 185 | if (nread > 0 && line[nread - 1] == '\n') { 186 | line[nread - 1] = '\0'; 187 | } 188 | char *encoded_path = url_encode(line); 189 | size_t str_size = strlen(PATH_PREFIX) + strlen(encoded_path) + 1; 190 | (*selected_files)[i] = malloc(str_size); 191 | snprintf((*selected_files)[i], str_size, "%s%s", PATH_PREFIX, encoded_path); 192 | free(line); 193 | free(encoded_path); 194 | } 195 | (*selected_files)[num_lines] = NULL; 196 | 197 | fclose(fp); 198 | return 0; 199 | } 200 | 201 | static int method_open_file(sd_bus_message *msg, void *data, 202 | sd_bus_error *ret_error) { 203 | int ret = 0; 204 | 205 | char *handle, *app_id, *parent_window, *title; 206 | ret = sd_bus_message_read(msg, "osss", &handle, &app_id, &parent_window, 207 | &title); 208 | if (ret < 0) { 209 | return ret; 210 | } 211 | 212 | ret = sd_bus_message_enter_container(msg, 'a', "{sv}"); 213 | if (ret < 0) { 214 | return ret; 215 | } 216 | char *key; 217 | int inner_ret = 0; 218 | int multiple = 0, directory = 0; 219 | char *current_folder = NULL; 220 | 221 | while ((ret = sd_bus_message_enter_container(msg, 'e', "sv")) > 0) { 222 | inner_ret = sd_bus_message_read(msg, "s", &key); 223 | if (inner_ret < 0) { 224 | return inner_ret; 225 | } 226 | 227 | logprint(DEBUG, "dbus: option %s", key); 228 | if (strcmp(key, "multiple") == 0) { 229 | sd_bus_message_read(msg, "v", "b", &multiple); 230 | logprint(DEBUG, "dbus: option multiple: %d", multiple); 231 | } else if (strcmp(key, "modal") == 0) { 232 | int modal; 233 | sd_bus_message_read(msg, "v", "b", &modal); 234 | logprint(DEBUG, "dbus: option modal: %d", modal); 235 | } else if (strcmp(key, "directory") == 0) { 236 | sd_bus_message_read(msg, "v", "b", &directory); 237 | logprint(DEBUG, "dbus: option directory: %d", directory); 238 | } else if (strcmp(key, "current_folder") == 0) { 239 | const void *p = NULL; 240 | size_t sz = 0; 241 | inner_ret = sd_bus_message_enter_container(msg, 'v', "ay"); 242 | if (inner_ret < 0) { 243 | return inner_ret; 244 | } 245 | inner_ret = sd_bus_message_read_array(msg, 'y', &p, &sz); 246 | if (inner_ret < 0) { 247 | return inner_ret; 248 | } 249 | inner_ret = sd_bus_message_exit_container(msg); 250 | if (inner_ret < 0) { 251 | return inner_ret; 252 | } 253 | 254 | current_folder = (char *)p; 255 | logprint(DEBUG, "dbus: option current_folder: %s", current_folder); 256 | } else { 257 | logprint(WARN, "dbus: unknown option %s", key); 258 | sd_bus_message_skip(msg, "v"); 259 | } 260 | 261 | inner_ret = sd_bus_message_exit_container(msg); 262 | if (inner_ret < 0) { 263 | return inner_ret; 264 | } 265 | } 266 | if (ret < 0) { 267 | return ret; 268 | } 269 | ret = sd_bus_message_exit_container(msg); 270 | if (ret < 0) { 271 | return ret; 272 | } 273 | 274 | // TODO: cleanup this 275 | struct xdpw_request *req = 276 | xdpw_request_create(sd_bus_message_get_bus(msg), handle); 277 | if (req == NULL) { 278 | return -ENOMEM; 279 | } 280 | 281 | char **selected_files = NULL; 282 | size_t num_selected_files = 0; 283 | if (current_folder == NULL || current_folder[0] != '/') { 284 | struct xdpw_state *state = data; 285 | char *default_dir = state->config->filechooser_conf.default_dir; 286 | if (!default_dir) { 287 | logprint(ERROR, "default_dir not specified"); 288 | return -1; 289 | } 290 | current_folder = default_dir; 291 | } 292 | 293 | ret = exec_filechooser(data, false, multiple, directory, current_folder, 294 | &selected_files, &num_selected_files); 295 | if (ret) { 296 | goto cleanup; 297 | } 298 | 299 | logprint(TRACE, "(OpenFile) Number of selected files: %d", 300 | num_selected_files); 301 | for (size_t i = 0; i < num_selected_files; i++) { 302 | logprint(TRACE, "%d. %s", i, selected_files[i]); 303 | } 304 | 305 | sd_bus_message *reply = NULL; 306 | ret = sd_bus_message_new_method_return(msg, &reply); 307 | if (ret < 0) { 308 | goto cleanup; 309 | } 310 | 311 | ret = sd_bus_message_append(reply, "u", PORTAL_RESPONSE_SUCCESS, 1); 312 | if (ret < 0) { 313 | goto cleanup; 314 | } 315 | 316 | ret = sd_bus_message_open_container(reply, 'a', "{sv}"); 317 | if (ret < 0) { 318 | goto cleanup; 319 | } 320 | 321 | ret = sd_bus_message_open_container(reply, 'e', "sv"); 322 | if (ret < 0) { 323 | goto cleanup; 324 | } 325 | 326 | ret = sd_bus_message_append_basic(reply, 's', "uris"); 327 | if (ret < 0) { 328 | goto cleanup; 329 | } 330 | 331 | ret = sd_bus_message_open_container(reply, 'v', "as"); 332 | if (ret < 0) { 333 | goto cleanup; 334 | } 335 | 336 | ret = sd_bus_message_append_strv(reply, selected_files); 337 | if (ret < 0) { 338 | goto cleanup; 339 | } 340 | 341 | ret = sd_bus_message_close_container(reply); 342 | if (ret < 0) { 343 | goto cleanup; 344 | } 345 | 346 | ret = sd_bus_message_close_container(reply); 347 | if (ret < 0) { 348 | goto cleanup; 349 | } 350 | 351 | ret = sd_bus_message_close_container(reply); 352 | if (ret < 0) { 353 | goto cleanup; 354 | } 355 | 356 | ret = sd_bus_send(NULL, reply, NULL); 357 | if (ret < 0) { 358 | goto cleanup; 359 | } 360 | 361 | sd_bus_message_unref(reply); 362 | 363 | cleanup: 364 | for (size_t i = 0; i < num_selected_files; i++) { 365 | free(selected_files[i]); 366 | } 367 | free(selected_files); 368 | 369 | remove(PATH_PORTAL); 370 | return ret; 371 | } 372 | 373 | static int method_save_file(sd_bus_message *msg, void *data, 374 | sd_bus_error *ret_error) { 375 | int ret = 0; 376 | 377 | char *handle, *app_id, *parent_window, *title; 378 | ret = sd_bus_message_read(msg, "osss", &handle, &app_id, &parent_window, 379 | &title); 380 | if (ret < 0) { 381 | return ret; 382 | } 383 | 384 | ret = sd_bus_message_enter_container(msg, 'a', "{sv}"); 385 | if (ret < 0) { 386 | return ret; 387 | } 388 | char *key; 389 | int inner_ret = 0; 390 | char *current_name; 391 | char *current_folder = NULL; 392 | while ((ret = sd_bus_message_enter_container(msg, 'e', "sv")) > 0) { 393 | inner_ret = sd_bus_message_read(msg, "s", &key); 394 | if (inner_ret < 0) { 395 | return inner_ret; 396 | } 397 | 398 | logprint(DEBUG, "dbus: option %s", key); 399 | if (strcmp(key, "current_name") == 0) { 400 | sd_bus_message_read(msg, "v", "s", ¤t_name); 401 | logprint(DEBUG, "dbus: option current_name: %s", current_name); 402 | } else if (strcmp(key, "current_folder") == 0) { 403 | const void *p = NULL; 404 | size_t sz = 0; 405 | inner_ret = sd_bus_message_enter_container(msg, 'v', "ay"); 406 | if (inner_ret < 0) { 407 | return inner_ret; 408 | } 409 | inner_ret = sd_bus_message_read_array(msg, 'y', &p, &sz); 410 | if (inner_ret < 0) { 411 | return inner_ret; 412 | } 413 | current_folder = (char *)p; 414 | logprint(DEBUG, "dbus: option current_folder: %s", current_folder); 415 | } else if (strcmp(key, "current_file") == 0) { 416 | // when saving an existing file 417 | const void *p = NULL; 418 | size_t sz = 0; 419 | inner_ret = sd_bus_message_enter_container(msg, 'v', "ay"); 420 | if (inner_ret < 0) { 421 | return inner_ret; 422 | } 423 | inner_ret = sd_bus_message_read_array(msg, 'y', &p, &sz); 424 | if (inner_ret < 0) { 425 | return inner_ret; 426 | } 427 | current_name = (char *)p; 428 | logprint(DEBUG, 429 | "dbus: option replace current_name with current_file : %s", 430 | current_name); 431 | } else { 432 | logprint(WARN, "dbus: unknown option %s", key); 433 | sd_bus_message_skip(msg, "v"); 434 | } 435 | 436 | inner_ret = sd_bus_message_exit_container(msg); 437 | if (inner_ret < 0) { 438 | return inner_ret; 439 | } 440 | } 441 | 442 | // TODO: cleanup this 443 | struct xdpw_request *req = 444 | xdpw_request_create(sd_bus_message_get_bus(msg), handle); 445 | if (req == NULL) { 446 | return -ENOMEM; 447 | } 448 | 449 | if (current_folder == NULL || current_folder[0] != '/') { 450 | struct xdpw_state *state = data; 451 | char *default_dir = state->config->filechooser_conf.default_dir; 452 | if (!default_dir) { 453 | logprint(ERROR, "default_dir not specified"); 454 | return -1; 455 | } 456 | current_folder = default_dir; 457 | } 458 | 459 | size_t path_size = 460 | snprintf(NULL, 0, "%s/%s", current_folder, current_name) + 1; 461 | char *path = malloc(path_size); 462 | snprintf(path, path_size, "%s/%s", current_folder, current_name); 463 | 464 | bool file_already_exists = true; 465 | while (file_already_exists) { 466 | if (access(path, F_OK) == 0) { 467 | char *path_tmp = malloc(path_size); 468 | snprintf(path_tmp, path_size, "%s", path); 469 | path_size += 1; 470 | path = realloc(path, path_size); 471 | snprintf(path, path_size, "%s_", path_tmp); 472 | free(path_tmp); 473 | } else { 474 | file_already_exists = false; 475 | } 476 | } 477 | 478 | char **selected_files = NULL; 479 | size_t num_selected_files = 0; 480 | ret = exec_filechooser(data, true, false, false, path, &selected_files, 481 | &num_selected_files); 482 | if (ret || num_selected_files == 0) { 483 | remove(path); 484 | ret = -1; 485 | goto cleanup; 486 | } 487 | 488 | logprint(TRACE, "(SaveFile) Number of selected files: %d", 489 | num_selected_files); 490 | for (size_t i = 0; i < num_selected_files; i++) { 491 | logprint(TRACE, "%d. %s", i, selected_files[i]); 492 | } 493 | 494 | sd_bus_message *reply = NULL; 495 | ret = sd_bus_message_new_method_return(msg, &reply); 496 | if (ret < 0) { 497 | goto cleanup; 498 | } 499 | 500 | ret = sd_bus_message_append(reply, "u", PORTAL_RESPONSE_SUCCESS, 1); 501 | if (ret < 0) { 502 | goto cleanup; 503 | } 504 | 505 | ret = sd_bus_message_open_container(reply, 'a', "{sv}"); 506 | if (ret < 0) { 507 | goto cleanup; 508 | } 509 | 510 | ret = sd_bus_message_open_container(reply, 'e', "sv"); 511 | if (ret < 0) { 512 | goto cleanup; 513 | } 514 | 515 | ret = sd_bus_message_append_basic(reply, 's', "uris"); 516 | if (ret < 0) { 517 | goto cleanup; 518 | } 519 | 520 | ret = sd_bus_message_open_container(reply, 'v', "as"); 521 | if (ret < 0) { 522 | goto cleanup; 523 | } 524 | 525 | ret = sd_bus_message_append_strv(reply, selected_files); 526 | if (ret < 0) { 527 | goto cleanup; 528 | } 529 | 530 | ret = sd_bus_message_close_container(reply); 531 | if (ret < 0) { 532 | goto cleanup; 533 | } 534 | 535 | ret = sd_bus_message_close_container(reply); 536 | if (ret < 0) { 537 | goto cleanup; 538 | } 539 | 540 | ret = sd_bus_message_close_container(reply); 541 | if (ret < 0) { 542 | goto cleanup; 543 | } 544 | 545 | ret = sd_bus_send(NULL, reply, NULL); 546 | if (ret < 0) { 547 | goto cleanup; 548 | } 549 | 550 | sd_bus_message_unref(reply); 551 | 552 | cleanup: 553 | for (size_t i = 0; i < num_selected_files; i++) { 554 | free(selected_files[i]); 555 | } 556 | free(selected_files); 557 | free(path); 558 | 559 | remove(PATH_PORTAL); 560 | return ret; 561 | } 562 | 563 | static const sd_bus_vtable filechooser_vtable[] = { 564 | SD_BUS_VTABLE_START(0), 565 | SD_BUS_METHOD("OpenFile", "osssa{sv}", "ua{sv}", method_open_file, 566 | SD_BUS_VTABLE_UNPRIVILEGED), 567 | SD_BUS_METHOD("SaveFile", "osssa{sv}", "ua{sv}", method_save_file, 568 | SD_BUS_VTABLE_UNPRIVILEGED), 569 | SD_BUS_VTABLE_END}; 570 | 571 | int xdpw_filechooser_init(struct xdpw_state *state) { 572 | // TODO: cleanup 573 | sd_bus_slot *slot = NULL; 574 | logprint(DEBUG, "dbus: init %s", interface_name); 575 | return sd_bus_add_object_vtable(state->bus, &slot, object_path, 576 | interface_name, filechooser_vtable, state); 577 | } 578 | --------------------------------------------------------------------------------