summaryrefslogtreecommitdiff
path: root/qutebrowser/completion/models/filepathcategory.py
blob: f9b6608a3bab8b020e4d636f957057d1ab020294 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:

# Copyright 2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser.  If not, see <https://www.gnu.org/licenses/>.

"""Completion category for filesystem paths.

NOTE: This module deliberatly uses os.path rather than pathlib, because of how
it interacts with the completion, which operates on strings. For example, we
need to be able to tell the difference between "~/input" and "~/input/". Also,
if we get "~/input", we want to glob "~/input*" rather than "~/input/*" which
is harder to achieve via pathlib.
"""

import glob
import os
import os.path
from typing import List, Optional, Iterable

from PyQt5.QtCore import QAbstractListModel, QModelIndex, QObject, Qt, QUrl

from qutebrowser.config import config
from qutebrowser.utils import log


class FilePathCategory(QAbstractListModel):
    """Represent filesystem paths matching a pattern."""

    def __init__(self, name: str, parent: QObject = None) -> None:
        super().__init__(parent)
        self._paths: List[str] = []
        self.name = name
        self.columns_to_filter = [0]

    def _contract_user(self, val: str, path: str) -> str:
        """Contract ~/... and ~user/... in results.

        Arguments:
            val: The user's partially typed path.
            path: The found path based on the input.
        """
        if not val.startswith('~'):
            return path
        head = val.split(os.sep)[0]
        return path.replace(os.path.expanduser(head), head, 1)

    def _glob(self, val: str) -> Iterable[str]:
        """Find paths based on the given pattern."""
        if not os.path.isabs(val):
            return []
        try:
            return glob.glob(glob.escape(val) + '*')
        except ValueError as e:  # pragma: no cover
            # e.g. "embedded null byte" with \x00 on Python 3.6 and 3.7
            log.completion.debug(f"Failed to glob: {e}")
            return []

    def _url_to_path(self, val: str) -> str:
        """Get a path from a file:/// URL."""
        url = QUrl(val)
        assert url.isValid(), url
        assert url.scheme() == 'file', url
        return url.toLocalFile()

    def set_pattern(self, val: str) -> None:
        """Compute list of suggested paths (called from `CompletionModel`).

        Args:
            val: The user's partially typed URL/path.
        """
        if not val:
            self._paths = config.val.completion.favorite_paths or []
        elif val.startswith('file:///'):
            url_path = self._url_to_path(val)
            self._paths = sorted(
                QUrl.fromLocalFile(path).toString()
                for path in self._glob(url_path)
            )
        else:
            try:
                expanded = os.path.expanduser(val)
            except UnicodeEncodeError:
                # os.path.expanduser('~\ud800') can raise UnicodeEncodeError
                # via pwd.getpwnam
                expanded = val
            paths = self._glob(expanded)
            self._paths = sorted(self._contract_user(val, path) for path in paths)

    def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Optional[str]:
        """Implement abstract method in QAbstractListModel."""
        if role == Qt.DisplayRole and index.column() == 0:
            return self._paths[index.row()]
        return None

    def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
        """Implement abstract method in QAbstractListModel."""
        if parent.isValid():
            return 0
        return len(self._paths)