File: paths.py

package info (click to toggle)
input-remapper 2.2.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 2,876 kB
  • sloc: python: 27,262; sh: 191; xml: 33; makefile: 3
file content (156 lines) | stat: -rw-r--r-- 5,191 bytes parent folder | download | duplicates (2)
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# -*- coding: utf-8 -*-
# input-remapper - GUI for device specific keyboard mappings
# Copyright (C) 2025 sezanzeb <b8x45ygc9@mozmail.com>
#
# This file is part of input-remapper.
#
# input-remapper 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.
#
# input-remapper 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 input-remapper.  If not, see <https://www.gnu.org/licenses/>.

# TODO: convert everything to use pathlib.Path

"""Path constants to be used."""


import os
import shutil
from typing import List, Union, Optional

from inputremapper.logging.logger import logger
from inputremapper.user import UserUtils


# TODO maybe this could become, idk, ConfigService and PresetService
class PathUtils:
    rel_path = ".config/input-remapper-2"

    @staticmethod
    def config_path() -> str:
        # TODO when proper DI is being done, construct PathUtils and configure it in
        #  the constructor. Then there is no need to recompute the config_path
        #  each time. Tests might have overwritten UserUtils.home.
        return os.path.join(UserUtils.home, PathUtils.rel_path)

    @staticmethod
    def chown(path):
        """Set the owner of a path to the user."""
        try:
            logger.debug('Chown "%s", "%s"', path, UserUtils.user)
            shutil.chown(path, user=UserUtils.user, group=UserUtils.user)
        except LookupError:
            # the users group was unknown in one case for whatever reason
            shutil.chown(path, user=UserUtils.user)

    @staticmethod
    def touch(path: Union[str, os.PathLike], log=True):
        """Create an empty file and all its parent dirs, give it to the user."""
        if str(path).endswith("/"):
            raise ValueError(f"Expected path to not end with a slash: {path}")

        if os.path.exists(path):
            return

        if log:
            logger.info('Creating file "%s"', path)

        PathUtils.mkdir(os.path.dirname(path), log=False)

        os.mknod(path)
        PathUtils.chown(path)

    @staticmethod
    def mkdir(path, log=True):
        """Create a folder, give it to the user."""
        if path == "" or path is None:
            return

        if os.path.exists(path):
            return

        if log:
            logger.info('Creating dir "%s"', path)

        # give all newly created folders to the user.
        # e.g. if .config/input-remapper/mouse/ is created the latter two
        base = os.path.split(path)[0]
        PathUtils.mkdir(base, log=False)

        os.makedirs(path)
        PathUtils.chown(path)

    @staticmethod
    def split_all(path: Union[os.PathLike, str]) -> List[str]:
        """Split the path into its segments."""
        parts = []
        while True:
            path, tail = os.path.split(path)
            parts.append(tail)
            if path == os.path.sep:
                # we arrived at the root '/'
                parts.append(path)
                break
            if not path:
                # arrived at start of relative path
                break

        parts.reverse()
        return parts

    @staticmethod
    def remove(path):
        """Remove whatever is at the path."""
        if not os.path.exists(path):
            return

        if os.path.isdir(path):
            shutil.rmtree(path)
        else:
            os.remove(path)

    @staticmethod
    def sanitize_path_component(group_name: str) -> str:
        """Replace characters listed in
        https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
        with an underscore.
        """
        for character in '/\\?%*:|"<>':
            if character in group_name:
                group_name = group_name.replace(character, "_")
        return group_name

    @staticmethod
    def get_preset_path(group_name: Optional[str] = None, preset: Optional[str] = None):
        """Get a path to the stored preset, or to store a preset to."""
        presets_base = os.path.join(PathUtils.config_path(), "presets")

        if group_name is None:
            return presets_base

        group_name = PathUtils.sanitize_path_component(group_name)

        if preset is not None:
            # the extension of the preset should not be shown in the ui.
            # if a .json extension arrives this place, it has not been
            # stripped away properly prior to this.
            if not preset.endswith(".json"):
                preset = f"{preset}.json"

        if preset is None:
            return os.path.join(presets_base, group_name)

        return os.path.join(presets_base, group_name, preset)

    @staticmethod
    def get_config_path(*paths) -> str:
        """Get a path in ~/.config/input-remapper/."""
        return os.path.join(PathUtils.config_path(), *paths)