implement new gui

This commit is contained in:
Ian Burgwin
2020-07-19 20:34:22 -07:00
parent 187e27fc95
commit a58bfa4ae1
5 changed files with 419 additions and 465 deletions

View File

@@ -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).

409
ci-gui.py Normal file
View File

@@ -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()

438
gui.py
View File

@@ -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('<Button-1>', 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('<Enter>', lambda e: _bound_to_mousewheel(e, container))
container.bind(
'<Leave>', 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('<MouseWheel>', lambda e: _on_mousewheel(e, child))
child.bind_all('<Shift-MouseWheel>',
lambda e: _on_shiftmouse(e, child))
else:
child.bind_all('<Button-4>', lambda e: _on_mousewheel(e, child))
child.bind_all('<Button-5>', lambda e: _on_mousewheel(e, child))
child.bind_all('<Shift-Button-4>', lambda e: _on_shiftmouse(e, child))
child.bind_all('<Shift-Button-5>', lambda e: _on_shiftmouse(e, child))
def _unbound_to_mousewheel(event, widget):
if platform.system() == 'Windows' or platform.system() == 'Darwin':
widget.unbind_all('<MouseWheel>')
widget.unbind_all('<Shift-MouseWheel>')
else:
widget.unbind_all('<Button-4>')
widget.unbind_all('<Button-5>')
widget.unbind_all('<Shift-Button-4>')
widget.unbind_all('<Shift-Button-5>')
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("<Enter>", self.enter)
self.widget.bind("<Leave>", 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()

2
requirements-win32.txt Normal file
View File

@@ -0,0 +1,2 @@
-r requirements.txt
comtypes

View File

@@ -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"