chore(repo): baseline du fork input-remapper (upstream v2.2.1)
Le dépôt ne contenait que README.md ; ajout de l'intégralité du code fonctionnel du fork comme socle stable de la branche de release. L'état runtime d'orchestration (.ideai/) est exclu du suivi. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
0
install/__init__.py
Normal file
0
install/__init__.py
Normal file
116
install/__main__.py
Normal file
116
install/__main__.py
Normal file
@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- 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/>.
|
||||
|
||||
"""Build input-remapper including its translations and system-files.
|
||||
|
||||
pip, in many cases, fails to install data files, which need to go into system paths,
|
||||
and instead puts them (despite them being absolute paths) into
|
||||
/usr/lib/python3/inputremapper/usr/share/...
|
||||
|
||||
python3 setup.py install is deprecated
|
||||
|
||||
meson fails to install the module into a path that can actually be imported, and
|
||||
its python features require one to specify each individual file of the module.
|
||||
|
||||
So instead, input-remapper uses a custom python solution. Hopefulls this works well
|
||||
enough to prevent all ModuleNotFoundErrors in the future.
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import os
|
||||
import sys
|
||||
from enum import Enum
|
||||
|
||||
from install.check_dependencies import check_dependencies
|
||||
from install.data_files import build_data_files
|
||||
from install.module import build_input_remapper_module
|
||||
from install.language import make_lang
|
||||
|
||||
import argparse
|
||||
|
||||
|
||||
class Components(str, Enum):
|
||||
data_files = "data_files"
|
||||
python_module = "python_module"
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="Tool to install input-remapper with.")
|
||||
parser.add_argument(
|
||||
"--root",
|
||||
type=str,
|
||||
help=(
|
||||
"Where to install input-remapper to. For example ./build to prepare a "
|
||||
"package archive or for debugging, or / to install it to the system."
|
||||
),
|
||||
required=True,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--components",
|
||||
type=str, nargs='+',
|
||||
help=(
|
||||
f'A list of components to install. Default: --components '
|
||||
f'{Components.python_module.value} {Components.data_files.value}'
|
||||
),
|
||||
default=[Components.python_module, Components.data_files],
|
||||
metavar="COMPONENT"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def ask(msg) -> bool:
|
||||
answer = ""
|
||||
while answer not in ["y", "n"]:
|
||||
answer = input(f"{msg} [y/n] ").lower()
|
||||
return answer == "y"
|
||||
|
||||
|
||||
def print_headline(message: str) -> None:
|
||||
print(f"\033[7m{message}\033[0m")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
|
||||
if os.path.exists(args.root) and not args.root.startswith("/"):
|
||||
delete = ask(f"{args.root} already exists. Delete?")
|
||||
if delete:
|
||||
shutil.rmtree(args.root)
|
||||
else:
|
||||
sys.exit(3)
|
||||
|
||||
for component in args.components:
|
||||
if component not in [Components.data_files, Components.python_module]:
|
||||
raise ValueError(f"Unknown component {component}")
|
||||
|
||||
if Components.data_files in args.components:
|
||||
print_headline(f'Installing component "{Components.data_files.value}"')
|
||||
build_data_files(args.root)
|
||||
make_lang(args.root)
|
||||
|
||||
if Components.python_module in args.components:
|
||||
print_headline(f'Installing component "{Components.python_module.value}"')
|
||||
check_dependencies()
|
||||
build_input_remapper_module(args.root)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
43
install/check_dependencies.py
Normal file
43
install/check_dependencies.py
Normal file
@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- 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/>.
|
||||
|
||||
|
||||
def check_dependencies() -> None:
|
||||
print("Checking dependencies")
|
||||
try:
|
||||
import gi
|
||||
|
||||
gi.require_version("Gdk", "3.0")
|
||||
gi.require_version("GLib", "2.0")
|
||||
gi.require_version("Gst", "1.0")
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("GtkSource", "4")
|
||||
from gi.repository import GObject, Gtk, Gst, Gdk, GLib, Pango, Gio, GtkSource
|
||||
import evdev
|
||||
import psutil
|
||||
import dasbus
|
||||
import pygobject
|
||||
import pydantic
|
||||
|
||||
print("All required Python modules found")
|
||||
except ImportError as e:
|
||||
print(f"\033[93mMissing Python module: {e}\033[0m")
|
||||
except Exception as e:
|
||||
print(f"\033[93mException while checking dependencies: {e}\033[0m")
|
||||
66
install/data_files.py
Normal file
66
install/data_files.py
Normal file
@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- 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/>.
|
||||
|
||||
"""Copy input-remappers system-files around."""
|
||||
|
||||
import glob
|
||||
import os
|
||||
import shutil
|
||||
|
||||
|
||||
DATA_DIR = "usr/share/input-remapper"
|
||||
|
||||
|
||||
def get_data_files() -> list[tuple[str, list[str]]]:
|
||||
return [
|
||||
# see development.md#files
|
||||
(DATA_DIR, glob.glob("data/*")),
|
||||
("usr/share/applications/", ["data/input-remapper-gtk.desktop"]),
|
||||
(
|
||||
"usr/share/metainfo/",
|
||||
["data/io.github.sezanzeb.input_remapper.metainfo.xml"],
|
||||
),
|
||||
("usr/share/icons/hicolor/scalable/apps/", ["data/input-remapper.svg"]),
|
||||
("usr/share/polkit-1/actions/", ["data/input-remapper.policy"]),
|
||||
("usr/lib/systemd/system", ["data/input-remapper.service"]),
|
||||
# Fun fact: At some point during development and testing on arch, I ended up
|
||||
# with an empty inputremapper.Control.conf file, causing dbus to fail to start,
|
||||
# which rendered the whole operating system unusable.
|
||||
("usr/share/dbus-1/system.d/", ["data/inputremapper.Control.conf"]),
|
||||
("etc/xdg/autostart/", ["data/input-remapper-autoload.desktop"]),
|
||||
("usr/lib/udev/rules.d", ["data/69-input-remapper-forwarded.rules"]),
|
||||
("usr/lib/udev/rules.d", ["data/99-input-remapper.rules"]),
|
||||
("usr/bin/", ["bin/input-remapper-gtk"]),
|
||||
("usr/bin/", ["bin/input-remapper-service"]),
|
||||
("usr/bin/", ["bin/input-remapper-control"]),
|
||||
("usr/bin/", ["bin/input-remapper-reader-service"]),
|
||||
]
|
||||
|
||||
|
||||
def build_data_files(root: str) -> None:
|
||||
for target_dir, files in get_data_files():
|
||||
# We specify the root via argv instead. Argparse would ignore the first
|
||||
# arguments with a leading slash.
|
||||
assert not target_dir.startswith("/")
|
||||
for file_ in files:
|
||||
destination_dir = os.path.join(root, target_dir)
|
||||
print("Copying", file_, "to", destination_dir)
|
||||
os.makedirs(destination_dir, exist_ok=True)
|
||||
shutil.copy(file_, os.path.join(destination_dir, os.path.basename(file_)))
|
||||
54
install/language.py
Normal file
54
install/language.py
Normal file
@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- 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/>.
|
||||
|
||||
"""Build language files and copy them to the target."""
|
||||
|
||||
import glob
|
||||
import os
|
||||
import subprocess
|
||||
from os.path import basename, splitext, join, dirname
|
||||
|
||||
|
||||
def make_lang(root: str) -> None:
|
||||
"""Build po files as mo files into the expected directory."""
|
||||
os.makedirs("mo", exist_ok=True)
|
||||
for po_file in glob.glob("po/*.po"):
|
||||
lang = splitext(basename(po_file))[0]
|
||||
target = join(
|
||||
root,
|
||||
"usr",
|
||||
"share",
|
||||
"input-remapper",
|
||||
"lang",
|
||||
lang,
|
||||
"LC_MESSAGES",
|
||||
"input-remapper.mo",
|
||||
)
|
||||
os.makedirs(dirname(target), exist_ok=True)
|
||||
print(f"Generating translation {target}")
|
||||
subprocess.run(
|
||||
[
|
||||
"msgfmt",
|
||||
"-o",
|
||||
target,
|
||||
str(po_file),
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
187
install/module.py
Normal file
187
install/module.py
Normal file
@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- 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/>.
|
||||
|
||||
"""Figure out where the python module needs to be placed in order to reliable import it.
|
||||
|
||||
Dealing with varying sys.paths is frustrating. The sys.paths in /usr are not mirrored
|
||||
in /usr/local consistently. Meson uses /usr/local/python3, which ubuntus python3 does
|
||||
not import from. /usr/local is ignored by python within udev. Arch does not import
|
||||
from /usr/local. When in doubt, do not install into /usr/local. I do not want to deal
|
||||
with ModuleNotFoundErrors. I don't care if it should be in /usr/local by convention.
|
||||
I don't know how much this varies across ubuntu versions and other debian based
|
||||
distributions. I want the .deb to install reliably.
|
||||
|
||||
sys.path samples:
|
||||
endeavouros user: ['', '/usr/lib/python313.zip', '/usr/lib/python3.13', '/usr/lib/python3.13/lib-dynload', '/usr/lib/python3.13/site-packages']
|
||||
endeavouros root: ['', '/usr/lib/python313.zip', '/usr/lib/python3.13', '/usr/lib/python3.13/lib-dynload', '/usr/lib/python3.13/site-packages']
|
||||
endeavouros udev: ['/usr/bin', '/lib/python313.zip', '/lib/python3.13', '/lib/python3.13/lib-dynload', '/lib/python3.13/site-packages']
|
||||
ubuntu 25.04 user: ['', '/usr/lib/python313.zip', '/usr/lib/python3.13', '/usr/lib/python3.13/lib-dynload', '/home/user/.local/lib/python3.13/site-packages', '/usr/local/lib/python3.13/dist-packages', '/usr/lib/python3/dist-packages']
|
||||
ubuntu 25.04 root: ['', '/usr/lib/python313.zip', '/usr/lib/python3.13', '/usr/lib/python3.13/lib-dynload', '/usr/local/lib/python3.13/dist-packages', '/usr/lib/python3/dist-packages']
|
||||
ubuntu 25.04 udev: ['/usr/bin', '/lib/python313.zip', '/lib/python3.13', '/lib/python3.13/lib-dynload', '/lib/python3/dist-packages']
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
import shutil
|
||||
import re
|
||||
import tomllib
|
||||
|
||||
from install.data_files import DATA_DIR
|
||||
|
||||
|
||||
def _key(path) -> int:
|
||||
favorability = 0
|
||||
|
||||
# Good
|
||||
if re.match(r".*/lib/python3/.+-packages", path):
|
||||
# Independent of the installed python version, so it probably just continues
|
||||
# to be importable after a python version upgrade.
|
||||
favorability = 5
|
||||
elif re.match(r".*/lib/python3.+?/.+-packages", path):
|
||||
favorability = 4
|
||||
|
||||
# Not meant for python code, but it probably works flawlessly
|
||||
elif re.match(r".*/lib/python3.+?/lib-dynload", path):
|
||||
favorability = 3
|
||||
|
||||
# pip refuses to uninstall from standard-library directories once installed there
|
||||
elif re.match(r".*/lib/python3", path):
|
||||
favorability = 2
|
||||
elif re.match(r".*/lib/python3.+?", path):
|
||||
favorability = 1
|
||||
|
||||
# Check the prefix
|
||||
# (beware, udev imports from /lib, which I think is equivalent to /usr/lib, so
|
||||
# don't require /usr as a prefix)
|
||||
if path.startswith("/usr/local"):
|
||||
# udev does not import from /usr/local
|
||||
favorability -= 5
|
||||
elif path.startswith("/home"):
|
||||
# Something like /home/user/.local/lib/python3.13/site-packages
|
||||
# The python package needs to be installed system-wide
|
||||
favorability -= 50
|
||||
|
||||
if not os.path.exists(path):
|
||||
# If it doesn't exist yet, pip will apparently create it
|
||||
favorability -= 2
|
||||
elif not os.path.isdir(path):
|
||||
# If it exists but is not a dir, do not use it
|
||||
favorability -= 99
|
||||
|
||||
return -favorability
|
||||
|
||||
|
||||
def _get_packages_dir() -> str:
|
||||
"""Where to install the input-remapper module to.
|
||||
|
||||
For example "/usr/lib/python3.13
|
||||
"""
|
||||
packages_dirs = sorted(sys.path, key=_key)
|
||||
packages_dir = packages_dirs[0]
|
||||
print(f'Picked "{packages_dir}" from {packages_dirs}')
|
||||
return packages_dir
|
||||
|
||||
|
||||
def _get_commit_hash() -> str:
|
||||
git_call = subprocess.check_output(["git", "rev-parse", "HEAD"])
|
||||
commit = git_call.decode().strip()
|
||||
return commit
|
||||
|
||||
|
||||
def _set_variables(target: str) -> None:
|
||||
path = os.path.join(target, "inputremapper", "installation_info.py")
|
||||
assert os.path.exists(path)
|
||||
|
||||
with open(path, "r") as f:
|
||||
contents = f.read()
|
||||
|
||||
with open("pyproject.toml", "rb") as f:
|
||||
version = tomllib.load(f)["project"]["version"]
|
||||
|
||||
values = {
|
||||
"COMMIT_HASH": _get_commit_hash(),
|
||||
"VERSION": version,
|
||||
"DATA_DIR": f"/{DATA_DIR}",
|
||||
}
|
||||
|
||||
print("Setting", values, "in", path)
|
||||
|
||||
with open(path, "w") as f:
|
||||
for variable_name, value in values.items():
|
||||
contents = re.sub(
|
||||
rf"{variable_name}\s*=.+",
|
||||
f"{variable_name} = '{value}'",
|
||||
contents,
|
||||
)
|
||||
|
||||
f.write(contents)
|
||||
|
||||
|
||||
def build_input_remapper_module(root: str) -> None:
|
||||
# I'd use --prefix and --root, but
|
||||
# `pip install . --root ./build --prefix usr`
|
||||
# makes it end up in ./build/usr/local
|
||||
|
||||
# if root is not /, then input-remapper is built into some other directory for the
|
||||
# purpose of merging it into / later. So regardless of root, we use the same
|
||||
# package_dir.
|
||||
package_dir = _get_packages_dir()
|
||||
if package_dir.startswith("/"):
|
||||
package_dir = package_dir[1:]
|
||||
|
||||
target = os.path.join(root, package_dir)
|
||||
|
||||
# Make sure we don't install input-remapper twice on that system into two different
|
||||
# paths, which commonly causes unexpected behavior. Also, we need to specifically
|
||||
# tell pip to replace an existing installation, or uninstall it beforehand.
|
||||
if root == "/":
|
||||
print("Uninstalling existing input-remapper installation")
|
||||
os.system("pip uninstall input-remapper --break-system-packages -y")
|
||||
# If root is not "/", it is probably part of a packaging script for a distro,
|
||||
# which will take care of uninstalling the old python-package for us.
|
||||
|
||||
command = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
".",
|
||||
"--target",
|
||||
target,
|
||||
"--no-deps",
|
||||
]
|
||||
|
||||
print("Running", " ".join(command))
|
||||
|
||||
# Fix the stdout ordering in github workflows
|
||||
sys.stdout.flush()
|
||||
|
||||
subprocess.check_call(command)
|
||||
|
||||
# pip puts its own leftovers into ./build that we don't need.
|
||||
# This only happens, when root is set to "build".
|
||||
if "build" in root:
|
||||
if os.path.exists("./build/lib/"):
|
||||
shutil.rmtree("./build/lib/")
|
||||
if os.path.exists("./build/bdist.linux-x86_64/"):
|
||||
shutil.rmtree("./build/bdist.linux-x86_64/")
|
||||
|
||||
_set_variables(target)
|
||||
77
install/uninstall.py
Normal file
77
install/uninstall.py
Normal file
@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- 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/>.
|
||||
|
||||
"""Remove a system-wide input-remapper installation."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from install.data_files import get_data_files
|
||||
|
||||
|
||||
def uninstall() -> None:
|
||||
# remove data files
|
||||
for directory, files in get_data_files():
|
||||
for file_ in files:
|
||||
filename = os.path.basename(file_)
|
||||
path = os.path.join("/", directory, filename)
|
||||
|
||||
# Removing files from system directories is risky. Low propability, very
|
||||
# high damage. To avoid accidentally removing system directories due to
|
||||
# a bug, assert that this is an input-remapper path.
|
||||
# If this happens, urgently create a new issue on github!
|
||||
assert "input" in path and "remapper" in path
|
||||
|
||||
try:
|
||||
os.unlink(path)
|
||||
print("Removed", path)
|
||||
except FileNotFoundError:
|
||||
print(path, "not found")
|
||||
|
||||
# language files are not in data_files
|
||||
data_path = "/usr/share/input-remapper/"
|
||||
try:
|
||||
shutil.rmtree(data_path)
|
||||
print("Removed", data_path)
|
||||
except FileNotFoundError:
|
||||
print(data_path, "not found")
|
||||
|
||||
# remove pip module
|
||||
command = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"pip",
|
||||
"uninstall",
|
||||
"input-remapper",
|
||||
"--break-system-packages",
|
||||
]
|
||||
print("Running", " ".join(command))
|
||||
# Fix the stdout ordering in github workflows
|
||||
sys.stdout.flush()
|
||||
subprocess.check_call(command)
|
||||
|
||||
os.system("sudo systemctl stop input-remapper")
|
||||
os.system("sudo systemctl daemon-reload")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uninstall()
|
||||
Reference in New Issue
Block a user