umu-commander 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- umu_commander/Types/__init__.py +18 -0
- umu_commander/__init__.py +1 -0
- umu_commander/__main__.py +205 -0
- umu_commander/configuration.py +94 -0
- umu_commander/database.py +66 -0
- umu_commander/proton.py +65 -0
- umu_commander/tracking.py +125 -0
- umu_commander/umu_config.py +203 -0
- umu_commander/util.py +54 -0
- umu_commander-2.0.0.dist-info/METADATA +65 -0
- umu_commander-2.0.0.dist-info/RECORD +14 -0
- umu_commander-2.0.0.dist-info/WHEEL +4 -0
- umu_commander-2.0.0.dist-info/entry_points.txt +2 -0
- umu_commander-2.0.0.dist-info/licenses/LICENSE.txt +21 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from enum import IntEnum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Element(str):
|
|
5
|
+
def __new__(cls, value: str, name: str = None):
|
|
6
|
+
instance = super().__new__(cls, value)
|
|
7
|
+
if name is None:
|
|
8
|
+
instance.name = value
|
|
9
|
+
else:
|
|
10
|
+
instance.name = name
|
|
11
|
+
|
|
12
|
+
return instance
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ExitCode(IntEnum):
|
|
16
|
+
SUCCESS = 0
|
|
17
|
+
DECODING_ERROR = 1
|
|
18
|
+
INVALID_SELECTION = 2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
VERSION = "v2.0.0"
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
from argparse import ArgumentParser, Namespace
|
|
4
|
+
from json import JSONDecodeError
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from InquirerPy.exceptions import InvalidArgument
|
|
8
|
+
|
|
9
|
+
from umu_commander import VERSION
|
|
10
|
+
from umu_commander import configuration as config
|
|
11
|
+
from umu_commander import database as db
|
|
12
|
+
from umu_commander import tracking, umu_config
|
|
13
|
+
from umu_commander.configuration import CONFIG_DIR, CONFIG_NAME, DEFAULT_UMU_CONFIG_NAME
|
|
14
|
+
from umu_commander.Types import ExitCode
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def init() -> ExitCode:
|
|
18
|
+
try:
|
|
19
|
+
config.load()
|
|
20
|
+
|
|
21
|
+
except (JSONDecodeError, KeyError):
|
|
22
|
+
config_path: Path = CONFIG_DIR / CONFIG_NAME
|
|
23
|
+
config_path_old: Path = CONFIG_DIR / (str(CONFIG_NAME) + ".old")
|
|
24
|
+
|
|
25
|
+
print(f"Config file at {config_path} could not be read.")
|
|
26
|
+
|
|
27
|
+
if not config_path_old.exists():
|
|
28
|
+
print(f"Config file renamed to {config_path_old}.")
|
|
29
|
+
config_path.rename(config_path_old)
|
|
30
|
+
|
|
31
|
+
return ExitCode.DECODING_ERROR
|
|
32
|
+
|
|
33
|
+
except FileNotFoundError:
|
|
34
|
+
config.dump()
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
db.load()
|
|
38
|
+
|
|
39
|
+
except JSONDecodeError:
|
|
40
|
+
db_path: Path = config.DB_DIR / config.DB_NAME
|
|
41
|
+
db_path_old: Path = config.DB_DIR / (str(config.DB_NAME) + ".old")
|
|
42
|
+
|
|
43
|
+
print(f"Tracking file at {db_path} could not be read.")
|
|
44
|
+
|
|
45
|
+
if not db_path_old.exists():
|
|
46
|
+
db_path.rename(db_path_old)
|
|
47
|
+
print(f"DB file renamed to {db_path_old}.")
|
|
48
|
+
|
|
49
|
+
except FileNotFoundError:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
return ExitCode.SUCCESS
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_parser_results() -> tuple[ArgumentParser, Namespace]:
|
|
56
|
+
parser = argparse.ArgumentParser(
|
|
57
|
+
prog=f"umu-commander",
|
|
58
|
+
description="Interactive CLI tool to augment umu-launcher",
|
|
59
|
+
epilog="For details, usage, and more, see the README.md file, or visit https://github.com/Mpaxlamitsounas/umu-commander.",
|
|
60
|
+
add_help=True,
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"-v",
|
|
64
|
+
"--version",
|
|
65
|
+
action="version",
|
|
66
|
+
version=f"%(prog)s {VERSION}",
|
|
67
|
+
help="Prints this menu.",
|
|
68
|
+
)
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"-q",
|
|
71
|
+
"--quiet",
|
|
72
|
+
action="store_true",
|
|
73
|
+
help="Suppresses all non interactive output.",
|
|
74
|
+
)
|
|
75
|
+
parser.add_argument(
|
|
76
|
+
"verb",
|
|
77
|
+
help="Selects program functionality.",
|
|
78
|
+
choices=["track", "untrack", "users", "delete", "create", "run", "fix"],
|
|
79
|
+
)
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"-i",
|
|
82
|
+
"--input",
|
|
83
|
+
help=f'Path to file or directory that will be acted upon. Default: $PWD for directories, "{DEFAULT_UMU_CONFIG_NAME}" for files',
|
|
84
|
+
type=Path,
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"-pr",
|
|
88
|
+
"--proton",
|
|
89
|
+
help="Path to Proton version that will be used. Default: Latest umu proton",
|
|
90
|
+
type=Path,
|
|
91
|
+
)
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
"-wp",
|
|
94
|
+
"--wine-prefix",
|
|
95
|
+
help='Sets the WINE prefix path in config creation. Default: directory/"prefix"',
|
|
96
|
+
type=Path,
|
|
97
|
+
dest="prefix",
|
|
98
|
+
)
|
|
99
|
+
parser.add_argument(
|
|
100
|
+
"-dll",
|
|
101
|
+
"--dll-overrides",
|
|
102
|
+
help="Sets the WINE DLL override string in config creation. Default: No overrides",
|
|
103
|
+
)
|
|
104
|
+
parser.add_argument(
|
|
105
|
+
"-l",
|
|
106
|
+
"--lang",
|
|
107
|
+
help="Sets the language locale override in config creation. Default: No override",
|
|
108
|
+
)
|
|
109
|
+
parser.add_argument(
|
|
110
|
+
"-a",
|
|
111
|
+
"--launch_args",
|
|
112
|
+
help="Sets launch arguments in config creation. Default: No arguments",
|
|
113
|
+
)
|
|
114
|
+
parser.add_argument(
|
|
115
|
+
"-o",
|
|
116
|
+
"--output",
|
|
117
|
+
help=f"Sets output config filename in config creation. Default directory/{DEFAULT_UMU_CONFIG_NAME}",
|
|
118
|
+
type=Path,
|
|
119
|
+
)
|
|
120
|
+
parser.add_argument(
|
|
121
|
+
"-ni",
|
|
122
|
+
"--no-interactive",
|
|
123
|
+
help="Uses defaults for missing values instead of prompting.",
|
|
124
|
+
action="store_false",
|
|
125
|
+
dest="interactive",
|
|
126
|
+
)
|
|
127
|
+
parser.add_argument(
|
|
128
|
+
"-nr",
|
|
129
|
+
"--no-refresh",
|
|
130
|
+
"-nu",
|
|
131
|
+
"--no-update",
|
|
132
|
+
help="Do not check for new Proton versions while running.",
|
|
133
|
+
action="store_false",
|
|
134
|
+
dest="refresh",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
args = parser.parse_args()
|
|
138
|
+
|
|
139
|
+
return parser, args
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def main() -> int:
|
|
143
|
+
if (return_code := init()) != ExitCode.SUCCESS:
|
|
144
|
+
return return_code.value
|
|
145
|
+
|
|
146
|
+
parser, args = get_parser_results()
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
match args.verb:
|
|
150
|
+
case "track":
|
|
151
|
+
tracking.track(
|
|
152
|
+
args.proton,
|
|
153
|
+
args.input,
|
|
154
|
+
refresh_versions=args.refresh,
|
|
155
|
+
quiet=args.quiet,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
case "untrack":
|
|
159
|
+
tracking.untrack(args.input, quiet=args.quiet)
|
|
160
|
+
|
|
161
|
+
case "users":
|
|
162
|
+
tracking.users(args.proton)
|
|
163
|
+
|
|
164
|
+
case "delete":
|
|
165
|
+
tracking.delete()
|
|
166
|
+
|
|
167
|
+
case "create":
|
|
168
|
+
umu_config.create(
|
|
169
|
+
args.prefix,
|
|
170
|
+
args.proton,
|
|
171
|
+
args.dll_overrides,
|
|
172
|
+
args.lang,
|
|
173
|
+
args.launch_args,
|
|
174
|
+
args.input,
|
|
175
|
+
args.output,
|
|
176
|
+
interactive=args.interactive,
|
|
177
|
+
refresh_versions=args.refresh,
|
|
178
|
+
quiet=args.quiet,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
case "run":
|
|
182
|
+
umu_config.run(args.input)
|
|
183
|
+
|
|
184
|
+
case "fix":
|
|
185
|
+
umu_config.fix(args.input)
|
|
186
|
+
|
|
187
|
+
case _:
|
|
188
|
+
print("Unrecognised verb.")
|
|
189
|
+
parser.print_help()
|
|
190
|
+
return ExitCode.INVALID_SELECTION.value
|
|
191
|
+
|
|
192
|
+
except InvalidArgument:
|
|
193
|
+
print("No choices to select from.")
|
|
194
|
+
return ExitCode.INVALID_SELECTION.value
|
|
195
|
+
|
|
196
|
+
else:
|
|
197
|
+
return ExitCode.SUCCESS.value
|
|
198
|
+
|
|
199
|
+
finally:
|
|
200
|
+
tracking.untrack_unlinked()
|
|
201
|
+
db.dump()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
if __name__ == "__main__":
|
|
205
|
+
exit(main())
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import os
|
|
3
|
+
import tomllib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import tomli_w
|
|
8
|
+
|
|
9
|
+
from umu_commander.Types import Element
|
|
10
|
+
|
|
11
|
+
CONFIG_DIR: Path = Path.home() / ".config"
|
|
12
|
+
CONFIG_NAME: Path = Path("umu-commander.toml")
|
|
13
|
+
|
|
14
|
+
PROTON_PATHS: tuple[Path, ...] = (
|
|
15
|
+
Path.home() / ".local/share/Steam/compatibilitytools.d",
|
|
16
|
+
Path.home() / ".local/share/umu/compatibilitytools",
|
|
17
|
+
Path.home()
|
|
18
|
+
/ ".var/app/com.valvesoftware.Steam/.local/share/Steam/compatibilitytools.d",
|
|
19
|
+
)
|
|
20
|
+
UMU_PROTON_PATH: Path = Path(Path.home() / ".local/share/Steam/compatibilitytools.d")
|
|
21
|
+
|
|
22
|
+
DB_NAME: Path = Path("tracking.json")
|
|
23
|
+
DB_DIR: Path = Path.home() / ".local/share/umu/compatibilitytools"
|
|
24
|
+
DEFAULT_UMU_CONFIG_NAME: Path = Path("umu-config.toml")
|
|
25
|
+
DEFAULT_PREFIX_DIR: Path = Path.home() / ".local/share/wineprefixes/"
|
|
26
|
+
DLL_OVERRIDES_OPTIONS: tuple[Element, ...] = (
|
|
27
|
+
Element("winhttp.dll=n,b;", "winhttp for BepInEx"),
|
|
28
|
+
)
|
|
29
|
+
LANG_OVERRIDES_OPTIONS: tuple[Element, ...] = (Element("ja_JP.UTF8", "Japanese"),)
|
|
30
|
+
|
|
31
|
+
module = importlib.import_module(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load():
|
|
35
|
+
with open(os.path.join(CONFIG_DIR, CONFIG_NAME), "rb") as conf_file:
|
|
36
|
+
toml_conf = tomllib.load(conf_file)
|
|
37
|
+
|
|
38
|
+
# Proton dirs translation
|
|
39
|
+
setattr(
|
|
40
|
+
module,
|
|
41
|
+
"PROTON_PATHS",
|
|
42
|
+
(Path(proton_dir) for proton_dir in toml_conf["PROTON_PATHS"]),
|
|
43
|
+
)
|
|
44
|
+
del toml_conf["PROTON_PATHS"]
|
|
45
|
+
|
|
46
|
+
# DLL/LANG Override translation
|
|
47
|
+
for key in ["DLL", "LANG"]:
|
|
48
|
+
setattr(
|
|
49
|
+
module,
|
|
50
|
+
f"{key}_OVERRIDES_OPTIONS",
|
|
51
|
+
(
|
|
52
|
+
Element(value, name)
|
|
53
|
+
for name, value in toml_conf[f"{key}_OVERRIDES_OPTIONS"].items()
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
del toml_conf[f"{key}_OVERRIDES_OPTIONS"]
|
|
57
|
+
|
|
58
|
+
for key, value in toml_conf.items():
|
|
59
|
+
setattr(module, key, Path(value))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _get_attributes() -> dict[str, Any]:
|
|
63
|
+
attributes: dict[str, Any] = {}
|
|
64
|
+
for key in dir(module):
|
|
65
|
+
value = getattr(module, key)
|
|
66
|
+
if not key.startswith("_") and not callable(value) and key.upper() == key:
|
|
67
|
+
attributes[key] = value
|
|
68
|
+
|
|
69
|
+
return attributes
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def dump():
|
|
73
|
+
if not os.path.exists(CONFIG_DIR):
|
|
74
|
+
os.mkdir(CONFIG_DIR)
|
|
75
|
+
|
|
76
|
+
with open(os.path.join(CONFIG_DIR, CONFIG_NAME), "wb") as conf_file:
|
|
77
|
+
toml_conf = _get_attributes()
|
|
78
|
+
del toml_conf["CONFIG_DIR"]
|
|
79
|
+
del toml_conf["CONFIG_NAME"]
|
|
80
|
+
|
|
81
|
+
for key, value in toml_conf.items():
|
|
82
|
+
match key:
|
|
83
|
+
case "PROTON_PATHS":
|
|
84
|
+
toml_conf[key] = [str(proton_dir) for proton_dir in PROTON_PATHS]
|
|
85
|
+
|
|
86
|
+
case "DLL_OVERRIDES_OPTIONS" | "LANG_OVERRIDES_OPTIONS":
|
|
87
|
+
toml_conf[key] = {
|
|
88
|
+
override.name: override for override in getattr(module, key)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
case _:
|
|
92
|
+
toml_conf[key] = str(value)
|
|
93
|
+
|
|
94
|
+
tomli_w.dump(toml_conf, conf_file)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import umu_commander.configuration as config
|
|
6
|
+
|
|
7
|
+
_db: defaultdict[Path, defaultdict[Path, list[Path]]] = defaultdict(
|
|
8
|
+
lambda: defaultdict(list)
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def load():
|
|
13
|
+
global _db
|
|
14
|
+
|
|
15
|
+
if not config.DB_DIR.exists():
|
|
16
|
+
config.DB_DIR.mkdir()
|
|
17
|
+
|
|
18
|
+
with open(config.DB_DIR / config.DB_NAME, "rt") as db_file:
|
|
19
|
+
db: dict[Path, dict[Path, list[Path]]] = {}
|
|
20
|
+
for proton_dir, proton_vers in json.load(db_file).items():
|
|
21
|
+
proton_dir = Path(proton_dir)
|
|
22
|
+
db[proton_dir] = {}
|
|
23
|
+
for proton_ver, proton_users in proton_vers.items():
|
|
24
|
+
proton_ver = proton_dir / proton_ver
|
|
25
|
+
db[proton_dir][proton_ver] = [Path(user) for user in proton_users]
|
|
26
|
+
# noinspection PyTypeChecker
|
|
27
|
+
_db.update(db)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def dump():
|
|
31
|
+
if not config.DB_DIR.exists():
|
|
32
|
+
config.DB_DIR.mkdir()
|
|
33
|
+
|
|
34
|
+
db: dict[str, dict[str, list[str]]] = {}
|
|
35
|
+
for proton_dir, proton_vers in _db.items():
|
|
36
|
+
proton_dir = str(proton_dir)
|
|
37
|
+
db[proton_dir] = {}
|
|
38
|
+
for proton_ver, proton_users in proton_vers.items():
|
|
39
|
+
proton_ver = proton_ver.name
|
|
40
|
+
db[proton_dir][proton_ver] = [str(user) for user in proton_users]
|
|
41
|
+
|
|
42
|
+
with open(config.DB_DIR / config.DB_NAME, "wt") as db_file:
|
|
43
|
+
# noinspection PyTypeChecker
|
|
44
|
+
json.dump(db, db_file, indent="\t")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get(
|
|
48
|
+
proton_dir: Path = None, proton_ver: Path = None
|
|
49
|
+
) -> dict[Path, dict[Path, list[Path]]] | dict[Path, list[Path]] | list[Path]:
|
|
50
|
+
global _db
|
|
51
|
+
|
|
52
|
+
if proton_dir is None and proton_ver is None:
|
|
53
|
+
return _db
|
|
54
|
+
|
|
55
|
+
if proton_ver is None:
|
|
56
|
+
return _db[proton_dir]
|
|
57
|
+
|
|
58
|
+
if proton_ver not in _db[proton_dir]:
|
|
59
|
+
_db[proton_dir][proton_ver] = []
|
|
60
|
+
|
|
61
|
+
return _db[proton_dir][proton_ver]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _reset():
|
|
65
|
+
global _db
|
|
66
|
+
_db = defaultdict(lambda: defaultdict(list))
|
umu_commander/proton.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import subprocess
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import umu_commander.configuration as config
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _natural_sort_proton_ver_key(p: Path, _nsre=re.compile(r"(\d+)")):
|
|
10
|
+
s: str = p.name
|
|
11
|
+
return [int(text) if text.isdigit() else text for text in _nsre.split(s)]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def refresh_proton_versions():
|
|
15
|
+
print("Updating umu Proton.")
|
|
16
|
+
umu_update_process = subprocess.run(
|
|
17
|
+
["umu-run", '""'],
|
|
18
|
+
env={"PROTONPATH": "UMU-Latest", "UMU_LOG": "debug"},
|
|
19
|
+
capture_output=True,
|
|
20
|
+
text=True,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
for line in umu_update_process.stderr.split("\n"):
|
|
24
|
+
if "PROTONPATH" in line and "/" in line:
|
|
25
|
+
try:
|
|
26
|
+
left: int = line.rfind("/") + 1
|
|
27
|
+
print(f"Latest UMU-Proton: {line[left:len(line) - 1]}.")
|
|
28
|
+
except ValueError:
|
|
29
|
+
print("Could not fetch latest UMU-Proton.")
|
|
30
|
+
|
|
31
|
+
break
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def collect_proton_versions(sort: bool = False) -> dict[Path, Iterable[Path]]:
|
|
35
|
+
versions: dict[Path, Iterable[Path]] = {}
|
|
36
|
+
for proton_dir in config.PROTON_PATHS:
|
|
37
|
+
dir_versions = [version for version in proton_dir.iterdir() if version.is_dir()]
|
|
38
|
+
if len(dir_versions) == 0:
|
|
39
|
+
continue
|
|
40
|
+
else:
|
|
41
|
+
versions[proton_dir] = dir_versions
|
|
42
|
+
|
|
43
|
+
if sort:
|
|
44
|
+
versions[proton_dir] = sorted(
|
|
45
|
+
versions[proton_dir], key=_natural_sort_proton_ver_key, reverse=True
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return versions
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_latest_umu_proton() -> Path | None:
|
|
52
|
+
umu_proton_versions: list[Path] = [
|
|
53
|
+
config.UMU_PROTON_PATH / proton_ver
|
|
54
|
+
for proton_ver in config.UMU_PROTON_PATH.iterdir()
|
|
55
|
+
if "UMU" in proton_ver.name and (config.UMU_PROTON_PATH / proton_ver).is_dir()
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
if len(umu_proton_versions) == 0:
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
umu_proton_versions = sorted(
|
|
62
|
+
umu_proton_versions, key=_natural_sort_proton_ver_key, reverse=True
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return umu_proton_versions[0]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from InquirerPy import inquirer
|
|
5
|
+
|
|
6
|
+
import umu_commander.database as db
|
|
7
|
+
from umu_commander.configuration import DEFAULT_UMU_CONFIG_NAME
|
|
8
|
+
from umu_commander.proton import (
|
|
9
|
+
collect_proton_versions,
|
|
10
|
+
get_latest_umu_proton,
|
|
11
|
+
refresh_proton_versions,
|
|
12
|
+
)
|
|
13
|
+
from umu_commander.util import (
|
|
14
|
+
build_choices,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def select_config() -> str:
|
|
19
|
+
files = [file for file in Path.cwd().iterdir() if file.is_file()]
|
|
20
|
+
choices = build_choices(files, None)
|
|
21
|
+
return inquirer.select("Select umu-commander config:", choices).execute()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def untrack(target_dir: Path = None, *, quiet: bool = False):
|
|
25
|
+
if target_dir is None:
|
|
26
|
+
target_dir = Path.cwd()
|
|
27
|
+
|
|
28
|
+
target_dir = target_dir.absolute()
|
|
29
|
+
|
|
30
|
+
for proton_dir in db.get().keys():
|
|
31
|
+
for proton_ver in db.get(proton_dir):
|
|
32
|
+
if target_dir in db.get(proton_dir, proton_ver):
|
|
33
|
+
db.get(proton_dir, proton_ver).remove(target_dir)
|
|
34
|
+
|
|
35
|
+
if not quiet:
|
|
36
|
+
print("Directory removed from all tracking lists.")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def track(
|
|
40
|
+
proton_ver: Path = None,
|
|
41
|
+
config: Path = None,
|
|
42
|
+
*,
|
|
43
|
+
interactive: bool = True,
|
|
44
|
+
refresh_versions: bool = True,
|
|
45
|
+
quiet: bool = False,
|
|
46
|
+
):
|
|
47
|
+
if config is None:
|
|
48
|
+
if interactive:
|
|
49
|
+
config = select_config()
|
|
50
|
+
|
|
51
|
+
else:
|
|
52
|
+
config = Path.cwd() / DEFAULT_UMU_CONFIG_NAME
|
|
53
|
+
|
|
54
|
+
if refresh_versions:
|
|
55
|
+
refresh_proton_versions()
|
|
56
|
+
|
|
57
|
+
if proton_ver is None:
|
|
58
|
+
proton_dirs = collect_proton_versions(sort=True)
|
|
59
|
+
choices = build_choices(None, proton_dirs)
|
|
60
|
+
proton_ver: Path = inquirer.select(
|
|
61
|
+
"Select Proton version to track directory with:", choices
|
|
62
|
+
).execute()
|
|
63
|
+
|
|
64
|
+
proton_ver = proton_ver.absolute()
|
|
65
|
+
config = config.absolute()
|
|
66
|
+
|
|
67
|
+
untrack(config, quiet=True)
|
|
68
|
+
db.get(proton_ver.parent, proton_ver).append(config)
|
|
69
|
+
|
|
70
|
+
if not quiet:
|
|
71
|
+
print(
|
|
72
|
+
f"Directory {config} added to Proton version's {proton_ver.name} in {proton_ver.parent} tracking list."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def users(proton_ver: Path = None):
|
|
77
|
+
if proton_ver is None:
|
|
78
|
+
proton_dirs = collect_proton_versions(sort=True)
|
|
79
|
+
choices = build_choices(None, proton_dirs, count_elements=True)
|
|
80
|
+
proton_ver: Path = inquirer.select(
|
|
81
|
+
"Select Proton version to view user list:", choices
|
|
82
|
+
).execute()
|
|
83
|
+
|
|
84
|
+
proton_ver = proton_ver.absolute()
|
|
85
|
+
|
|
86
|
+
if proton_ver.parent in db.get() and proton_ver in db.get(proton_ver.parent):
|
|
87
|
+
version_users: list[Path] = db.get(proton_ver.parent, proton_ver)
|
|
88
|
+
if len(version_users) > 0:
|
|
89
|
+
print(
|
|
90
|
+
f"Directories tracked by {proton_ver.name} of {proton_ver.parent}:",
|
|
91
|
+
*version_users,
|
|
92
|
+
sep="\n\t",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
else:
|
|
96
|
+
print("This version is tracking no directories.")
|
|
97
|
+
|
|
98
|
+
else:
|
|
99
|
+
print("This version hasn't been used by umu before.")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def delete():
|
|
103
|
+
for proton_dir in db.get().keys():
|
|
104
|
+
for proton_ver, version_users in db.get(proton_dir).copy().items():
|
|
105
|
+
if proton_ver == get_latest_umu_proton():
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
if len(version_users) == 0:
|
|
109
|
+
confirmed: bool = inquirer.confirm(
|
|
110
|
+
f"Version {proton_ver.name} in {proton_dir} is tracking no directories, delete?"
|
|
111
|
+
).execute()
|
|
112
|
+
if confirmed:
|
|
113
|
+
try:
|
|
114
|
+
shutil.rmtree(proton_dir / proton_ver)
|
|
115
|
+
except FileNotFoundError:
|
|
116
|
+
pass
|
|
117
|
+
del db.get(proton_dir)[proton_ver]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def untrack_unlinked():
|
|
121
|
+
for proton_dir in db.get().keys():
|
|
122
|
+
for proton_ver, version_users in db.get()[proton_dir].items():
|
|
123
|
+
for user in version_users:
|
|
124
|
+
if not user.exists():
|
|
125
|
+
db.get(proton_dir, proton_ver).remove(user)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
import tomllib
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import tomli_w
|
|
9
|
+
from InquirerPy import inquirer
|
|
10
|
+
|
|
11
|
+
import umu_commander.configuration as config
|
|
12
|
+
from umu_commander import database as db
|
|
13
|
+
from umu_commander import tracking
|
|
14
|
+
from umu_commander.configuration import (
|
|
15
|
+
DEFAULT_UMU_CONFIG_NAME,
|
|
16
|
+
DLL_OVERRIDES_OPTIONS,
|
|
17
|
+
LANG_OVERRIDES_OPTIONS,
|
|
18
|
+
)
|
|
19
|
+
from umu_commander.proton import (
|
|
20
|
+
collect_proton_versions,
|
|
21
|
+
get_latest_umu_proton,
|
|
22
|
+
refresh_proton_versions,
|
|
23
|
+
)
|
|
24
|
+
from umu_commander.Types import Element
|
|
25
|
+
from umu_commander.util import build_choices
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def select_prefix() -> Path:
|
|
29
|
+
default = Element(str(Path.cwd() / "prefix"), "Current directory")
|
|
30
|
+
choices = build_choices([default, *config.DEFAULT_PREFIX_DIR.iterdir()], None)
|
|
31
|
+
return inquirer.select("Select wine prefix:", choices, default).execute()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def select_proton() -> Path:
|
|
35
|
+
choices = build_choices(None, collect_proton_versions(sort=True))
|
|
36
|
+
return inquirer.select(
|
|
37
|
+
"Select Proton version:", choices, Path.cwd() / "prefix"
|
|
38
|
+
).execute()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def select_dll_override() -> str:
|
|
42
|
+
choices = build_choices(DLL_OVERRIDES_OPTIONS, None)
|
|
43
|
+
return "".join(
|
|
44
|
+
[
|
|
45
|
+
selection
|
|
46
|
+
for selection in inquirer.checkbox(
|
|
47
|
+
"Select DLLs to override:", choices
|
|
48
|
+
).execute()
|
|
49
|
+
]
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def select_lang() -> str:
|
|
54
|
+
default = Element("", "No override")
|
|
55
|
+
choices = build_choices([default, *LANG_OVERRIDES_OPTIONS], None)
|
|
56
|
+
return inquirer.select("Select locale:", choices, default).execute()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def set_launch_args() -> list[str]:
|
|
60
|
+
options: str = inquirer.text(
|
|
61
|
+
"Enter executable options, separated by space:"
|
|
62
|
+
).execute()
|
|
63
|
+
return [opt.strip() for opt in options.split(" ")]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def select_exe() -> Path:
|
|
67
|
+
files = [file for file in Path.cwd().iterdir() if file.is_file()]
|
|
68
|
+
choices = build_choices(files, None)
|
|
69
|
+
return inquirer.select("Select game executable:", choices).execute()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def create(
|
|
73
|
+
prefix: Path = None,
|
|
74
|
+
proton_ver: Path = None,
|
|
75
|
+
dll_overrides: str = None,
|
|
76
|
+
lang: str = None,
|
|
77
|
+
launch_args: Iterable[str] = None,
|
|
78
|
+
exe: Path = None,
|
|
79
|
+
output: Path = None,
|
|
80
|
+
*,
|
|
81
|
+
interactive: bool = True,
|
|
82
|
+
refresh_versions: bool = True,
|
|
83
|
+
quiet: bool = False,
|
|
84
|
+
):
|
|
85
|
+
if refresh_versions:
|
|
86
|
+
refresh_proton_versions()
|
|
87
|
+
|
|
88
|
+
# Prefix selection
|
|
89
|
+
if prefix is None:
|
|
90
|
+
if interactive:
|
|
91
|
+
prefix = select_prefix()
|
|
92
|
+
|
|
93
|
+
else:
|
|
94
|
+
prefix = Path.cwd() / "prefix"
|
|
95
|
+
|
|
96
|
+
# Proton selection
|
|
97
|
+
if proton_ver is None:
|
|
98
|
+
if interactive:
|
|
99
|
+
proton_ver = select_proton()
|
|
100
|
+
|
|
101
|
+
else:
|
|
102
|
+
proton_ver = get_latest_umu_proton()
|
|
103
|
+
|
|
104
|
+
# Select DLL overrides
|
|
105
|
+
if dll_overrides is None and interactive:
|
|
106
|
+
dll_overrides = select_dll_override()
|
|
107
|
+
|
|
108
|
+
# Set language locale
|
|
109
|
+
if lang is None and interactive:
|
|
110
|
+
lang = select_lang()
|
|
111
|
+
|
|
112
|
+
# Input executable launch args
|
|
113
|
+
if launch_args is None and interactive:
|
|
114
|
+
launch_args = set_launch_args()
|
|
115
|
+
|
|
116
|
+
# Select executable name
|
|
117
|
+
if exe is None:
|
|
118
|
+
exe = select_exe()
|
|
119
|
+
|
|
120
|
+
params: dict[str, Any] = {
|
|
121
|
+
"umu": {
|
|
122
|
+
"prefix": str(prefix.absolute()),
|
|
123
|
+
"proton": str(proton_ver.absolute()),
|
|
124
|
+
"launch_args": launch_args,
|
|
125
|
+
"exe": str(exe),
|
|
126
|
+
},
|
|
127
|
+
"env": {"WINEDLLOVERRIDES": dll_overrides, "LANG": lang},
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if not params["umu"]["launch_args"]:
|
|
131
|
+
del params["umu"]["launch_args"]
|
|
132
|
+
|
|
133
|
+
if not params["env"]["WINEDLLOVERRIDES"]:
|
|
134
|
+
del params["env"]["WINEDLLOVERRIDES"]
|
|
135
|
+
|
|
136
|
+
if not params["env"]["LANG"]:
|
|
137
|
+
del params["env"]["LANG"]
|
|
138
|
+
|
|
139
|
+
if output is None:
|
|
140
|
+
output = Path.cwd() / DEFAULT_UMU_CONFIG_NAME
|
|
141
|
+
|
|
142
|
+
output = output.absolute()
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
with open(output, "wb") as file:
|
|
146
|
+
tomli_w.dump(params, file)
|
|
147
|
+
|
|
148
|
+
except (ValueError, TypeError):
|
|
149
|
+
if not quiet:
|
|
150
|
+
print("Could not create configuration file.")
|
|
151
|
+
|
|
152
|
+
if not quiet:
|
|
153
|
+
print(f"Configuration file {output.name} created in {output.parent}.")
|
|
154
|
+
print(f"Use with umu-commander run.")
|
|
155
|
+
|
|
156
|
+
tracking.track(proton_ver, output, refresh_versions=False, quiet=quiet)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def run(umu_config: Path = None):
|
|
160
|
+
if umu_config is None:
|
|
161
|
+
umu_config = Path.cwd() / DEFAULT_UMU_CONFIG_NAME
|
|
162
|
+
|
|
163
|
+
if not umu_config.exists():
|
|
164
|
+
print("Specified umu config does not exist.")
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
with open(umu_config, "rb") as toml_file:
|
|
168
|
+
toml_conf = tomllib.load(toml_file)
|
|
169
|
+
|
|
170
|
+
prefix_path = Path(toml_conf["umu"]["prefix"])
|
|
171
|
+
if not prefix_path.exists():
|
|
172
|
+
prefix_path.mkdir()
|
|
173
|
+
|
|
174
|
+
os.environ.update(toml_conf.get("env", {}))
|
|
175
|
+
subprocess.run(
|
|
176
|
+
args=["umu-run", "--config", config.DEFAULT_UMU_CONFIG_NAME],
|
|
177
|
+
env=os.environ,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def fix(umu_config: Path = None):
|
|
182
|
+
if umu_config is None:
|
|
183
|
+
umu_config = Path.cwd() / DEFAULT_UMU_CONFIG_NAME
|
|
184
|
+
|
|
185
|
+
if not umu_config.exists():
|
|
186
|
+
print("Specified umu config does not exist.")
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
with open(umu_config, "rb") as toml_file:
|
|
190
|
+
toml_conf = tomllib.load(toml_file)
|
|
191
|
+
|
|
192
|
+
base_dir = umu_config.absolute().parent
|
|
193
|
+
proton = Path(toml_conf["umu"]["proton"])
|
|
194
|
+
prefix = Path(toml_conf["umu"]["prefix"])
|
|
195
|
+
if not prefix.exists():
|
|
196
|
+
if base_dir in db.get(proton.parent, proton):
|
|
197
|
+
db.get(proton.parent, proton).append(base_dir)
|
|
198
|
+
|
|
199
|
+
toml_conf["umu"]["prefix"] = base_dir / prefix.name
|
|
200
|
+
|
|
201
|
+
exe = Path(toml_conf["umu"]["prefix"])
|
|
202
|
+
if not exe.exists():
|
|
203
|
+
toml_conf["umu"]["exe"] = base_dir / exe.name
|
umu_commander/util.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from InquirerPy.base.control import Choice
|
|
5
|
+
from InquirerPy.separator import Separator
|
|
6
|
+
|
|
7
|
+
from umu_commander import database as db
|
|
8
|
+
from umu_commander.Types import Element
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def count_users(proton_dir: Path, proton_ver: Path) -> str:
|
|
12
|
+
return (
|
|
13
|
+
f"({len(db.get(proton_dir, proton_ver))})"
|
|
14
|
+
if proton_ver in db.get(proton_dir)
|
|
15
|
+
else "(-)"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def build_choices(
|
|
20
|
+
elements: Iterable[Path | Element] | None,
|
|
21
|
+
groups: dict[Path | Element, Iterable[Path | Element]] | None,
|
|
22
|
+
*,
|
|
23
|
+
count_elements: bool = False,
|
|
24
|
+
) -> list[Separator | Choice | str]:
|
|
25
|
+
if elements is None:
|
|
26
|
+
elements = []
|
|
27
|
+
|
|
28
|
+
if groups is None:
|
|
29
|
+
groups = {}
|
|
30
|
+
|
|
31
|
+
choices: list[Choice | Separator] = [Choice(el, name=el.name) for el in elements]
|
|
32
|
+
if len(choices) > 0:
|
|
33
|
+
choices.append(Separator(""))
|
|
34
|
+
|
|
35
|
+
for group, elements in groups.items():
|
|
36
|
+
choices.extend(
|
|
37
|
+
[
|
|
38
|
+
Separator(f"In: {group}"),
|
|
39
|
+
*[
|
|
40
|
+
Choice(
|
|
41
|
+
el,
|
|
42
|
+
name=(
|
|
43
|
+
el.name
|
|
44
|
+
if not count_elements
|
|
45
|
+
else f"{el.name} {count_users(group, el)}"
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
for el in elements
|
|
49
|
+
],
|
|
50
|
+
Separator(""),
|
|
51
|
+
]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return choices
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: umu-commander
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: umu-commander is an interactive CLI tool to help you manage umu.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Mpaxlamitsounas/umu-commander
|
|
6
|
+
Project-URL: Issues, https://github.com/Mpaxlamitsounas/umu-commander/issues
|
|
7
|
+
Author-email: Mpaxlamitsounas <mpaxlamitsounas@mailbox.org>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE.txt
|
|
10
|
+
Keywords: umu,umu-launcher
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Requires-Python: >=3.12
|
|
14
|
+
Requires-Dist: inquirerpy
|
|
15
|
+
Requires-Dist: tomli-w
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
## umu-commander
|
|
19
|
+
### umu-commander is a CLI tool to augment umu-launcher as well as help you manage its Proton versions.
|
|
20
|
+
|
|
21
|
+
This tool does not provide a centralised way of managing your games or utilise template umu-configs. See [faugus-launcher](https://github.com/Faugus/faugus-launcher) or [Nero-umu](https://github.com/SeongGino/Nero-umu) for something more akin to a games launcher, and [umu-wrapper](https://github.com/korewaChino/umu-wrapper) for templating functionality.
|
|
22
|
+
|
|
23
|
+
Proton versions can track and untrack directories, with the intention of safely removing them once no game depends on one.
|
|
24
|
+
|
|
25
|
+
Vanilla umu config files currently (06/2025) do not support setting environmental variables. This tool adds such functionality with an extra TOML table within said configs, see `example_config.toml` for an example.
|
|
26
|
+
|
|
27
|
+
### Config
|
|
28
|
+
The configuration file lives at `~/.config/umu-commander.toml`, which cannot be changed as of now. You can generate one by running the app by itself.
|
|
29
|
+
|
|
30
|
+
The config schema is as follows:
|
|
31
|
+
|
|
32
|
+
| Name | Description |
|
|
33
|
+
|:---------------------------|:-------------------------------------------------------------------|
|
|
34
|
+
| `DB_DIR` | Directory where the Tracking DB is stored. |
|
|
35
|
+
| `DB_NAME` | Tracking DB filename. |
|
|
36
|
+
| `DEFAULT_PREFIX_DIR` | Directory where umu-commander will search for WINE prefixes. |
|
|
37
|
+
| `PROTON_PATHS` | List of directories umu-commander will search for Proton versions. |
|
|
38
|
+
| `UMU_CONFIG_NAME` | Name of the umu config created using umu-commander create. |
|
|
39
|
+
| `UMU_PROTON_PATH` | Directory where umu-launcher downloads its UMU-Proton versions. |
|
|
40
|
+
| `[DLL_OVERRIDES_OPTIONS]` | TOML table where all possible DLL overrides are listed. |
|
|
41
|
+
| `[LANG_OVERRIDES_OPTIONS]` | TOML table where all possible LANG overrides are listed. |
|
|
42
|
+
|
|
43
|
+
To add an extra DLL override option, add a line below the table in the form "`Label`" = "`WINE DLL override string`". Use the winhttp example as an example. You can add LANG overrides in a similar way.
|
|
44
|
+
|
|
45
|
+
### Verbs
|
|
46
|
+
umu-commander needs one of the following verbs specified after the executable name:
|
|
47
|
+
|
|
48
|
+
| Name | Description |
|
|
49
|
+
|:----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
50
|
+
| `track` | Tracks current directory with the selected Proton version.<br/>Also removes it from any other tracking lists. |
|
|
51
|
+
| `untrack` | Removes the current directory from all tracking lists. |
|
|
52
|
+
| `users` | Lists which directories the selected Proton version is tracking. |
|
|
53
|
+
| `delete` | Interactively deletes Proton versions that are currently tracking nothing.<br/>Will not remove the latest UMU-Proton and Proton versions that haven't been used for tracking before.<br/>umu-commander will never delete anything without invoking this verb and confirming. |
|
|
54
|
+
| `create` | Creates an augmented umu config in the current directory.<br/>These configs are compatible with vanilla umu-launcher, although the DLL override functionality won't work. |
|
|
55
|
+
| `run` | Runs a program using the umu config in the current directory. |
|
|
56
|
+
|
|
57
|
+
### Installation/Usage
|
|
58
|
+
Add umu-run to your PATH and then install with pipx by running `pipx install umu-commander`. After that you can use umu-commander by running `umu-commander <verb>`.
|
|
59
|
+
|
|
60
|
+
### Return codes
|
|
61
|
+
| Number | Name | Description |
|
|
62
|
+
|:-------|:--------------------|:----------------------------------------------------------------|
|
|
63
|
+
| 0 | `SUCCESS` | Program executed as intended. |
|
|
64
|
+
| 1 | `DECODING_ERROR` | Failed to parse a file. |
|
|
65
|
+
| 2 | `INVALID_SELECTION` | User selected an invalid verb or there are no valid selections. |
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
umu_commander/__init__.py,sha256=-3JbEnHqNK10K0pGrhFCFjxwXzBjLE5T-1MkDxQ9IUc,19
|
|
2
|
+
umu_commander/__main__.py,sha256=ggA7wDkJiQjy4qA3MxgoAOG_cdOVAe8hsAyWth8bzEI,5752
|
|
3
|
+
umu_commander/configuration.py,sha256=wUvUNloJ17sZ8bRt1NmNQLyV83nnKE_sQ8hxmOLa7kw,2968
|
|
4
|
+
umu_commander/database.py,sha256=Ix5ISzUKWF0xs3BUfhB8-azmKiWgP3NiaviKMydI-0E,1890
|
|
5
|
+
umu_commander/proton.py,sha256=ZYew3Yn4m02tY0Y13E6loMSdxRSOIHA4t-1_BnxZa4g,1962
|
|
6
|
+
umu_commander/tracking.py,sha256=l7bmedS-Z0d1oBrfpqx6bN2Ywged6gWwmSgzNyhXy0I,3837
|
|
7
|
+
umu_commander/umu_config.py,sha256=iM3Dkx3wFlLtw4y86BHnA5vAZjPzgD7Ah7VevMz1w8s,5620
|
|
8
|
+
umu_commander/util.py,sha256=xd9oF2sP49m7z1dFtvhD8NeAI2-_i1eX0aLcm8ttd4o,1450
|
|
9
|
+
umu_commander/Types/__init__.py,sha256=oDPHddB3i2Loo5eGg947USr6L0qGvMoS5TBo-3OR1dk,369
|
|
10
|
+
umu_commander-2.0.0.dist-info/METADATA,sha256=006spJJU2jDMLKNNjDtWljx3U5tjauXIYeio6eyg0z8,5924
|
|
11
|
+
umu_commander-2.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
12
|
+
umu_commander-2.0.0.dist-info/entry_points.txt,sha256=ljyUmDmgCCMm7mQgB1syoWbf_5AiemyrS6YN7eTn9CI,62
|
|
13
|
+
umu_commander-2.0.0.dist-info/licenses/LICENSE.txt,sha256=yipFXBRmVZ2Q44x1q18HccPUAECBQLXAOAr21aS57uY,1071
|
|
14
|
+
umu_commander-2.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 mpaxlamitsounas
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|