diff --git a/README.md b/README.md index c8ea832..f817c72 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ to enable and disable them, respectively. Upon first use, you will be prompted for the locations of your mods folder and 7 Days to Die installation. After configuring these, they are stored in -`seven-mods.cfg`. +`seven-mods.cfg`. The default location for the mods folder is the current +working directory. The default location for the 7 Days to Die installation directory is autodetected if installed from steam, or blank otherwise. seven-days also allows the user to create separate mod profiles. If, for instance, you frequent a multiplayer server with a certain set of mods, but @@ -29,7 +30,7 @@ were enabled when the profile was last saved. ## CLI Version -The main source file for the CLI version is `seven-mods.py`. General usage is: +The main source file for the CLI version is `seven_mods.py`. General usage is: `python seven-mods.py ` @@ -64,12 +65,27 @@ come in the future. ## GUI Version -A GUI version that uses tkinter is planned, but not yet ready. It'll be great -though! +The main source file for the GUI version is `seven_mods_gui.py`. It must be in +the same directory as `seven_mods.py` in order to work. -Trust me. +Upon first startup, it will prompt to configure the locations of your mods and +installation directories. Once finished, click "save" on the prompt and close +the window. -I'm a doctor.\* +Note: Sometimes the configuration prompt will appear behind the main window, so +if you don't see it at first, that's probably where it ended up. -\* I'm not an actual doctor. +The configuration prompt can be reopened at any time by clicking the "Configure" +button in the upper left corner of the main window. The combobox at the top is +for profiles. A profile can be loaded by selecting one from the combobox's +dropdown menu, then clicking the "Load" button. A profile can be saved by typing +its name into the combobox, then clicking "Save". A profile can be overwritten +the same way, or by selecting it from the dropdown menu and clicking "Save". +The main portion of the display is a scrollable listing of all available mods. +Similarly to the CLI version's `list` command, enabled mods are shown in green, +and disabled mods are shown in red. Clicking on a mod's list entry will toggle +it. + +Note: If you click twice too fast, tkinter will think you're double-clicking, +and won't change its state twice. \ No newline at end of file diff --git a/seven-mods.py b/seven_mods.py similarity index 96% rename from seven-mods.py rename to seven_mods.py index 5000aee..fe9c595 100755 --- a/seven-mods.py +++ b/seven_mods.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # -# seven-mods.py +# seven_mods.py # # Copyright 2023 Patrick Marsee # @@ -208,20 +208,16 @@ def save_config(cfg: Config) -> bool: return False def load_config(): - try: - lines = [] - with open(CONFIG_FILE, 'r') as config_file: - lines = config_file.readlines() - cfg_dict = {} - for line in lines: - if not "=" in line: - continue - var, val = line.split("=") - cfg_dict[var.strip()] = val.strip() - return Config(**cfg_dict) - except: - # no config or invalid config - return prompt_configuration_cli() + lines = [] + with open(CONFIG_FILE, 'r') as config_file: + lines = config_file.readlines() + cfg_dict = {} + for line in lines: + if not "=" in line: + continue + var, val = line.split("=") + cfg_dict[var.strip()] = val.strip() + return Config(**cfg_dict) def get_loaded_mods(cfg: Config) -> list: internal_mods_path = get_internal_mods_path(cfg) @@ -350,7 +346,12 @@ def parse_input(args: list, cfg: Config, profiles: ModProfiles): print(command) def main(args): - cfg = load_config() + cfg = None + try: + cfg = load_config() + except: + # no config or invalid config + cfg = prompt_configuration_cli() profiles = ModProfiles() profiles.load_mod_profiles() try: diff --git a/seven_mods_gui.py b/seven_mods_gui.py new file mode 100755 index 0000000..61c23b5 --- /dev/null +++ b/seven_mods_gui.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# seven_mods_gui.py +# +# Copyright 2023 Patrick Marsee +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# + +from tkinter import * +from tkinter import ttk +from tkinter import messagebox +from tkinter import filedialog +import seven_mods +import os.path +import xml.etree.ElementTree as et # I don't need anything powerful for this + +class AppFrame(ttk.Frame): + + def __init__(self, root): + super(AppFrame, self).__init__(root) + self.grid(column = 0, row = 0) + self.cfg = None + try: + self.cfg = seven_mods.load_config() + + except: + # no config or invalid config + print("Should be making a new window") + self.prompt_configuration_window() + self.profiles = seven_mods.ModProfiles() + self.profiles.load_mod_profiles() + self.initialize_widgets() + self.refresh_mod_list() + + def initialize_widgets(self): + self.columnconfigure(1, weight = 1) + + self.configure_button = ttk.Button(self, + text = "Configure", + command = self.prompt_configuration_window) + self.configure_button.grid(column = 0, row = 0) + + self.profile_var = StringVar() + self.profile_box = ttk.Combobox(self, + textvariable = self.profile_var) + self.refresh_profile_list() + self.profile_box.grid(column = 1, row = 0, sticky = (E, W)) + self.profile_load_btn = ttk.Button(self, + text = "Load", + command = self.command_load_button) + self.profile_load_btn.grid(column = 2, row = 0) + self.profile_save_btn = ttk.Button(self, + text = "Save", + command = self.command_save_button) + self.profile_save_btn.grid(column = 3, row = 0) + self.mod_list = ttk.Treeview(self, + columns = ("name", "version", "author", "enabled"), + height = 20, + selectmode = "browse") + self.mod_list.heading("name", text = "Name") + self.mod_list.heading("version", text = "Version") + self.mod_list.heading("author", text = "Author") + self.mod_list.heading("enabled", text = "Enabled") + self.mod_list.tag_configure("DISABLED", foreground = "red") + self.mod_list.tag_configure("ENABLED", foreground = "green") + self.mod_list.tag_bind("DISABLED", '<>', lambda e: self.command_enable(self.mod_list.selection()[0])) + self.mod_list.tag_bind("ENABLED", '<>', lambda e: self.command_disable(self.mod_list.selection()[0])) + self.mod_list.grid(column = 0, row = 1, columnspan = 4, sticky = (N, E, S, W)) + + def prompt_configuration_window(self): + SEVEN_DIR = "" + MODS_DIR = "" + if self.cfg == None: + SEVEN_DIR = seven_mods.get_seven_days_install_path() + MODS_DIR = seven_mods.MODS_DIR + else: + SEVEN_DIR = self.cfg.seven_dir + MODS_DIR = self.cfg.mods_dir + t = Toplevel(self) + t.title("Configuration") + t.columnconfigure(1, weight = 1) + t.geometry("800x90") + + ttk.Label(t, text = "Mod folder").grid(column = 0, row = 0) + mod_path_var = StringVar() + mod_path_var.set(MODS_DIR) + mod_path_entry = ttk.Entry(t, textvariable = mod_path_var) + mod_path_entry.grid(column = 1, row = 0, sticky = (E, W)) + mod_path_button = ttk.Button(t, + text = "Browse...", + command = lambda: mod_path_var.set(filedialog.askdirectory(initialdir = MODS_DIR))) + mod_path_button.grid(column = 2, row = 0, sticky = (E, W)) + + ttk.Label(t, text = "7D2D installation folder").grid(column = 0, row = 1) + seven_path_var = StringVar() + seven_path_var.set(SEVEN_DIR) + seven_path_entry = ttk.Entry(t, textvariable = seven_path_var) + seven_path_entry.grid(column = 1, row = 1, sticky = (E, W)) + seven_path_button = ttk.Button(t, + text = "Browse...", + command = lambda: seven_path_var.set(filedialog.askdirectory(initialdir = SEVEN_DIR))) + seven_path_button.grid(column = 2, row = 1, sticky = (E, W)) + + save_button = ttk.Button(t, text = "Save", command = lambda: self.save_config(mod_path_var.get(), seven_path_var.get())) + save_button.grid(column = 2, row = 2) + + def save_config(self, mods_dir: str, seven_dir: str): + self.cfg = seven_mods.Config(mods_dir = mods_dir, seven_dir = seven_dir) + seven_mods.save_config(self.cfg) + self.refresh_mod_list() + + def get_mod_data(self, mod: str) -> tuple: + mod_info_path = os.path.join(self.cfg.mods_dir, mod, "ModInfo.xml") + try: + tree = et.parse(mod_info_path) + except: + return "", "", "" + root = tree.getroot() + name = "" + author = "" + version = "" + for node in root[0]: + if node.tag == "Name": + name = node.attrib["value"] + elif node.tag == "Author": + author = node.attrib["value"] + elif node.tag == "Version": + version = node.attrib["value"] + return name, version, author + + def refresh_mod_list(self): + if self.cfg == None: + return + available_mods = seven_mods.get_available_mods(self.cfg) + loaded_mods = seven_mods.get_loaded_mods(self.cfg) + # remove the current list items + for item in self.mod_list.get_children(""): + self.mod_list.delete(item) + # add the new ones + for mod in available_mods: + enabled_text = "DISABLED" + if mod in loaded_mods: + enabled_text = "ENABLED" + values = *self.get_mod_data(mod), enabled_text + self.mod_list.insert("", "end", mod, text = mod, values = values, tags = (enabled_text)) + + def refresh_profile_list(self): + prof_list = [p for p in self.profiles.profiles] + self.profile_box["values"] = prof_list + + def command_enable(self, mod): + try: + seven_mods.enable_mod(self.cfg, mod) + self.mod_list.item(mod, tags = ("ENABLED")) + self.mod_list.set(mod, "enabled", "ENABLED") + except seven_mods.SevenModsError as e: + messagebox.showerror(message = str(e)) + + def command_disable(self, mod): + try: + seven_mods.disable_mod(self.cfg, mod) + self.mod_list.item(mod, tags = ("DISABLED")) + self.mod_list.set(mod, "enabled", "DISABLED") + except seven_mods.SevenModsError as e: + messagebox.showerror(message = str(e)) + + def command_load_button(self): + prof = self.profile_var.get() + if prof in self.profiles.profiles: + try: + seven_mods.command_disable(["", "", "*"], self.cfg, self.profiles) + seven_mods.command_enable(["", ""] + self.profiles.profiles[prof], self.cfg, self.profiles) + self.refresh_mod_list() + except seven_mods.SevenModsError as e: + messagebox.showerror(message = str(e)) + else: + messagebox.showerror(message = "Must load an existing profile.") + + def command_save_button(self): + prof = self.profile_var.get() + try: + self.profiles.profiles[prof] = seven_mods.get_loaded_mods(cfg) + self.profiles.save_mod_profiles() + self.refresh_profile_list() + except seven_mods.SevenModsError as e: + messagebox.showerror(message = str(e)) + +def main(args): + root = Tk() + root.title("Seven Mods") + root.columnconfigure(0, weight=1) + root.rowconfigure(0, weight=1) + AppFrame(root) + root.mainloop() + return 0 + +if __name__ == '__main__': + import sys + sys.exit(main(sys.argv))