diff --git a/README.md b/README.md index 0fd377d..d37a807 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ Experimental script to automate the process of a manual title install for Ninten Note for Windows users: Enabling "Add Python 3.X to PATH" is **NOT** required! Python is installed with the `py` launcher by default. 1. [Dump boot9.bin and movable.sed](https://ihaveamac.github.io/dump.html) from a 3DS system. -2. Download the repo ([zip link](https://github.com/ihaveamac/custom-install/archive/module-new-gui.zip) or `git clone`) +2. Download the repo ([zip link](https://github.com/ihaveamac/custom-install/archive/module-newer-gui.zip) or `git clone`) 3. Install the packages: - * Windows: `py -3 -m pip install --user -r requirements.txt` + * Windows: `py -3 -m pip install --user -r requirements-win32.txt` * macOS/Linux: `python3 -m pip install --user -r requirements.txt` 4. Run `custominstall.py` with boot9.bin, movable.sed, path to the SD root, and CIA files to install (see Usage section). 5. Download and use [custom-install-finalize](https://github.com/ihaveamac/custom-install/releases) on the 3DS system to finish the install. @@ -52,21 +52,20 @@ python3 custominstall.py -b boot9.bin -m movable.sed --sd /media/GM9SD file.cia ``` ## GUI -GUI wrapper to easily manage your apps. (More will go here...) - -![GUI](https://raw.githubusercontent.com/LyfeOnEdge/custom-install/master/docu/main.png) +A GUI is provided to make the process easier. ### GUI Setup +Linux users may need to install a Tk package: - Ubuntu/Debian: `sudo apt install python3-tk` - Manjaro/Arch: `sudo pacman -S tk` -- Mac: Sometimes the default tkinter libs that ship with mac don't work, you can get them on the python site - `https://www.python.org/downloads/mac-osx/` -- Windows: Install python - `Remember to install tcl/tk when doing a custom installation` + +Install the requirements listed in "Summary", then run `ci-gui.py`. ## License/Credits [save3ds by wwylele](https://github.com/wwylele/save3ds) is used to interact with the Title Database (details in `bin/README`). -Thanks to @LyfeOnEdge from the [brewtools Discord](https://brewtools.dev) for designing the GUI. Special thanks to CrafterPika and archbox for testing. - Thanks to @nek0bit for redesigning `custominstall.py` to work as a module, and for implementing an earlier GUI. +Thanks to @LyfeOnEdge from the [brewtools Discord](https://brewtools.dev) for designing the second version of the GUI. Special thanks to CrafterPika and archbox for testing. + Thanks to @BpyH64 for [researching how to generate the cmacs](https://github.com/d0k3/GodMode9/issues/340#issuecomment-487916606). diff --git a/ci-gui.py b/ci-gui.py new file mode 100644 index 0000000..0621cde --- /dev/null +++ b/ci-gui.py @@ -0,0 +1,409 @@ +# This file is a part of custom-install.py. +# +# custom-install is copyright (c) 2019-2020 Ian Burgwin +# This file is licensed under The MIT License (MIT). +# You can find the full license text in LICENSE.md in the root of this project. + +from os import environ, scandir +from os.path import abspath, join, isfile, dirname +from sys import exc_info, platform +from threading import Thread, Lock +from time import strftime +from traceback import format_exception +import tkinter as tk +import tkinter.ttk as ttk +import tkinter.filedialog as fd +import tkinter.messagebox as mb +from typing import TYPE_CHECKING + +from pyctr.crypto.engine import b9_paths +from pyctr.util import config_dirs + +from custominstall import CustomInstall + +if TYPE_CHECKING: + from typing import List + +is_windows = platform == 'win32' +if is_windows: + import comtypes.client as cc + + tbl = cc.GetModule('TaskbarLib.tlb') + + taskbar = cc.CreateObject('{56FDF344-FD6D-11D0-958A-006097C9A090}', interface=tbl.ITaskbarList3) + taskbar.HrInit() + +file_parent = dirname(__file__) + +# automatically load boot9 if it's in the current directory +b9_paths.insert(0, join(file_parent, 'boot9.bin')) +b9_paths.insert(0, join(file_parent, 'boot9_prot.bin')) + +seeddb_paths = [join(x, 'seeddb.bin') for x in config_dirs] +try: + seeddb_paths.insert(0, environ['SEEDDB_PATH']) +except KeyError: + pass +# automatically load seeddb if it's in the current directory +seeddb_paths.insert(0, join(file_parent, 'seeddb.bin')) + + +def find_first_file(paths): + for p in paths: + if isfile(p): + return p + + +# find boot9, seeddb, and movable.sed to auto-select in the gui +default_b9_path = find_first_file(b9_paths) +default_seeddb_path = find_first_file(seeddb_paths) +default_movable_sed_path = find_first_file([join(file_parent, 'movable.sed')]) + + +class ConsoleFrame(ttk.Frame): + def __init__(self, parent: tk.BaseWidget = None, starting_lines: 'List[str]' = None): + super().__init__(parent) + self.parent = parent + + self.rowconfigure(0, weight=1) + self.columnconfigure(0, weight=1) + + scrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL) + scrollbar.grid(row=0, column=1, sticky=tk.NSEW) + + self.text = tk.Text(self, highlightthickness=0, wrap='word', yscrollcommand=scrollbar.set) + self.text.grid(row=0, column=0, sticky=tk.NSEW) + + scrollbar.config(command=self.text.yview) + + if starting_lines: + for l in starting_lines: + self.text.insert(tk.END, l + '\n') + + self.text.see(tk.END) + self.text.configure(state=tk.DISABLED) + + def log(self, *message, end='\n', sep=' '): + self.text.configure(state=tk.NORMAL) + self.text.insert(tk.END, sep.join(message) + end) + self.text.see(tk.END) + self.text.configure(state=tk.DISABLED) + + +class CustomInstallGUI(ttk.Frame): + console = None + + def __init__(self, parent: tk.Tk = None): + super().__init__(parent) + self.parent = parent + + self.lock = Lock() + + self.log_messages = [] + + self.hwnd = None # will be set later + + self.rowconfigure(2, weight=1) + self.columnconfigure(0, weight=1) + + if is_windows: + # this is so progress can be shown in the taskbar + def setup_tab(): + self.hwnd = int(parent.wm_frame(), 16) + taskbar.ActivateTab(self.hwnd) + + self.after(100, setup_tab) + + # self.sd_path = None + # self.b9_path = default_b9_path + # self.seeddb_path = default_seeddb_path + # self.movable_sed_path = default_movable_sed_path + + # ---------------------------------------------------------------- # + # create file pickers for base files + file_pickers = ttk.Frame(self) + file_pickers.grid(row=0, column=0, sticky=tk.EW) + file_pickers.columnconfigure(1, weight=1) + + self.file_picker_textboxes = {} + + def sd_callback(): + f = fd.askdirectory(parent=parent, title='Select SD root (the directory or drive that contains ' + '"Nintendo 3DS")', initialdir=file_parent, mustexist=True) + if f: + sd_selected.delete('1.0', tk.END) + sd_selected.insert(tk.END, f) + + sd_msed_path = find_first_file([join(f, 'gm9', 'out', 'movable.sed'), join(f, 'movable.sed')]) + if sd_msed_path: + self.log('Found movable.sed on SD card at ' + sd_msed_path) + box = self.file_picker_textboxes['movable.sed'] + box.delete('1.0', tk.END) + box.insert(tk.END, sd_msed_path) + + # sd_button = ttk.Button(file_pickers, text='Select SD root', command=sd_callback) + # sd_button.grid(row=0, column=0, sticky=tk.NSEW) + # sd_selected = ttk.Label(file_pickers, text='') + # sd_selected.grid(row=0, column=1, sticky=tk.NSEW) + + sd_type_label = ttk.Label(file_pickers, text='SD root') + sd_type_label.grid(row=0, column=0) + + sd_selected = tk.Text(file_pickers, wrap='none', height=1) + sd_selected.grid(row=0, column=1, sticky=tk.EW) + + sd_button = ttk.Button(file_pickers, text='...', command=sd_callback) + sd_button.grid(row=0, column=2) + + self.file_picker_textboxes['sd'] = sd_selected + + # This feels so wrong. + def create_required_file_picker(type_name, types, default, row): + def internal_callback(): + f = fd.askopenfilename(parent=parent, title='Select ' + type_name, filetypes=types, + initialdir=file_parent) + if f: + selected.delete('1.0', tk.END) + selected.insert(tk.END, f) + + type_label = ttk.Label(file_pickers, text=type_name) + type_label.grid(row=row, column=0) + + selected = tk.Text(file_pickers, wrap='none', height=1) + selected.grid(row=row, column=1, sticky=tk.EW) + if default: + selected.insert(tk.END, default) + + button = ttk.Button(file_pickers, text='...', command=internal_callback) + button.grid(row=row, column=2) + + self.file_picker_textboxes[type_name] = selected + + create_required_file_picker('boot9', [('boot9 file', '*.bin')], default_b9_path, 1) + create_required_file_picker('seeddb', [('seeddb file', '*.bin')], default_seeddb_path, 2) + create_required_file_picker('movable.sed', [('movable.sed file', '*.sed')], default_movable_sed_path, 3) + + # ---------------------------------------------------------------- # + # create buttons to add cias + listbox_buttons = ttk.Frame(self) + listbox_buttons.grid(row=1, column=0) + + def add_cias_callback(): + files = fd.askopenfilenames(parent=parent, title='Select CIA files', filetypes=[('CIA files', '*.cia')], + initialdir=file_parent) + for f in files: + self.add_cia(f) + + add_cias = ttk.Button(listbox_buttons, text='Add CIAs', command=add_cias_callback) + add_cias.grid(row=0, column=0) + + def add_dirs_callback(): + d = fd.askdirectory(parent=parent, title='Select folder containing CIA files', initialdir=file_parent) + if d: + for f in scandir(d): + if f.name.lower().endswith('.cia'): + self.add_cia(f.path) + + add_dirs = ttk.Button(listbox_buttons, text='Add folder', command=add_dirs_callback) + add_dirs.grid(row=0, column=1) + + def remove_selected_callback(): + indexes = self.cia_listbox.curselection() + n = 0 + for i in indexes: + self.cia_listbox.delete(i - n) + n += 1 + + remove_selected = ttk.Button(listbox_buttons, text='Remove selected', command=remove_selected_callback) + remove_selected.grid(row=0, column=2) + + # ---------------------------------------------------------------- # + # create listbox + listbox_frame = ttk.Frame(self) + listbox_frame.grid(row=2, column=0, sticky=tk.NSEW) + listbox_frame.rowconfigure(0, weight=1) + listbox_frame.columnconfigure(0, weight=1) + + cia_listbox_scrollbar = ttk.Scrollbar(listbox_frame, orient=tk.VERTICAL) + cia_listbox_scrollbar.grid(row=0, column=1, sticky=tk.NSEW) + + self.cia_listbox = tk.Listbox(listbox_frame, highlightthickness=0, yscrollcommand=cia_listbox_scrollbar.set, + selectmode=tk.EXTENDED) + self.cia_listbox.grid(row=0, column=0, sticky=tk.NSEW) + + cia_listbox_scrollbar.config(command=self.cia_listbox.yview) + + # ---------------------------------------------------------------- # + # create progressbar + + self.progressbar = ttk.Progressbar(self, orient=tk.HORIZONTAL, mode='determinate') + self.progressbar.grid(row=3, column=0, sticky=tk.NSEW) + + # ---------------------------------------------------------------- # + # create start and console buttons + + control_frame = ttk.Frame(self) + control_frame.grid(row=4, column=0) + + self.skip_contents_var = tk.IntVar() + skip_contents_checkbox = ttk.Checkbutton(control_frame, text='Skip contents (only add to title database)', + variable=self.skip_contents_var) + skip_contents_checkbox.grid(row=0, column=0) + + show_console = ttk.Button(control_frame, text='Show console', command=self.open_console) + show_console.grid(row=0, column=1) + + start = ttk.Button(control_frame, text='Start install', command=self.start_install) + start.grid(row=0, column=2) + + self.status_label = ttk.Label(self, text='Waiting...') + self.status_label.grid(row=5, column=0, sticky=tk.NSEW) + + self.log('custom-install by ihaveamac', status=False) + self.log('https://github.com/ihaveamac/custom-install', status=False) + + self.log('Ready.') + + self.disable_during_install = (add_cias, add_dirs, remove_selected, start, *self.file_picker_textboxes.values()) + + def add_cia(self, path): + path = abspath(path) + self.cia_listbox.insert(tk.END, path) + + def open_console(self): + if self.console: + self.console.parent.lift() + self.console.focus() + else: + console_window = tk.Toplevel() + console_window.title('custom-install Console') + + self.console = ConsoleFrame(console_window, self.log_messages) + self.console.pack(fill=tk.BOTH, expand=True) + + def close(): + with self.lock: + try: + console_window.destroy() + except: + pass + self.console = None + + console_window.focus() + + console_window.protocol('WM_DELETE_WINDOW', close) + + def log(self, line, status=True): + with self.lock: + log_msg = f"{strftime('%H:%M:%S')} - {line}" + self.log_messages.append(log_msg) + if self.console: + self.console.log(log_msg) + + if status: + self.status_label.config(text=line) + + def show_error(self, message): + mb.showerror('Error', message, parent=self.parent) + + def show_info(self, message): + mb.showinfo('Info', message, parent=self.parent) + + def disable_buttons(self): + for b in self.disable_during_install: + b.config(state=tk.DISABLED) + + def enable_buttons(self): + for b in self.disable_during_install: + b.config(state=tk.NORMAL) + + def start_install(self): + sd_root = self.file_picker_textboxes['sd'].get('1.0', tk.END).strip() + boot9 = self.file_picker_textboxes['boot9'].get('1.0', tk.END).strip() + seeddb = self.file_picker_textboxes['seeddb'].get('1.0', tk.END).strip() + movable_sed = self.file_picker_textboxes['movable.sed'].get('1.0', tk.END).strip() + + if not sd_root: + self.show_error('SD root is not specified.') + return + if not boot9: + self.show_error('boot9 is not specified.') + return + if not seeddb: + self.show_error('seeddb is not specified.') + return + if not movable_sed: + self.show_error('movable.sed is not specified.') + return + + self.disable_buttons() + self.log('Starting install...') + + cias = self.cia_listbox.get(0, tk.END) + + installer = CustomInstall(boot9=boot9, + seeddb=seeddb, + movable=movable_sed, + cias=cias, + sd=sd_root, + skip_contents=self.skip_contents_var.get() == 1) + + finished_percent = 0 + max_percentage = 100 * len(cias) + self.progressbar.config(maximum=max_percentage) + + def ci_on_log_msg(message, *args, **kwargs): + # ignoring end + self.log(message) + + def ci_update_percentage(total_percent, total_read, size): + self.progressbar.config(value=total_percent + finished_percent) + if is_windows: + taskbar.SetProgressValue(self.hwnd, int(total_percent + finished_percent), max_percentage) + + def ci_on_error(exc): + if is_windows: + taskbar.SetProgressState(self.hwnd, tbl.TBPF_ERROR) + for line in format_exception(*exc): + for line2 in line.split('\n')[:-1]: + installer.log(line2) + self.show_error('An error occurred during installation.') + self.open_console() + + def ci_on_cia_start(idx): + nonlocal finished_percent + finished_percent = idx * 100 + if is_windows: + taskbar.SetProgressValue(self.hwnd, finished_percent, max_percentage) + + installer.event.on_log_msg += ci_on_log_msg + installer.event.update_percentage += ci_update_percentage + installer.event.on_error += ci_on_error + installer.event.on_cia_start += ci_on_cia_start + + if is_windows: + taskbar.SetProgressState(self.hwnd, tbl.TBPF_NORMAL) + + def install(): + try: + result = installer.start(continue_on_fail=False) + if result is True: + self.log('Done!') + self.show_info('To finish the install, run custom-install-finalize on the console.\n' + 'This will install a ticket and seed if required.') + elif result is False: + self.show_error('An error occurred when trying to run save3ds_fuse.') + self.open_console() + except: + installer.event.on_error(exc_info()) + + self.enable_buttons() + + Thread(target=install).start() + + +window = tk.Tk() +window.title('custom-install') +frame = CustomInstallGUI(window) +frame.pack(fill=tk.BOTH, expand=True) +window.mainloop() diff --git a/gui.py b/gui.py deleted file mode 100644 index cd1a609..0000000 --- a/gui.py +++ /dev/null @@ -1,438 +0,0 @@ -# This file is a part of custom-install.py. -# -# custom-install is copyright (c) 2019-2020 Ian Burgwin -# This file is licensed under The MIT License (MIT). -# You can find the full license text in LICENSE.md in the root of this project. - -# A gui for custom-install.py -# By LyfeOnEdge -import os, sys, platform, subprocess, threading -import tkinter as tk -import tkinter.filedialog as tkfiledialog -import style - - -class themedFrame(tk.Frame): - def __init__(self, frame, **kw): - tk.Frame.__init__(self, frame, **kw) - if not (kw.get("background") or kw.get("bg")): - self.configure(bg=style.BACKGROUND_COLOR) - if not kw.get("borderwidth"): - self.configure(borderwidth=0) - if not kw.get("highlightthickness"): - self.configure(highlightthickness=0) - - -class Button(tk.Label): - """Cross-platform button""" - - def __init__(self, frame, callback, **kw): - self.callback = callback - self.background = "#aaaaaa" - self.selected = False - tk.Label.__init__(self, frame, **kw) - self.configure(anchor="center") - self.configure(background=self.background) - self.configure(highlightthickness=1) - if not "font" in kw.keys(): - self.configure(font=style.BUTTON_FONT) - self.configure(highlightbackground="#999999") - self.bind('', self.on_click) - - # Use callback when our makeshift "button" clicked - def on_click(self, event=None): - self.configure(background="#dddddd") - if not self.selected: - self.after(100, self.on_click_color_change) - if self.callback: - self.callback() - - # Function to set the button's image - def setimage(self, image): - self.configure(image=image) - - # Function to set the button's text - def settext(self, text): - self.configure(text=text) - - def deselect(self): - self.selected = False - self.configure(background=self.background) - - def on_click_color_change(self): - if not self.selected: - self.configure(background=self.background) - - -class PathEntry(tk.Entry): - """Tkinter entry widget with a button to set the file path using tkinter's file dialog""" - - def __init__(self, frame, dir=False, filetypes=None, *args, **kw): - self.dir = dir - self.filetypes = filetypes - container = themedFrame(frame) - self.button = Button(container, self.set_path, text="...") - self.button.place(relheight=1, relx=1, x=- style.BUTTONSIZE, width=style.BUTTONSIZE) - tk.Entry.__init__(self, container, *args, **kw) - self.text_var = tk.StringVar() - self.configure(textvariable=self.text_var) - self.configure(background=style.ENTRY_COLOR) - self.configure(foreground=style.ENTRY_FOREGROUND) - self.configure(borderwidth=0) - self.configure(highlightthickness=2) - self.configure(highlightbackground=style.BUTTON_COLOR) - super().place(relwidth=1, relheight=1, width=- style.BUTTONSIZE) - self.container = container - - def clear(self): - self.text_var.set("") - - def set(self, string): - self.text_var.set(string) - - def get_var(self): - return self.text_var - - def get(self): - return self.text_var.get() - - def place(self, **kw): - self.container.place(**kw) - - def set_path(self): - if not self.dir: - self.set(tkfiledialog.askopenfilename(filetypes=self.filetypes)) - else: - self.set(tkfiledialog.askdirectory()) - - -class LabeledPathEntry(PathEntry): - """Gives the PathEntry class a label""" - - def __init__(self, frame, text, *args, **kw): - self.xtainer = themedFrame(frame) - label = tk.Label(self.xtainer, text=text, background=style.BACKGROUND_COLOR, foreground=style.LABEL_COLOR) - label.place(width=label.winfo_reqwidth(), relheight=1) - PathEntry.__init__(self, self.xtainer, *args, **kw) - PathEntry.place(self, relwidth=1, relheight=1, width=- (label.winfo_reqwidth() + 5), - x=label.winfo_reqwidth() + 5) - - def place(self, **kw): - self.xtainer.place(**kw) - - -class AutoScroll(object): - def __init__(self, master): - try: - vsb = tk.Scrollbar(master, orient='vertical', command=self.yview) - except: - pass - hsb = tk.Scrollbar(master, orient='horizontal', command=self.xview) - - try: - self.configure(yscrollcommand=self._autoscroll(vsb)) - except: - pass - self.configure(xscrollcommand=self._autoscroll(hsb)) - - self.grid(column=0, row=0, sticky='nsew') - try: - vsb.grid(column=1, row=0, sticky='ns') - except: - pass - hsb.grid(column=0, row=1, sticky='ew') - - master.grid_columnconfigure(0, weight=1) - master.grid_rowconfigure(0, weight=1) - - methods = tk.Pack.__dict__.keys() | tk.Grid.__dict__.keys() \ - | tk.Place.__dict__.keys() - - for m in methods: - if m[0] != '_' and m not in ('config', 'configure'): - setattr(self, m, getattr(master, m)) - - @staticmethod - def _autoscroll(sbar): - '''Hide and show scrollbar as needed.''' - - def wrapped(first, last): - first, last = float(first), float(last) - if first <= 0 and last >= 1: - sbar.grid_remove() - else: - sbar.grid() - sbar.set(first, last) - - return wrapped - - def __str__(self): - return str(self.master) - - -def _create_container(func): - '''Creates a tk Frame with a given master, and use this new frame to - place the scrollbars and the widget.''' - - def wrapped(cls, master, **kw): - container = themedFrame(master) - container.bind('', lambda e: _bound_to_mousewheel(e, container)) - container.bind( - '', lambda e: _unbound_to_mousewheel(e, container)) - return func(cls, container, **kw) - - return wrapped - - -def _bound_to_mousewheel(event, widget): - child = widget.winfo_children()[0] - if platform.system() == 'Windows' or platform.system() == 'Darwin': - child.bind_all('', lambda e: _on_mousewheel(e, child)) - child.bind_all('', - lambda e: _on_shiftmouse(e, child)) - else: - child.bind_all('', lambda e: _on_mousewheel(e, child)) - child.bind_all('', lambda e: _on_mousewheel(e, child)) - child.bind_all('', lambda e: _on_shiftmouse(e, child)) - child.bind_all('', lambda e: _on_shiftmouse(e, child)) - - -def _unbound_to_mousewheel(event, widget): - if platform.system() == 'Windows' or platform.system() == 'Darwin': - widget.unbind_all('') - widget.unbind_all('') - else: - widget.unbind_all('') - widget.unbind_all('') - widget.unbind_all('') - widget.unbind_all('') - - -def _on_mousewheel(event, widget): - if platform.system() == 'Windows': - widget.yview_scroll(-1 * int(event.delta / 120), 'units') - elif platform.system() == 'Darwin': - widget.yview_scroll(-1 * int(event.delta), 'units') - else: - if event.num == 4: - widget.yview_scroll(-1, 'units') - elif event.num == 5: - widget.yview_scroll(1, 'units') - - -class ScrolledText(AutoScroll, tk.Text): - @_create_container - def __init__(self, master, **kw): - tk.Text.__init__(self, master, **kw) - AutoScroll.__init__(self, master) - - -# from https://stackoverflow.com/questions/3221956/how-do-i-display-tooltips-in-tkinter - - -class CreateToolTip(object): - ''' - create a tooltip for a given widget - ''' - - def __init__(self, widget, text='widget info'): - self.widget = widget - self.text = text - self.widget.bind("", self.enter) - self.widget.bind("", self.close) - - def enter(self, event=None): - x = y = 0 - x, y, cx, cy = self.widget.bbox("insert") - x += self.widget.winfo_rootx() - y += self.widget.winfo_rooty() + 20 - # creates a toplevel window - self.tw = tk.Toplevel(self.widget) - # Leaves only the label and removes the app window - self.tw.wm_overrideredirect(True) - self.tw.wm_geometry("+%d+%d" % (x, y)) - label = tk.Label(self.tw, text=self.text, justify='left', - background='gray', foreground=style.LABEL_COLOR, - relief='solid', borderwidth=2, - font=("times", "12", "normal"), - wraplength=self.widget.winfo_width()) - label.pack(ipadx=1) - - def close(self, event=None): - if self.tw: - self.tw.destroy() - - -class threader_object: - """an object to be declared outside of tk root so - things can be called asyncronously (you cannot start - a new thread from within a tkinter callback so you - must call it from an object that exists outside)""" - - def do_async(self, func, arglist=[]): - threading.Thread(target=func, args=arglist).start() - - -class gui(tk.Tk): - def __init__(self, threader): - self.threader = threader - tk.Tk.__init__(self) - self.minsize(300, 400) - self.title("custom-install gui") - self.f = themedFrame(self) - self.f.place(relwidth=1, relheight=1) - - outer_frame = themedFrame(self.f) - outer_frame.place(relwidth=1, relheight=1, x=+ style.STANDARD_OFFSET, width=- 2 * style.STANDARD_OFFSET, - y=+ style.STANDARD_OFFSET, height=- 2 * style.STANDARD_OFFSET) - - self.sd_box = LabeledPathEntry(outer_frame, "Path to SD root -", dir=True) - self.sd_box.place(relwidth=1, height=20, x=0) - CreateToolTip(self.sd_box.xtainer, "Select the root of the sd card you wish to install the cias to.") - - self.sed_box = LabeledPathEntry(outer_frame, "Path to movable.sed file -", filetypes=[('sed file', '*.sed')]) - self.sed_box.place(relwidth=1, height=20, x=0, y=30) - CreateToolTip(self.sed_box.xtainer, "Select movable.sed file, this can be dumped from a 3ds") - - self.boot9_box = LabeledPathEntry(outer_frame, "Path to boot9 file -", filetypes=[('boot9 file', '*.bin')]) - self.boot9_box.place(relwidth=1, height=20, x=0, y=60) - CreateToolTip(self.boot9_box.xtainer, "Select the path to boot9.bin, this can be dumped from a 3ds") - - self.seeddb_box = LabeledPathEntry(outer_frame, "Path to seeddb file -", filetypes=[('seeddb file', '*.bin')]) - self.seeddb_box.place(relwidth=1, height=20, x=0, y=90) - CreateToolTip(self.seeddb_box.xtainer, "Select the path to seeddb.bin, this can retrieved from online") - - # ------------------------------------------------- - cia_container = themedFrame(outer_frame, borderwidth=0, highlightthickness=0) - cia_container.place(y=120, relwidth=1, height=190) - - cia_label = tk.Label(cia_container, text="cia paths - ", foreground=style.LABEL_COLOR, - background=style.BACKGROUND_COLOR) - cia_label.place(relwidth=1, height=20) - self.cia_box = tk.Listbox(cia_container, highlightthickness=0, bg=style.ENTRY_COLOR, - foreground=style.ENTRY_FOREGROUND) - self.cia_box.place(relwidth=1, height=70, y=20) - CreateToolTip(cia_label, - "Select the cias you wish to install to the sd card. The `add folder` button will add all cias in the selected folder, but will not check subdirs. The `remove cia` button will remove the currently selected file from the listbox.") - - add_cia_button = Button(cia_container, self.add_cia, text="add cia", font=style.monospace) - add_cia_button.place(relx=0, relwidth=0.333, height=20, y=95, width=- 6) - - add_cia_folder_button = Button(cia_container, self.add_cia_folder, text="add folder", font=style.monospace) - add_cia_folder_button.place(relx=0.333, relwidth=0.333, height=20, y=95, x=+ 3, width=- 6) - - remove_cia_button = Button(cia_container, self.remove_cia, text="remove cia", font=style.monospace) - remove_cia_button.place(relx=0.666, relwidth=0.333, height=20, y=95, x=+ 6, width=- 6) - # ------------------------------------------------- - - self.skip_contents = tk.IntVar() - skip_contents_checkbutton = tk.Checkbutton(outer_frame, text="Skip contents? (only add title info)", - variable=self.skip_contents, background=style.BACKGROUND_COLOR, - foreground=style.LABEL_COLOR, borderwidth=0, highlightthickness=0) - skip_contents_checkbutton.place(relwidth=1, y=239, height=20) - - console_label = tk.Label(outer_frame, text="Console:", background="black", foreground="white", - font=style.boldmonospace, borderwidth=0, highlightthickness=0) - console_label.place(relwidth=1, height=20, y=260) - self.console = ScrolledText(outer_frame, background="black", foreground="white", highlightthickness=0) - self.console.place(relwidth=1, relheight=1, y=280, height=- 272) - run_button = Button(outer_frame, self.run, text="run", font=style.boldmonospace) - run_button.place(relwidth=1, rely=1, y=- 22) - - def run(self): - args_extra = [] - - self.output_to_console("-----------------------\nStarting...\n") - - boot9 = self.boot9_box.get() - if not boot9: - self.output_to_console( - "Warning - boot9 not selected, if it's not set externally you may run into problems.\n") - else: - args_extra.extend(['-b', boot9]) - - sed = self.sed_box.get() - if not sed: - self.output_to_console("Failed to run - No movable.sed selected.\n") - return - args_extra.extend(['-m', sed]) - - sd = self.sd_box.get().strip() - if not sd: - self.output_to_console("Failed to run - SD path not selected.\n") - return - args_extra.extend(['--sd', sd]) - - seed = self.seeddb_box.get().strip() - if not seed: - self.output_to_console("Optional Seeddb not given - Certain CIAs May Require This!\n") - args_extra.extend(['--seeddb', seed]) - - cias = [] - for i in range(0, self.cia_box.size()): - cias.append(self.cia_box.get(i).strip()) - for cia in cias: - args_extra.append(cia) - - if self.skip_contents.get(): - args_extra.append('--skip-contents') - - print(f"Running custom-install.py with args {args_extra}\n") - - self.threader.do_async(execute_script, [args_extra, self.output_to_console]) - - def output_to_console(self, outstring): - self.console.insert('end', outstring) - self.console.see('end') - - def add_cia(self): - cia_to_add = tkfiledialog.askopenfilename(filetypes=[('cia file', '*.cia')]) - if cia_to_add: - self.cia_box.insert('end', cia_to_add) - - def add_cia_folder(self): - cia_dir_to_add = tkfiledialog.askdirectory() - if cia_dir_to_add: - cias_to_add = [f for f in os.listdir(cia_dir_to_add) if - (os.path.isfile(os.path.join(cia_dir_to_add, f)) and f.endswith(".cia"))] - if cias_to_add: - for cia_to_add in cias_to_add: - self.cia_box.insert('end', os.path.join(cia_dir_to_add, cia_to_add)) - - def remove_cia(self): - index = self.cia_box.curselection() - if index: - self.cia_box.delete(index) - if self.cia_box.size(): - self.cia_box.select_clear(0, 'end') - if self.cia_box.size() > 1: - try: - self.cia_box.select_set(index) - except: - pass - else: - self.cia_box.select_set(0) - - -def execute_script(args_extra, printer): - """Wrapper function to pipe install script output to a printer""" - args = [sys.executable, '-u', os.path.join(os.path.dirname(__file__), "custominstall.py")] - try: - args.extend(args_extra) - p = subprocess.Popen(args, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - bufsize=1, - ) - - with p.stdout: - for line in iter(p.stdout.readline, b''): - printer(line) - p.wait() - except Exception as e: - printer(f"Error while executing script with args - {args} | Exception - {e}\n") - - -t = threader_object() -window = gui(t) -window.mainloop() diff --git a/requirements-win32.txt b/requirements-win32.txt new file mode 100644 index 0000000..c7b2581 --- /dev/null +++ b/requirements-win32.txt @@ -0,0 +1,2 @@ +-r requirements.txt +comtypes diff --git a/style.py b/style.py deleted file mode 100644 index 66d806d..0000000 --- a/style.py +++ /dev/null @@ -1,18 +0,0 @@ -STANDARD_OFFSET = 10 #Offset to place everything -BUTTONSIZE = 30 - - -monospace = ("Monospace",10) -boldmonospace = ("Monospace",10,"bold") - -BUTTON_FONT = monospace - -BACKGROUND_COLOR = "#20232a" -BUTTON_COLOR = "#aaaaaa" -ENTRY_COLOR = "#373940" -ENTRY_FOREGROUND = "black" - -LABEL_COLOR = "#61dafb" - - -