Repository: qutebrowser/qutebrowser

Issue:
Add an option to use the native filepicker for downloads
Some users would prefer a native filepicker dialog to the current statusbar input (without completion), so there should be an option to use that.

edit: In addition, this should allow to switch from the default behavior of using the native file dialog for uploads to using the statusbar input as well.



Discussion:
+1 this is helpful when you want to preview images before uploading

Hi, I'm trying to get involved in the development of Qutebrowser and I figured this may be a good place for me to start, could you tell me what files I should look at and any other tips you have for me. Thanks!
Hey @BunnyApocalypse!

Right now is a bit of a bad timing as I have a quite big backlog (as I was busy with exams and then holidays until this week), and because a new config system is soon going to be merged, which will make contributions against the `master` branch conflict once that's in.

I'll get back to you in a few days though - if you don't hear back within a week or so, feel free to ping me again!

In the meantime, you might want to take a look at the [contribution docs](https://github.com/qutebrowser/qutebrowser/blob/master/CONTRIBUTING.asciidoc) and get the testsuite to run on your machine.
@The-Compiler Isn't this done already? I have no idea what the "statusbar input" is.
@user202729 What's the file prompt thingy nowadays used to be an input in the statusbar. However, this isn't done yet -- there still isn't an option to use Qt's native file picker instead.
I have currently the problem that when using qutebrowser under Gnome 3.34, the GTK-file-chooser comes up when trying to upload files, but is not usable because it is listing only directories.

I don't mind which file-chooser comes up, but a non-functional one does not help me. When I read „native“, does this mean it will be an upload dialog like it is already done for saving files?
@poinck The GTK file picker *is* the native file picker (rather than qutebrowser's), so this issue is pretty much the opposite of what you describe. You're probably seeing #5231, which is a Qt bug fixed in Qt 5.14.2.
@The-Compiler Unfortunatly I have only 5.14.1 available on my distro. /:
For downloads, this is also useful for Flatpak, see #4879. Additionally, it'd be nice to have a shortcut in qutebrowser's prompt to open the native one instead.
For what it's worth, you can _kind of_ use an external file manager in place of the download prompt if your external file manager has decent commandline options and you're willing to create an alias and trigger a custom userscript.

Here's a proof-of-concept userscript that downloads the current page and lets you choose a destination with the help of ranger:

```
#!/usr/bin/env bash
set -o errexit
set -o pipefail

if [ -z "$QUTE_FIFO" ] ; then
    help
    exit
fi

tmp_file="/tmp/qute-ranger-chosen"

if test -f "$tmp_file"; then
    rm "$tmp_file"
fi

xterm -e "ranger --choosedir=\"$tmp_file\" --show-only-dirs"

if test -f "$tmp_file"; then
    chosen="$(cat "$tmp_file")"
    rm "$tmp_file"
    xterm -e "read -p \"File to save as? [d]efault/(o)verwrite/(n)ew: \" -N 1 choice; echo \"\$choice\" > \"$tmp_file\""
    choice="$(cat "$tmp_file")"
    rm "$tmp_file"
    case $choice in
        o)
            xterm -e "ranger --choosefile=\"$tmp_file\" --selectfile=\"$chosen\""
            if test -f "$tmp_file"; then
                chosen="$(cat "$tmp_file")"
                rm "$tmp_file"
            fi
            ;;
        n)
            xterm -e "read -p \"Name: \" name; echo \"\$name\" > \"$tmp_file\""
            if test -f "$tmp_file"; then
                chosen="$chosen/$(cat "$tmp_file")"
                rm "$tmp_file"
            fi
            ;;
    esac
    echo "download --dest \"$chosen\"" >> "$QUTE_FIFO"
fi
```
Given that we already have some discussion about using external filepickers for downloads in here, let's use this issue for both the Qt and external file pickers.

@AckslD maybe this is something you might want to pick up?
@The-Compiler That would indeed be great. I have less bandwidth at the moment so if someone wants to pick it up, go ahead. Otherwise I might try go get some time but might take a bit longer :)
Sure thing - we're not in a rush 🙂
@The-Compiler I thought of trying to look into this again. Some time ago I tried to look through the code to see where to change how download selection is handled but couldn't completely figure it out. Maybe you have some pointers for roughly where in the code you see this happening. Otherwise I'll try a bit more to figure it out :) 
I've easily changed the file picker to ranger by using the `fileselect.handler` option, but apparently the download selector is a different story. It'd be great to be able to replace it with ranger, and better still to improve the default interface to allow autocomplete and a more natural interaction, closer to [selectrum](https://github.com/raxod502/selectrum), for example... <3

Resolving patch:
diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py
--- a/qutebrowser/browser/shared.py
+++ b/qutebrowser/browser/shared.py
@@ -404,7 +404,7 @@ class FileSelectionMode(enum.Enum):
 
 
 def choose_file(qb_mode: FileSelectionMode) -> List[str]:
-    """Select file(s)/folder for uploading, using external command defined in config.
+    """Select file(s)/folder for up-/downloading, using an external command.
 
     Args:
         qb_mode: File selection mode
@@ -450,7 +450,7 @@ def _execute_fileselect_command(
     """Execute external command to choose file.
 
     Args:
-        multiple: Should selecting multiple files be allowed.
+        qb_mode: Should selecting multiple files be allowed.
         tmpfilename: Path to the temporary file if used, otherwise None.
 
     Return:
diff --git a/qutebrowser/mainwindow/prompt.py b/qutebrowser/mainwindow/prompt.py
--- a/qutebrowser/mainwindow/prompt.py
+++ b/qutebrowser/mainwindow/prompt.py
@@ -467,6 +467,27 @@ def prompt_yank(self, sel=False):
         utils.set_clipboard(question.url, sel)
         message.info("Yanked to {}: {}".format(target, question.url))
 
+    @cmdutils.register(
+        instance='prompt-container', scope='window',
+        modes=[usertypes.KeyMode.prompt])
+    def prompt_fileselect_external(self):
+        """Choose a location using a configured external picker."""
+        assert self._prompt is not None
+        if not isinstance(self._prompt, FilenamePrompt):
+            raise cmdutils.CommandError(
+                "Can only launch external fileselect for FilenamePrompt, "
+                f"not {self._prompt.__class__.__name__}"
+            )
+        # XXX to avoid current cyclic import
+        from qutebrowser.browser import shared
+        folders = shared.choose_file(shared.FileSelectionMode.folder)
+        if not folders:
+            message.info("No folder chosen.")
+            return
+        # choose_file already checks that this is max one folder
+        assert len(folders) == 1
+        self.prompt_accept(folders[0])
+
 
 class LineEdit(QLineEdit):
 
@@ -834,6 +855,7 @@ def _allowed_commands(self):
             ('prompt-open-download', "Open download"),
             ('prompt-open-download --pdfjs', "Open download via PDF.js"),
             ('prompt-yank', "Yank URL"),
+            ('prompt-fileselect-external', "Launch external file selector"),
         ]
         return cmds
 


Test patch:
diff --git a/tests/end2end/features/conftest.py b/tests/end2end/features/conftest.py
--- a/tests/end2end/features/conftest.py
+++ b/tests/end2end/features/conftest.py
@@ -712,3 +712,35 @@ def check_option_per_domain(quteproc, option, value, pattern, server):
     pattern = pattern.replace('(port)', str(server.port))
     actual_value = quteproc.get_setting(option, pattern=pattern)
     assert actual_value == value
+
+
+@bdd.when(bdd.parsers.parse('I setup a fake {kind} fileselector '
+                            'selecting "{files}" and writes to {output_type}'))
+def set_up_fileselector(quteproc, py_proc, tmpdir, kind, files, output_type):
+    """Set up fileselect.xxx.command to select the file(s)."""
+    cmd, args = py_proc(r"""
+        import os
+        import sys
+        tmp_file = None
+        for i, arg in enumerate(sys.argv):
+            if arg.startswith('--file='):
+                tmp_file = arg[len('--file='):]
+                sys.argv.pop(i)
+                break
+        selected_files = sys.argv[1:]
+        if tmp_file is None:
+            for selected_file in selected_files:
+                print(os.path.abspath(selected_file))
+        else:
+            with open(tmp_file, 'w') as f:
+                for selected_file in selected_files:
+                    f.write(os.path.abspath(selected_file) + '\n')
+    """)
+    files = files.replace('(tmpdir)', str(tmpdir))
+    files = files.replace('(dirsep)', os.sep)
+    args += files.split(' ')
+    if output_type == "a temporary file":
+        args += ['--file={}']
+    fileselect_cmd = json.dumps([cmd, *args])
+    quteproc.set_setting('fileselect.handler', 'external')
+    quteproc.set_setting(f'fileselect.{kind}.command', fileselect_cmd)
diff --git a/tests/end2end/features/downloads.feature b/tests/end2end/features/downloads.feature
--- a/tests/end2end/features/downloads.feature
+++ b/tests/end2end/features/downloads.feature
@@ -684,3 +684,25 @@ Feature: Downloading things from a website.
         When I set downloads.location.prompt to false
         And I open 500-inline
         Then the error "Download error: *INTERNAL SERVER ERROR" should be shown
+
+    ## External download path fileselector
+
+    Scenario: Select download path
+        When I set downloads.location.prompt to true
+        And I setup a fake folder fileselector selecting "(tmpdir)(dirsep)downloads(dirsep)subdir" and writes to a temporary file
+        And I open data/downloads/downloads.html
+        And I run :click-element id download
+        And I wait for the download prompt for "*"
+        And I run :prompt-fileselect-external
+        And I wait until the download is finished
+        Then the downloaded file subdir/download.bin should exist
+
+    Scenario: No download folder chosen
+        When I set downloads.location.prompt to true
+        And I set fileselect.folder.command to ['echo', '{}']
+        And I open data/downloads/downloads.html
+        And I run :click-element id download
+        And I wait for the download prompt for "*"
+        And I run :prompt-fileselect-external
+        Then the message "No folder chosen." should be shown
+        And the download prompt should be shown with "(tmpdir)(dirsep)downloads"
diff --git a/tests/end2end/features/test_editor_bdd.py b/tests/end2end/features/test_editor_bdd.py
--- a/tests/end2end/features/test_editor_bdd.py
+++ b/tests/end2end/features/test_editor_bdd.py
@@ -178,33 +178,3 @@ def save_editor_wait(tmpdir):
     # for posix, there IS a member so we need to ignore useless-suppression
     # pylint: disable=no-member,useless-suppression
     os.kill(pid, signal.SIGUSR2)
-
-
-@bdd.when(bdd.parsers.parse('I setup a fake {kind} fileselector '
-                            'selecting "{files}" and writes to {output_type}'))
-def set_up_fileselector(quteproc, py_proc, kind, files, output_type):
-    """Set up fileselect.xxx.command to select the file(s)."""
-    cmd, args = py_proc(r"""
-        import os
-        import sys
-        tmp_file = None
-        for i, arg in enumerate(sys.argv):
-            if arg.startswith('--file='):
-                tmp_file = arg[len('--file='):]
-                sys.argv.pop(i)
-                break
-        selected_files = sys.argv[1:]
-        if tmp_file is None:
-            for selected_file in selected_files:
-                print(os.path.abspath(selected_file))
-        else:
-            with open(tmp_file, 'w') as f:
-                for selected_file in selected_files:
-                    f.write(os.path.abspath(selected_file) + '\n')
-    """)
-    args += files.split(' ')
-    if output_type == "a temporary file":
-        args += ['--file={}']
-    fileselect_cmd = json.dumps([cmd, *args])
-    quteproc.set_setting('fileselect.handler', 'external')
-    quteproc.set_setting(f'fileselect.{kind}.command', fileselect_cmd)