umu-commander 1.6.2__py3-none-any.whl → 1.7.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.

Potentially problematic release.


This version of umu-commander might be problematic. Click here for more details.

umu_commander/__init__.py CHANGED
@@ -1 +1 @@
1
- VERSION = "v1.6.2"
1
+ VERSION = "v1.7.0"
umu_commander/__main__.py CHANGED
@@ -1,7 +1,8 @@
1
- import os
2
1
  import sys
3
- from collections.abc import Callable
4
2
  from json import JSONDecodeError
3
+ from pathlib import Path
4
+
5
+ from InquirerPy.exceptions import InvalidArgument
5
6
 
6
7
  from umu_commander import configuration as config
7
8
  from umu_commander import database as db
@@ -10,27 +11,22 @@ from umu_commander.classes import ExitCode
10
11
  from umu_commander.configuration import CONFIG_DIR, CONFIG_NAME
11
12
  from umu_commander.util import print_help
12
13
 
13
- # TODO: Add related projects shoutout
14
- # https://github.com/Faugus/faugus-launcher
15
- # https://github.com/SeongGino/Nero-umu
16
- # https://github.com/korewaChino/umu-wrapper
17
-
18
- # TODO: https://inquirerpy.readthedocs.io/en/latest/
19
-
20
14
 
21
- def main() -> ExitCode:
15
+ def init() -> ExitCode:
22
16
  try:
23
17
  config.load()
24
18
 
25
19
  except (JSONDecodeError, KeyError):
26
- config_path: str = os.path.join(CONFIG_DIR, CONFIG_NAME)
27
- config_path_old: str = os.path.join(CONFIG_DIR, CONFIG_NAME + ".old")
20
+ config_path: Path = CONFIG_DIR / CONFIG_NAME
21
+ config_path_old: Path = CONFIG_DIR / (str(CONFIG_NAME) + ".old")
28
22
 
29
23
  print(f"Config file at {config_path} could not be read.")
30
24
 
31
- if not os.path.exists(config_path_old):
25
+ if not config_path_old.exists():
32
26
  print(f"Config file renamed to {config_path_old}.")
33
- os.rename(config_path, config_path_old)
27
+ config_path.rename(config_path_old)
28
+
29
+ return ExitCode.DECODING_ERROR
34
30
 
35
31
  except FileNotFoundError:
36
32
  config.dump()
@@ -39,51 +35,59 @@ def main() -> ExitCode:
39
35
  db.load()
40
36
 
41
37
  except JSONDecodeError:
42
- db_path: str = os.path.join(config.DB_DIR, config.DB_NAME)
43
- db_path_old: str = os.path.join(config.DB_DIR, config.DB_NAME + ".old")
38
+ db_path: Path = config.DB_DIR / config.DB_NAME
39
+ db_path_old: Path = config.DB_DIR / (str(config.DB_NAME) + ".old")
44
40
 
45
41
  print(f"Tracking file at {db_path} could not be read.")
46
42
 
47
- if not os.path.exists(db_path_old):
43
+ if not db_path_old.exists():
44
+ db_path.rename(db_path_old)
48
45
  print(f"DB file renamed to {db_path_old}.")
49
- os.rename(db_path, db_path_old)
50
46
 
51
47
  except FileNotFoundError:
52
48
  pass
53
49
 
54
- dispatch: dict[str, Callable] = {
55
- "track": tracking.track,
56
- "untrack": tracking.untrack,
57
- "users": tracking.users,
58
- "delete": tracking.delete,
59
- "create": umu_config.create,
60
- "run": umu_config.run,
61
- }
50
+ return ExitCode.SUCCESS
51
+
52
+
53
+ def main() -> int:
54
+ if (return_code := init()) != ExitCode.SUCCESS:
55
+ return return_code.value
62
56
 
63
57
  try:
64
- dispatch[sys.argv[1]]()
58
+ match sys.argv[1]:
59
+ case "track":
60
+ tracking.track(),
61
+ case "untrack":
62
+ tracking.untrack(),
63
+ case "users":
64
+ tracking.users(),
65
+ case "delete":
66
+ tracking.delete(),
67
+ case "create":
68
+ umu_config.create({"umu": {}, "env": {}}, True),
69
+ case "run":
70
+ umu_config.run(),
71
+ case _:
72
+ print("Unrecognised verb.")
73
+ print_help()
74
+ return ExitCode.INVALID_SELECTION.value
65
75
 
66
76
  except IndexError:
67
77
  print_help()
68
- return_val = ExitCode.SUCCESS
69
-
70
- except KeyError:
71
- print("Unrecognised verb.")
72
- print_help()
73
- return_val = ExitCode.INVALID_SELECTION
78
+ return ExitCode.SUCCESS.value
74
79
 
75
- except ValueError:
76
- return_val = ExitCode.INVALID_SELECTION
80
+ except InvalidArgument:
81
+ print("No choices to select from.")
82
+ return ExitCode.INVALID_SELECTION.value
77
83
 
78
84
  else:
79
- return_val = ExitCode.SUCCESS
85
+ return ExitCode.SUCCESS.value
80
86
 
81
87
  finally:
82
88
  tracking.untrack_unlinked()
83
89
  db.dump()
84
90
 
85
- return return_val.value
86
-
87
91
 
88
92
  if __name__ == "__main__":
89
93
  exit(main())
umu_commander/classes.py CHANGED
@@ -1,78 +1,15 @@
1
- from dataclasses import dataclass
2
1
  from enum import Enum
3
2
 
4
3
 
5
- @dataclass
6
- class Element:
7
- group_id: str = ""
8
- value: str = ""
9
- info: str = ""
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
10
11
 
11
- def as_proton_ver(self) -> "ProtonVer":
12
- return ProtonVer(self.group_id, self.value, self.info)
13
-
14
- def as_dll_override(self) -> "DLLOverride":
15
- return DLLOverride(self.info, self.value)
16
-
17
-
18
- @dataclass
19
- class ProtonVer(Element):
20
- def __init__(self, dir: str = "", version_num: str = "", user_count: str = ""):
21
- super().__init__(group_id=dir, value=version_num, info=user_count)
22
-
23
- @property
24
- def dir(self):
25
- return self.group_id
26
-
27
- @property
28
- def version_num(self):
29
- return self.value
30
-
31
- @property
32
- def user_count(self):
33
- return self.info
34
-
35
-
36
- @dataclass
37
- class DLLOverride(Element):
38
- def __init__(self, label: str = "", override_str: str = ""):
39
- super().__init__(group_id="", value=override_str, info=label)
40
-
41
- @property
42
- def override_str(self):
43
- return self.value
44
-
45
- @property
46
- def label(self):
47
- return self.info
48
-
49
-
50
- @dataclass
51
- class Value(Element):
52
- def __init__(self, value: str):
53
- super().__init__(value=value)
54
-
55
-
56
- @dataclass
57
- class Group:
58
- identity: str = ""
59
- label: str = ""
60
- elements: list[Element] = list
61
-
62
-
63
- @dataclass
64
- class ProtonDir(Group):
65
- @property
66
- def path(self):
67
- return self.identity
68
-
69
- @property
70
- def versions(self) -> list[ProtonVer]:
71
- return [e.as_proton_ver() for e in self.elements]
72
-
73
- @versions.setter
74
- def versions(self, value):
75
- self.elements = value
12
+ return instance
76
13
 
77
14
 
78
15
  class ExitCode(Enum):
@@ -6,48 +6,59 @@ from typing import Any
6
6
 
7
7
  import tomli_w
8
8
 
9
- from umu_commander.classes import DLLOverride
9
+ from umu_commander.classes import Element
10
10
 
11
- CONFIG_DIR: str = os.path.join(Path.home(), ".config")
12
- CONFIG_NAME: str = "umu-commander.toml"
11
+ CONFIG_DIR: Path = Path.home() / ".config"
12
+ CONFIG_NAME: Path = Path("umu-commander.toml")
13
13
 
14
14
 
15
- PROTON_PATHS: tuple[str, ...] = (
16
- os.path.join(Path.home(), ".local/share/Steam/compatibilitytools.d/"),
17
- os.path.join(Path.home(), ".local/share/umu/compatibilitytools"),
15
+ PROTON_PATHS: tuple[Path, ...] = (
16
+ Path.home() / ".local/share/Steam/compatibilitytools.d/",
17
+ Path.home() / ".local/share/umu/compatibilitytools",
18
18
  )
19
- UMU_PROTON_PATH: str = os.path.join(
20
- Path.home(), ".local/share/Steam/compatibilitytools.d/"
21
- )
22
- DB_NAME: str = "tracking.json"
23
- DB_DIR: str = os.path.join(Path.home(), ".local/share/umu/compatibilitytools")
24
- UMU_CONFIG_NAME: str = "umu-config.toml"
25
- DEFAULT_PREFIX_DIR: str = os.path.join(Path.home(), ".local/share/wineprefixes/")
26
- DLL_OVERRIDES_OPTIONS: tuple[DLLOverride, ...] = (
27
- DLLOverride("winhttp for BepInEx", "winhttp.dll=n;"),
19
+ UMU_PROTON_PATH: Path = Path(Path.home() / ".local/share/Steam/compatibilitytools.d")
20
+
21
+ DB_NAME: Path = Path("tracking.json")
22
+ DB_DIR: Path = Path.home() / ".local/share/umu/compatibilitytools"
23
+ UMU_CONFIG_NAME: Path = Path("umu-config.toml")
24
+ DEFAULT_PREFIX_DIR: Path = Path.home() / ".local/share/wineprefixes/"
25
+ DLL_OVERRIDES_OPTIONS: tuple[Element, ...] = (
26
+ Element("winhttp.dll=n,b;", "winhttp for BepInEx"),
28
27
  )
28
+ LANG_OVERRIDES_OPTIONS: tuple[Element, ...] = (Element("ja_JP.UTF8", "Japanese"),)
29
+
30
+ module = importlib.import_module(__name__)
29
31
 
30
32
 
31
33
  def load():
32
34
  with open(os.path.join(CONFIG_DIR, CONFIG_NAME), "rb") as conf_file:
33
35
  toml_conf = tomllib.load(conf_file)
34
- if "DLL_OVERRIDES_OPTIONS" in toml_conf:
35
- toml_conf["DLL_OVERRIDES_OPTIONS"] = tuple(
36
- [
37
- DLLOverride(label, override_str)
38
- for label, override_str in toml_conf[
39
- "DLL_OVERRIDES_OPTIONS"
40
- ].items()
41
- ]
36
+
37
+ # Proton dirs translation
38
+ setattr(
39
+ module,
40
+ "PROTON_PATHS",
41
+ (Path(proton_dir) for proton_dir in toml_conf["PROTON_PATHS"]),
42
+ )
43
+ del toml_conf["PROTON_PATHS"]
44
+
45
+ # DLL/LANG Override translation
46
+ for key in ["DLL", "LANG"]:
47
+ setattr(
48
+ module,
49
+ f"{key}_OVERRIDES_OPTIONS",
50
+ (
51
+ Element(value, name)
52
+ for name, value in toml_conf[f"{key}_OVERRIDES_OPTIONS"].items()
53
+ ),
42
54
  )
55
+ del toml_conf[f"{key}_OVERRIDES_OPTIONS"]
43
56
 
44
- module = importlib.import_module(__name__)
45
57
  for key, value in toml_conf.items():
46
- setattr(module, key, value)
58
+ setattr(module, key, Path(value))
47
59
 
48
60
 
49
61
  def _get_attributes() -> dict[str, Any]:
50
- module = importlib.import_module(__name__)
51
62
  attributes: dict[str, Any] = {}
52
63
  for key in dir(module):
53
64
  value = getattr(module, key)
@@ -66,8 +77,17 @@ def dump():
66
77
  del toml_conf["CONFIG_DIR"]
67
78
  del toml_conf["CONFIG_NAME"]
68
79
 
69
- toml_conf["DLL_OVERRIDES_OPTIONS"] = dict(
70
- [(override.info, override.value) for override in DLL_OVERRIDES_OPTIONS]
71
- )
80
+ for key, value in toml_conf.items():
81
+ match key:
82
+ case "PROTON_PATHS":
83
+ toml_conf[key] = [str(proton_dir) for proton_dir in PROTON_PATHS]
84
+
85
+ case "DLL_OVERRIDES_OPTIONS" | "LANG_OVERRIDES_OPTIONS":
86
+ toml_conf[key] = {
87
+ override.name: override for override in getattr(module, key)
88
+ }
89
+
90
+ case _:
91
+ toml_conf[key] = str(value)
72
92
 
73
93
  tomli_w.dump(toml_conf, conf_file)
umu_commander/database.py CHANGED
@@ -1,10 +1,10 @@
1
1
  import json
2
- import os
3
2
  from collections import defaultdict
3
+ from pathlib import Path
4
4
 
5
5
  import umu_commander.configuration as config
6
6
 
7
- _db: defaultdict[str, defaultdict[str, list[str]]] = defaultdict(
7
+ _db: defaultdict[Path, defaultdict[Path, list[Path]]] = defaultdict(
8
8
  lambda: defaultdict(list)
9
9
  )
10
10
 
@@ -12,27 +12,41 @@ _db: defaultdict[str, defaultdict[str, list[str]]] = defaultdict(
12
12
  def load():
13
13
  global _db
14
14
 
15
- if not os.path.exists(config.DB_DIR):
16
- os.mkdir(config.DB_DIR)
17
-
18
- with open(
19
- os.path.join(os.path.join(config.DB_DIR, config.DB_NAME)), "rt"
20
- ) as db_file:
21
- _db.update(json.load(db_file))
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)
22
28
 
23
29
 
24
30
  def dump():
25
- if not os.path.exists(config.DB_DIR):
26
- os.mkdir(config.DB_DIR)
27
-
28
- with open(os.path.join(config.DB_DIR, config.DB_NAME), "wt") as db_file:
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:
29
43
  # noinspection PyTypeChecker
30
- json.dump(_db, db_file, indent="\t")
44
+ json.dump(db, db_file, indent="\t")
31
45
 
32
46
 
33
47
  def get(
34
- proton_dir: str = None, proton_ver: str = None
35
- ) -> dict[str, dict[str, list[str]]] | dict[str, list[str]] | list[str]:
48
+ proton_dir: Path = None, proton_ver: Path = None
49
+ ) -> dict[Path, dict[Path, list[Path]]] | dict[Path, list[Path]] | list[Path]:
36
50
  global _db
37
51
 
38
52
  if proton_dir is None and proton_ver is None:
umu_commander/proton.py CHANGED
@@ -1,14 +1,13 @@
1
- import os
2
1
  import re
3
2
  import subprocess
3
+ from collections.abc import Iterable
4
+ from pathlib import Path
4
5
 
5
6
  import umu_commander.configuration as config
6
- import umu_commander.database as db
7
- from umu_commander.classes import ProtonDir, ProtonVer
8
7
 
9
8
 
10
- def _natural_sort_proton_ver_key(p: ProtonVer, _nsre=re.compile(r"(\d+)")):
11
- s: str = p.version_num
9
+ def _natural_sort_proton_ver_key(p: Path, _nsre=re.compile(r"(\d+)")):
10
+ s: str = p.name
12
11
  return [int(text) if text.isdigit() else text for text in _nsre.split(s)]
13
12
 
14
13
 
@@ -32,51 +31,35 @@ def refresh_proton_versions():
32
31
  break
33
32
 
34
33
 
35
- def _sort_proton_versions(versions: list[ProtonVer]) -> list[ProtonVer]:
36
- return sorted(versions, key=_natural_sort_proton_ver_key, reverse=True)
37
-
38
-
39
- def collect_proton_versions(
40
- sort: bool = False, user_count: bool = False
41
- ) -> list[ProtonDir]:
42
- def get_user_count(proton_dir: str, proton_ver) -> str:
43
- return (
44
- "(" + str(len(db.get(proton_dir, proton_ver))) + ")"
45
- if proton_ver in db.get(proton_dir)
46
- else "(-)"
47
- )
48
-
49
- proton_dirs: list[ProtonDir] = []
34
+ def collect_proton_versions(sort: bool = False) -> dict[Path, Iterable[Path]]:
35
+ versions: dict[Path, Iterable[Path]] = {}
50
36
  for proton_dir in config.PROTON_PATHS:
51
- versions: list[ProtonVer] = [
52
- ProtonVer(
53
- proton_dir,
54
- version,
55
- get_user_count(proton_dir, version) if user_count else "",
56
- )
57
- for version in os.listdir(proton_dir)
58
- if os.path.isdir(os.path.join(proton_dir, version))
59
- ]
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
60
42
 
61
43
  if sort:
62
- versions = sorted(versions, key=_natural_sort_proton_ver_key, reverse=True)
63
-
64
- proton_dirs.append(
65
- ProtonDir(proton_dir, f"Proton versions in {proton_dir}:", versions)
66
- )
44
+ versions[proton_dir] = sorted(
45
+ versions[proton_dir], key=_natural_sort_proton_ver_key, reverse=True
46
+ )
67
47
 
68
- return proton_dirs
48
+ return versions
69
49
 
70
50
 
71
- def get_latest_umu_proton():
72
- umu_proton_versions: list[ProtonVer] = [
73
- ProtonVer(config.UMU_PROTON_PATH, version)
74
- for version in os.listdir(config.UMU_PROTON_PATH)
75
- if "UMU" in version
76
- and os.path.isdir(os.path.join(config.UMU_PROTON_PATH, version))
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()
77
56
  ]
57
+
58
+ if len(umu_proton_versions) == 0:
59
+ return None
60
+
78
61
  umu_proton_versions = sorted(
79
62
  umu_proton_versions, key=_natural_sort_proton_ver_key, reverse=True
80
63
  )
81
64
 
82
- return umu_proton_versions[0].version_num
65
+ return umu_proton_versions[0]
umu_commander/tracking.py CHANGED
@@ -1,64 +1,74 @@
1
- import os
2
1
  import shutil
2
+ from pathlib import Path
3
+
4
+ from InquirerPy import inquirer
3
5
 
4
6
  import umu_commander.database as db
5
- from umu_commander.classes import ProtonDir, ProtonVer
6
7
  from umu_commander.proton import (
7
8
  collect_proton_versions,
8
9
  get_latest_umu_proton,
9
10
  refresh_proton_versions,
10
11
  )
11
12
  from umu_commander.util import (
12
- get_selection,
13
+ build_choices,
13
14
  )
14
15
 
15
16
 
16
- def untrack(quiet: bool = False):
17
- current_dir: str = os.getcwd()
17
+ def untrack(target_dir: Path = None, *, quiet: bool = False):
18
+ if target_dir is None:
19
+ target_dir = Path.cwd()
20
+
18
21
  for proton_dir in db.get().keys():
19
22
  for proton_ver in db.get(proton_dir):
20
- if current_dir in db.get(proton_dir, proton_ver):
21
- db.get(proton_dir, proton_ver).remove(current_dir)
23
+ if target_dir in db.get(proton_dir, proton_ver):
24
+ db.get(proton_dir, proton_ver).remove(target_dir)
22
25
 
23
26
  if not quiet:
24
27
  print("Directory removed from all tracking lists.")
25
28
 
26
29
 
27
30
  def track(
28
- proton_ver: ProtonVer = None, refresh_versions: bool = True, quiet: bool = False
31
+ proton_ver: Path = None,
32
+ target_dir: Path = None,
33
+ *,
34
+ refresh_versions: bool = True,
35
+ quiet: bool = False,
29
36
  ):
30
- if refresh_versions:
37
+ if target_dir is None:
38
+ target_dir = Path.cwd()
39
+
40
+ if refresh_versions and proton_ver is None:
31
41
  refresh_proton_versions()
32
42
 
33
43
  if proton_ver is None:
34
- proton_ver: ProtonVer = get_selection(
35
- "Select Proton version to track directory with:",
36
- None,
37
- collect_proton_versions(sort=True),
38
- ).as_proton_ver()
44
+ proton_dirs = collect_proton_versions(sort=True)
45
+ choices = build_choices(None, proton_dirs)
46
+ proton_ver: Path = inquirer.select(
47
+ "Select Proton version to track directory with:", choices
48
+ ).execute()
39
49
 
40
50
  untrack(quiet=True)
41
- current_directory: str = os.getcwd()
42
- db.get(proton_ver.dir, proton_ver.version_num).append(current_directory)
51
+ db.get(proton_ver.parent, proton_ver).append(target_dir)
43
52
 
44
53
  if not quiet:
45
54
  print(
46
- f"Directory {current_directory} added to Proton version's {proton_ver.version_num} in {proton_ver.dir} tracking list."
55
+ f"Directory {target_dir} added to Proton version's {proton_ver.name} in {proton_ver.parent} tracking list."
47
56
  )
48
57
 
49
58
 
50
- def users():
51
- proton_dirs: list[ProtonDir] = collect_proton_versions(sort=True, user_count=True)
52
-
53
- proton_ver: ProtonVer = get_selection(
54
- "Select Proton version to view user list:", None, proton_dirs
55
- ).as_proton_ver()
56
-
57
- if proton_ver.dir in db.get() and proton_ver.version_num in db.get(proton_ver.dir):
58
- version_users: list[str] = db.get(proton_ver.dir, proton_ver.version_num)
59
+ def users(proton_ver: Path = None):
60
+ if proton_ver is None:
61
+ proton_dirs = collect_proton_versions(sort=True)
62
+ choices = build_choices(None, proton_dirs, count_elements=True)
63
+ proton_ver: Path = inquirer.select(
64
+ "Select Proton version to view user list:", choices
65
+ ).execute()
66
+
67
+ if proton_ver.parent in db.get() and proton_ver in db.get(proton_ver.parent):
68
+ version_users: list[Path] = db.get(proton_ver.parent, proton_ver)
59
69
  if len(version_users) > 0:
60
70
  print(
61
- f"Directories tracked by {proton_ver.version_num} of {proton_ver.dir}:",
71
+ f"Directories tracked by {proton_ver.name} of {proton_ver.parent}:",
62
72
  *version_users,
63
73
  sep="\n\t",
64
74
  )
@@ -77,12 +87,12 @@ def delete():
77
87
  continue
78
88
 
79
89
  if len(version_users) == 0:
80
- selection: str = input(
81
- f"Version {proton_ver} in {proton_dir} is tracking no directories, delete? (Y/N) ? "
82
- )
83
- if selection.lower() == "y":
90
+ confirmed: bool = inquirer.confirm(
91
+ f"Version {proton_ver.name} in {proton_dir} is tracking no directories, delete?"
92
+ ).execute()
93
+ if confirmed:
84
94
  try:
85
- shutil.rmtree(os.path.join(proton_dir, proton_ver))
95
+ shutil.rmtree(proton_dir / proton_ver)
86
96
  except FileNotFoundError:
87
97
  pass
88
98
  del db.get(proton_dir)[proton_ver]
@@ -92,5 +102,5 @@ def untrack_unlinked():
92
102
  for proton_dir in db.get().keys():
93
103
  for proton_ver, version_users in db.get()[proton_dir].items():
94
104
  for user in version_users:
95
- if not os.path.exists(user):
105
+ if not user.exists():
96
106
  db.get(proton_dir, proton_ver).remove(user)
@@ -1,117 +1,107 @@
1
1
  import os
2
2
  import subprocess
3
3
  import tomllib
4
+ from pathlib import Path
4
5
  from typing import Any
5
6
 
6
7
  import tomli_w
8
+ from InquirerPy import inquirer
7
9
 
8
10
  import umu_commander.configuration as config
9
11
  from umu_commander import tracking
10
- from umu_commander.classes import DLLOverride, ProtonVer, Value
11
- from umu_commander.proton import collect_proton_versions, refresh_proton_versions
12
- from umu_commander.util import get_selection, string_to_value, strings_to_values
12
+ from umu_commander.classes import Element
13
+ from umu_commander.configuration import DLL_OVERRIDES_OPTIONS, LANG_OVERRIDES_OPTIONS
14
+ from umu_commander.proton import (
15
+ collect_proton_versions,
16
+ get_latest_umu_proton,
17
+ refresh_proton_versions,
18
+ )
19
+ from umu_commander.util import build_choices
13
20
 
14
21
 
15
- def create():
16
- refresh_proton_versions()
22
+ def select_prefix() -> str:
23
+ default = Element(str(Path.cwd() / "prefix"), "Current directory")
24
+ choices = build_choices([default, *config.DEFAULT_PREFIX_DIR.iterdir()], None)
25
+ return str(inquirer.select("Select wine prefix:", choices, default).execute())
17
26
 
18
- params: dict[str, Any] = {"umu": {}, "env": {}}
19
27
 
20
- # Prefix selection
21
- prefix_default: Value = string_to_value("Current directory")
22
- selection: str = get_selection(
23
- "Select wine prefix:",
24
- [*strings_to_values(os.listdir(config.DEFAULT_PREFIX_DIR)), prefix_default],
25
- None,
26
- default_element=prefix_default,
27
- ).value
28
-
29
- if selection == "Current directory":
30
- params["umu"]["prefix"] = os.path.join(os.getcwd(), "prefix")
31
- else:
32
- params["umu"]["prefix"] = os.path.join(config.DEFAULT_PREFIX_DIR, selection)
28
+ def select_proton() -> str:
29
+ default = Element(str(get_latest_umu_proton()), "Latest UMU-Proton")
30
+ choices = build_choices([default], collect_proton_versions(sort=True))
31
+ return str(
32
+ inquirer.select(
33
+ "Select Proton version:", choices, Path.cwd() / "prefix"
34
+ ).execute()
35
+ )
33
36
 
34
- # Proton selection
35
- selected_umu_latest: bool = False
36
- proton_ver: ProtonVer = get_selection(
37
- "Select Proton version:",
38
- None,
39
- collect_proton_versions(sort=True),
40
- ).as_proton_ver()
41
- params["umu"]["proton"] = os.path.join(proton_ver.dir, proton_ver.version_num)
42
37
 
43
- # Select DLL overrides
44
- possible_overrides: list[DLLOverride] = [
45
- DLLOverride(label="Reset"),
46
- DLLOverride(label="Done"),
47
- *config.DLL_OVERRIDES_OPTIONS,
48
- ]
49
- selected: set[int] = set()
50
- while True:
51
- print("Select DLLs to override, multiple can be selected:")
52
- for idx, override in enumerate(possible_overrides):
53
- if idx in selected:
54
- idx = "Y"
55
- print(f"{idx}) {override.label}")
56
-
57
- index: str = input("? ")
58
- if index == "":
59
- break
60
-
61
- if index.isdecimal():
62
- index: int = int(index)
63
- else:
64
- continue
38
+ def select_dll_override() -> str:
39
+ choices = build_choices(DLL_OVERRIDES_OPTIONS, None)
40
+ return "".join(
41
+ [
42
+ selection
43
+ for selection in inquirer.checkbox(
44
+ "Select DLLs to override:", choices
45
+ ).execute()
46
+ ]
47
+ )
65
48
 
66
- # reset
67
- if index == 0:
68
- selected = set()
69
- continue
70
49
 
71
- # done
72
- if index == 1:
73
- break
50
+ def select_lang() -> str:
51
+ default = Element("", "No override")
52
+ choices = build_choices([default, *LANG_OVERRIDES_OPTIONS], None)
53
+ return inquirer.select("Select locale:", choices, default).execute()
74
54
 
75
- if index - 1 < len(possible_overrides):
76
- selected.add(index)
77
55
 
78
- if len(selected) > 0:
79
- params["env"]["WINEDLLOVERRIDES"] = ""
80
- for selection in selected:
81
- # noinspection PyTypeChecker
82
- params["env"]["WINEDLLOVERRIDES"] += possible_overrides[
83
- selection
84
- ].override_str
56
+ def set_launch_args() -> list[str]:
57
+ options: str = inquirer.text(
58
+ "Enter executable options, separated by space:"
59
+ ).execute()
60
+ return [opt.strip() for opt in options.split(" ")]
61
+
62
+
63
+ def select_exe() -> str:
64
+ files = [file for file in Path.cwd().iterdir() if file.is_file()]
65
+ choices = build_choices(files, None)
66
+ return str(inquirer.select("Select game executable:", choices).execute())
67
+
68
+
69
+ def create(params: dict[str, dict[str, Any]], interactive: bool):
70
+ refresh_proton_versions()
71
+
72
+ # Prefix selection
73
+ if params.get("umu").get("prefix") is None:
74
+ if interactive:
75
+ params["umu"]["prefix"] = select_prefix()
76
+ else:
77
+ params["umu"]["prefix"] = str(Path.cwd() / "prefix")
78
+
79
+ # Proton selection
80
+ if params.get("umu").get("proton") is None:
81
+ if interactive:
82
+ params["umu"]["proton"] = select_proton()
83
+ else:
84
+ params["umu"]["proton"] = str(get_latest_umu_proton())
85
+
86
+ selected_umu_latest: bool = params["umu"]["proton"] == str(get_latest_umu_proton())
87
+
88
+ # Select DLL overrides
89
+ if interactive:
90
+ params["env"]["WINEDLLOVERRIDES"] = select_dll_override()
91
+ print(params["env"]["WINEDLLOVERRIDES"])
85
92
 
86
93
  # Set language locale
87
- lang_default: Value = string_to_value("Default")
88
- match get_selection(
89
- "Select locale:",
90
- [lang_default, string_to_value("Japanese")],
91
- None,
92
- default_element=lang_default,
93
- ).value:
94
- case "Default":
95
- pass
96
- case "Japanese":
97
- params["env"]["LANG"] = "ja_JP.UTF8"
94
+ if interactive:
95
+ if (lang := select_lang()) != "":
96
+ params["env"]["LANG"] = lang
98
97
 
99
98
  # Input executable launch args
100
- launch_args: list[str] = input(
101
- "Enter executable options, separated by spaces:\n? "
102
- ).split()
103
- params["umu"]["launch_args"] = launch_args
99
+ if interactive:
100
+ params["umu"]["launch_args"] = set_launch_args()
104
101
 
105
102
  # Select executable name
106
- files: list[str] = [
107
- file
108
- for file in os.listdir(os.getcwd())
109
- if os.path.isfile(os.path.join(os.getcwd(), file))
110
- ]
111
- executable_name: str = get_selection(
112
- "Select game executable:", strings_to_values(files), None
113
- ).value
114
- params["umu"]["exe"] = executable_name
103
+ if params["umu"].get("exe") is None:
104
+ params["umu"]["exe"] = select_exe()
115
105
 
116
106
  try:
117
107
  with open(config.UMU_CONFIG_NAME, "wb") as file:
@@ -119,21 +109,26 @@ def create():
119
109
 
120
110
  print(f"Configuration file {config.UMU_CONFIG_NAME} created at {os.getcwd()}.")
121
111
  print(f"Use by running umu-commander run.")
122
- tracking.track(proton_ver, False)
112
+ if not selected_umu_latest:
113
+ tracking.track(
114
+ Path(params["umu"]["proton"], Path(params["umu"]["exe"])).parent,
115
+ refresh_versions=False,
116
+ )
123
117
  except:
124
118
  print("Could not create configuration file.")
125
119
 
126
120
 
127
121
  def run():
128
- if not os.path.exists(config.UMU_CONFIG_NAME):
122
+ if not config.UMU_CONFIG_NAME.exists():
129
123
  print("No umu config in current directory.")
130
124
  return
131
125
 
132
126
  with open(config.UMU_CONFIG_NAME, "rb") as toml_file:
133
127
  toml_conf = tomllib.load(toml_file)
134
128
 
135
- if not os.path.exists(toml_conf["umu"]["prefix"]):
136
- os.mkdir(toml_conf["umu"]["prefix"])
129
+ prefix_path = Path(toml_conf["umu"]["prefix"])
130
+ if not prefix_path.exists():
131
+ prefix_path.mkdir()
137
132
 
138
133
  os.environ.update(toml_conf.get("env", {}))
139
134
  subprocess.run(
umu_commander/util.py CHANGED
@@ -1,117 +1,58 @@
1
- from umu_commander import VERSION
2
- from umu_commander.classes import Element, Group, Value
3
-
4
-
5
- def string_to_value(value: str) -> Value:
6
- return Value(value)
7
-
8
-
9
- def strings_to_values(values: list[str]) -> list[Value]:
10
- return [string_to_value(value) for value in values]
11
-
12
-
13
- def _selection_set_valid(
14
- selection_elements: list[Element] | None, selection_groups: list[Group] | None
15
- ):
16
- if (selection_elements is None or len(selection_elements) == 0) and (
17
- len(selection_groups) == 0
18
- or all([len(group.elements) == 0 for group in selection_groups])
19
- ):
20
- return False
21
- else:
22
- return True
23
-
24
-
25
- def _print_selection_group(
26
- elements: list[Element], enum_start_idx: int, tab: bool = True
27
- ):
28
- prefix: str = "\t" if tab else ""
29
- print(
30
- *[
31
- f"{prefix}{idx}) {element.value} {element.info}"
32
- for idx, element in enumerate(elements, start=enum_start_idx)
33
- ],
34
- sep="\n",
35
- )
36
- print("")
37
-
1
+ from collections.abc import Iterable
2
+ from pathlib import Path
38
3
 
39
- def _translate_index_to_selection(
40
- selection_index: int,
41
- selection_elements: list[Element],
42
- selection_groups: list[Group],
43
- ) -> Element:
44
- len_counter: int = 0
4
+ from InquirerPy.base.control import Choice
5
+ from InquirerPy.separator import Separator
45
6
 
46
- if selection_elements is not None:
47
- selection_groups.insert(0, Group(elements=selection_elements))
7
+ from umu_commander import VERSION
8
+ from umu_commander import database as db
9
+ from umu_commander.classes import Element
48
10
 
49
- for group in selection_groups:
50
- len_counter += len(group.elements)
51
- if len_counter > selection_index:
52
- break
53
11
 
54
- return Element(
55
- group.identity, group.elements[selection_index - len_counter].value, ""
12
+ def count_users(proton_dir: Path, proton_ver: Path) -> str:
13
+ return (
14
+ f"({len(db.get(proton_dir, proton_ver))})"
15
+ if proton_ver in db.get(proton_dir)
16
+ else "(-)"
56
17
  )
57
18
 
58
19
 
59
- def get_selection(
60
- prompt: str,
61
- selection_elements: list[Element] | None,
62
- selection_groups: list[Group] | None,
63
- default_element: Element = None,
64
- ) -> Element:
65
- if not _selection_set_valid(selection_elements, selection_groups):
66
- print("Nothing to select from.")
67
- raise ValueError
68
-
69
- if selection_groups is None:
70
- selection_groups = []
71
-
72
- while True:
73
- enum_start_idx: int = 1
74
-
75
- print(prompt)
76
-
77
- if selection_elements is not None:
78
- _print_selection_group(selection_elements, enum_start_idx, tab=False)
79
- enum_start_idx += len(selection_elements)
80
-
81
- for group_idx, group in enumerate(selection_groups):
82
- if len(group.elements) == 0:
83
- continue
84
-
85
- print(group.label)
86
- _print_selection_group(group.elements, enum_start_idx)
87
-
88
- enum_start_idx += len(group.elements)
89
-
90
- selection: str = input("? ")
91
- print("")
92
- if selection == "":
93
- if default_element is not None:
94
- return default_element
95
-
96
- # If only 1 choice
97
- if len(selection_groups) == 0 and len(selection_elements) == 1:
98
- return selection_elements[0]
99
-
100
- # len(selection_groups) == 1 and len(selection_groups[0].elements) == 1
101
- groups_with_one_element: list[Group] = [
102
- group for group in selection_groups if len(group.elements) == 1
20
+ def build_choices(
21
+ elements: Iterable[Path | Element] | None,
22
+ groups: dict[Path | Element, Iterable[Path | Element]] | None,
23
+ *,
24
+ count_elements: bool = False,
25
+ ) -> list[Separator | Choice | str]:
26
+ if elements is None:
27
+ elements = []
28
+
29
+ if groups is None:
30
+ groups = {}
31
+
32
+ choices: list[Choice | Separator] = [Choice(el, name=el.name) for el in elements]
33
+ if len(choices) > 0:
34
+ choices.append(Separator(""))
35
+
36
+ for group, elements in groups.items():
37
+ choices.extend(
38
+ [
39
+ Separator(f"In: {group}"),
40
+ *[
41
+ Choice(
42
+ el,
43
+ name=(
44
+ el.name
45
+ if not count_elements
46
+ else f"{el.name} {count_users(group, el)}"
47
+ ),
48
+ )
49
+ for el in elements
50
+ ],
51
+ Separator(""),
103
52
  ]
104
- if len(groups_with_one_element) == 1:
105
- return groups_with_one_element[0].elements[0]
106
-
107
- elif selection.isdecimal():
108
- selection: int = int(selection) - 1
109
- if enum_start_idx - 1 > selection >= 0:
110
- break
53
+ )
111
54
 
112
- return _translate_index_to_selection(
113
- selection, selection_elements, selection_groups
114
- )
55
+ return choices
115
56
 
116
57
 
117
58
  def print_help():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: umu-commander
3
- Version: 1.6.2
3
+ Version: 1.7.0
4
4
  Summary: umu-commander is an interactive CLI tool to help you manage umu.
5
5
  Project-URL: Homepage, https://github.com/Mpaxlamitsounas/umu-commander
6
6
  Project-URL: Issues, https://github.com/Mpaxlamitsounas/umu-commander/issues
@@ -11,11 +11,14 @@ Keywords: umu,umu-launcher
11
11
  Classifier: Operating System :: POSIX :: Linux
12
12
  Classifier: Programming Language :: Python :: 3
13
13
  Requires-Python: >=3.12
14
+ Requires-Dist: inquirerpy
14
15
  Requires-Dist: tomli-w
15
16
  Description-Content-Type: text/markdown
16
17
 
17
18
  ## umu-commander
18
- ### umu-commander is an interactive CLI tool to augment umu-launcher as well as help you manage its Proton versions.
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.
19
22
 
20
23
  Proton versions can track and untrack directories, with the intention of safely removing them once no game depends on one.
21
24
 
@@ -26,17 +29,18 @@ The configuration file lives at `~/.config/umu-commander.toml`, which cannot be
26
29
 
27
30
  The config schema is as follows:
28
31
 
29
- | Name | Description |
30
- |:--------------------------|:-------------------------------------------------------------------|
31
- | `PROTON_PATHS` | List of directories umu-commander will search for Proton versions. |
32
- | `UMU_PROTON_PATH` | Directory where umu-launcher downloads its UMU-Proton versions. |
33
- | `DB_NAME` | Tracking DB filename. |
34
- | `DB_DIR` | Directory where the Tracking DB is stored. |
35
- | `UMU_CONFIG_NAME` | Name of the umu config created using umu-commander create. |
36
- | `DEFAULT_PREFIX_DIR` | Directory where umu-commander will search for WINE prefixes. |
37
- | `[DLL_OVERRIDES_OPTIONS]` | TOML table where all possible DLL overrides are listed. |
38
-
39
- To add an extra DLL override option, add a line below the table in the form "`Label`" = "`WINE override string`". Use the winhttp example as a template.
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.
40
44
 
41
45
  ### Verbs
42
46
  umu-commander needs one of the following verbs specified after the executable name:
@@ -0,0 +1,14 @@
1
+ umu_commander/__init__.py,sha256=89F2IQARHlHjXGiEH_BGITazcP0AvMuV3rjFPaIQHDE,19
2
+ umu_commander/__main__.py,sha256=RP6aH0JeYXb69P_gbFmlM_hsALUor4Sf4RENJ-oYs2I,2460
3
+ umu_commander/classes.py,sha256=k2ymgDkVdG0-yBLJsByfbgx4qi4fIOU0RXAQcSdlK-A,363
4
+ umu_commander/configuration.py,sha256=dUM4esIU8fj7Ts0pA8jM7bwhldsZbnRJdYlaNpfQuWg,2866
5
+ umu_commander/database.py,sha256=Ix5ISzUKWF0xs3BUfhB8-azmKiWgP3NiaviKMydI-0E,1890
6
+ umu_commander/proton.py,sha256=ZYew3Yn4m02tY0Y13E6loMSdxRSOIHA4t-1_BnxZa4g,1962
7
+ umu_commander/tracking.py,sha256=ugorxaI_g7pNYR5BfUowcRzdciMg0Fin6bqNxyBli2w,3302
8
+ umu_commander/umu_config.py,sha256=7U9888VP-rW61lFWfZsHMbCq_5_Q3-vqS4Y8trdMupU,4223
9
+ umu_commander/util.py,sha256=ZTi3yrvGHy0Rr8xKEzloT-xcwCSHgDqBsH36oftOKiE,1817
10
+ umu_commander-1.7.0.dist-info/METADATA,sha256=yCgJWJbntlR-HkurqJR5Dlutyajrqhjh1D6kseld0Uo,5920
11
+ umu_commander-1.7.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
+ umu_commander-1.7.0.dist-info/entry_points.txt,sha256=ljyUmDmgCCMm7mQgB1syoWbf_5AiemyrS6YN7eTn9CI,62
13
+ umu_commander-1.7.0.dist-info/licenses/LICENSE.txt,sha256=yipFXBRmVZ2Q44x1q18HccPUAECBQLXAOAr21aS57uY,1071
14
+ umu_commander-1.7.0.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- umu_commander/__init__.py,sha256=eKShG3zJqAmg0Iu_xbUNkhAa7HaW-1-IthmdvuW6whs,19
2
- umu_commander/__main__.py,sha256=b5T4HnlLYreaLL9rbr0p7xT4roIoMJ8qJO3o_tcpK_M,2352
3
- umu_commander/classes.py,sha256=3vEC7Iq3buEUaIIJKO1ze57hs9yQ_-9pnkazHkSfcLk,1655
4
- umu_commander/configuration.py,sha256=bF_1n_8PQ04GmJZpC1scQ5OBZbsVXPstJ9RGfKE-D5k,2277
5
- umu_commander/database.py,sha256=mkBX0e6YEv2dWU2hCMaZwhSD02f_XefW0qbErcVHHYo,1183
6
- umu_commander/proton.py,sha256=K3KhaXxZawOZN84XKhXKP-HkIrW-sLSbMwoKBL2SrAU,2552
7
- umu_commander/tracking.py,sha256=WfWeQWnNNK_q-1rFt0HPFNDD8z7VcTCYKI5A_W8xNIg,3113
8
- umu_commander/umu_config.py,sha256=cePb3uKcf2WU4fwyQrqXJvlXmP7Riihc1guWfNAyT2c,4230
9
- umu_commander/util.py,sha256=8zn0c3gyAuZ8uuiDga4amsOYGFRy2FwzADsyJ1au0LY,3671
10
- umu_commander-1.6.2.dist-info/METADATA,sha256=nuaRiii5hRfKQ4toY-4w2Zq3aSIsDN0OpQmSm6Mo-5o,5391
11
- umu_commander-1.6.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
- umu_commander-1.6.2.dist-info/entry_points.txt,sha256=ljyUmDmgCCMm7mQgB1syoWbf_5AiemyrS6YN7eTn9CI,62
13
- umu_commander-1.6.2.dist-info/licenses/LICENSE.txt,sha256=yipFXBRmVZ2Q44x1q18HccPUAECBQLXAOAr21aS57uY,1071
14
- umu_commander-1.6.2.dist-info/RECORD,,