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.
@@ -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))
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ umu-commander = umu_commander.__main__:main
@@ -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.