30 Commits

Author SHA1 Message Date
Ian Burgwin
a515ca7e61 finalize: fix using pointers incorrectly, meaning only one title would work 2020-05-23 11:02:25 -07:00
Ian Burgwin
167a80ff11 gui: use list for args instead of creating a string 2020-05-22 02:43:11 -07:00
Ian Burgwin
272cc544cd finalize: support all 3 versions of cifinish 2020-05-22 01:49:49 -07:00
Ian Burgwin
443498d706 finalize: update for libctru changes 2020-05-18 04:56:53 -07:00
Ian Burgwin
17404231d3 custominstall: support --seeddb argument (close #6), test id0 folders as a hexstring 2020-04-02 10:41:57 -07:00
Ian Burgwin
393fd03da1 update copyright notice 2020-04-02 10:39:48 -07:00
Ian Burgwin
26c21137ec requirements: allow any version of pycryptodomex above or equal to 3.9.4 2020-04-02 10:27:01 -07:00
Ian Burgwin
9c1709922a gui: reformat, add license information, use correct script name 2020-04-02 09:58:31 -07:00
Ian Burgwin
43ae023000 README: updates
* Use the original repository for the releases badge.
* Update text to not use second-person.
* Move GUI credits to License/Credits section.
2020-04-02 04:55:40 -07:00
Ian Burgwin
e7c6ff7344 Merge branch 'LyfeOnEdge-master' into module-new-gui 2020-03-31 05:51:07 -07:00
Ian Burgwin
5c41f03784 Merge branch 'master' of https://github.com/LyfeOnEdge/custom-install into LyfeOnEdge-master 2020-03-31 05:50:35 -07:00
Ian Burgwin
4693935d87 remove old gui 2020-03-31 05:49:34 -07:00
Andrew (LyfeOnEdge) (ArcticGentoo)
11cbbcdf1e Update README.md 2020-03-26 19:15:09 -07:00
Andrew (LyfeOnEdge) (ArcticGentoo)
8f4b3d1134 Update README.md 2020-03-26 19:14:21 -07:00
Andrew (LyfeOnEdge) (ArcticGentoo)
d5a4cbd8f8 Update README.md 2020-03-26 19:14:08 -07:00
Andrew (LyfeOnEdge) (ArcticGentoo)
625f1f9db5 Update README.md 2020-03-26 19:13:58 -07:00
Andrew (LyfeOnEdge) (ArcticGentoo)
61b27f33ed Update README.md 2020-03-26 19:13:11 -07:00
LyfeOnEdge
13d0cbd796 Update main.png 2020-03-26 18:43:37 -07:00
LyfeOnEdge
14e5692cac update color scheme and fix some elements' coloration 2020-03-26 18:16:40 -07:00
LyfeOnEdge
b799e3af1a add style.py and tooltips 2020-03-26 17:56:06 -07:00
LyfeOnEdge
e8787a2d9a Update README.md 2020-03-25 19:41:08 -07:00
LyfeOnEdge
a08654160a Update README.md 2020-03-25 19:40:43 -07:00
LyfeOnEdge
6922c0d209 Update README.md 2020-03-25 19:40:09 -07:00
LyfeOnEdge
29e17bec6d Update README.md 2020-03-25 19:35:22 -07:00
LyfeOnEdge
8ee248c793 Update README.md 2020-03-25 19:35:02 -07:00
LyfeOnEdge
12d59cad5d Update README.md 2020-03-25 19:23:20 -07:00
LyfeOnEdge
ff196a667d Update README.md 2020-03-25 19:22:33 -07:00
LyfeOnEdge
f21b63f9dd document gui 2020-03-25 19:21:22 -07:00
LyfeOnEdge
c0e1d45054 Add gui.py 2020-03-25 19:01:55 -07:00
Ian Burgwin
0195ea75d4 README: remove mention of --seeddb
This option does not actually exist on this branch. It will be in module-rewrite eventually.
2020-02-01 21:58:41 -08:00
9 changed files with 661 additions and 266 deletions

3
.gitignore vendored
View File

@@ -15,3 +15,6 @@ venv/
# JetBrains
.idea/
=======
*.pyc

View File

@@ -1,3 +1,5 @@
[![License](https://img.shields.io/badge/License-MIT-blue.svg)]() ![Releases](https://img.shields.io/github/downloads/ihaveamac/custom-install/total.svg)
# custom-install
Experimental script to automate the process of a manual title install for Nintendo 3DS. Originally created late June 2019.
@@ -11,7 +13,7 @@ Experimental script to automate the process of a manual title install for Ninten
5. Download and use [custom-install-finalize](https://github.com/ihaveamac/custom-install/releases) on the 3DS system to finish the install.
## Setup
Linux users must build [wwylele/save3ds](https://github.com/wwylele/save3ds) and place `save3ds_fuse` in `bin/linux`. Just install [rust using rustup](https://www.rust-lang.org/tools/install), then compile with: `cargo build`. Your compiled binary is located in `target/debug/save3ds_fuse`, copy it to `bin/linux`.
Linux users must build [wwylele/save3ds](https://github.com/wwylele/save3ds) and place `save3ds_fuse` in `bin/linux`. Install [rust using rustup](https://www.rust-lang.org/tools/install), then compile with: `cargo build`. The compiled binary is located in `target/debug/save3ds_fuse`, copy it to `bin/linux`.
movable.sed is required and can be provided with `-m` or `--movable`.
@@ -25,7 +27,7 @@ boot9 is needed:
A [SeedDB](https://github.com/ihaveamac/3DS-rom-tools/wiki/SeedDB-list) is needed for newer games (2015+) that use seeds.
SeedDB is checked in order of:
* `--seeddb` argument (if set)
* `-s` or `--seeddb` argument (if set)
* `SEEDDB_PATH` environment variable (if set)
* `%APPDATA%\3ds\seeddb.bin` (Windows-specific)
* `~/Library/Application Support/3ds/seeddb.bin` (macOS-specific)
@@ -47,9 +49,22 @@ python3 custominstall.py -b boot9.bin -m movable.sed --sd /Volumes/GM9SD file.ci
python3 custominstall.py -b boot9.bin -m movable.sed --sd /media/GM9SD file.cia file2.cia
```
## License/Credits
`pyctr/` is from [ninfs `795373d`](https://github.com/ihaveamac/ninfs/tree/795373db07be0cacd60215d8eccf16fe03535984/ninfs/pyctr).
## GUI
GUI wrapper to easily manage your apps. (More will go here...)
![GUI](https://raw.githubusercontent.com/LyfeOnEdge/custom-install/master/docu/main.png)
### GUI Setup
- 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`
## 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 @BpyH64 for [researching how to generate the cmacs](https://github.com/d0k3/GodMode9/issues/340#issuecomment-487916606).

View File

@@ -1,6 +1,6 @@
# This file is a part of custom-install.py.
#
# Copyright (c) 2019-2020 Ian Burgwin
# 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.
@@ -131,12 +131,16 @@ def save_cifinish(path: 'Union[PathLike, bytes, str]', data: dict):
class CustomInstall:
def __init__(self, boot9, movable, cias, sd, skip_contents=False):
cia: CIAReader
def __init__(self, boot9, seeddb, movable, cias, sd, skip_contents=False):
self.event = Events()
self.log_lines = [] # Stores all info messages for user to view
self.crypto = CryptoEngine(boot9=boot9)
self.crypto.setup_sd_key_from_file(movable)
self.seeddb = seeddb
self.cias = cias
self.sd = sd
self.skip_contents = skip_contents
@@ -151,7 +155,6 @@ class CustomInstall:
dst.write(data)
left -= to_read
total_read = size - left
# self.log(f' {(total_read / size) * 100:>5.1f}% {total_read / 1048576:>.1f} MiB / {size / 1048576:.1f} MiB')
self.event.update_percentage((total_read / size) * 100, total_read / 1048576, size / 1048576)
def start(self):
@@ -178,7 +181,7 @@ class CustomInstall:
for c in self.cias:
self.log('Reading ' + c)
cia = CIAReader(c)
cia = CIAReader(c, seeddb=self.seeddb)
self.cia = cia
tid_parts = (cia.tmd.title_id[0:8], cia.tmd.title_id[8:16])
@@ -401,8 +404,9 @@ class CustomInstall:
for d in scandir(sd_path):
if d.is_dir() and len(d.name) == 32:
try:
# id1_tmp = bytes.fromhex(d.name)
pass
# check if the name can be converted to hex
# I'm not sure what the 3DS does if there is a folder that is not a 32-char hex string.
bytes.fromhex(d.name)
except ValueError:
continue
else:
@@ -441,12 +445,14 @@ if __name__ == "__main__":
parser.add_argument('cia', help='CIA files', nargs='+')
parser.add_argument('-m', '--movable', help='movable.sed file', required=True)
parser.add_argument('-b', '--boot9', help='boot9 file')
parser.add_argument('-s', '--seeddb', help='seeddb file')
parser.add_argument('--sd', help='path to SD root', required=True)
parser.add_argument('--skip-contents', help="don't add contents, only add title info entry", action='store_true')
args = parser.parse_args()
installer = CustomInstall(boot9=args.boot9,
seeddb=args.seeddb,
cias=args.cia,
movable=args.movable,
sd=args.sd,

BIN
docu/main.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -7,7 +7,6 @@
#include "basetik_bin.h"
#define CIFINISH_PATH "/cifinish.bin"
#define REQUIRED_VERSION 3
// 0x10
struct finish_db_header {
@@ -16,8 +15,30 @@ struct finish_db_header {
u32 title_count;
};
// 0x30
struct finish_db_entry_v1 {
u64 title_id;
u8 common_key_index; // unused by this program
bool has_seed;
u8 magic[6]; // "TITLE" and a null byte
u8 title_key[0x10]; // unused by this program
u8 seed[0x10];
};
// 0x20
struct finish_db_entry {
// this one was accidential since I mixed up the order of the members in the script
// and the finalize program, but a lot of users probably used the bad one so I need
// to support this anyway.
struct finish_db_entry_v2 {
u8 magic[6]; // "TITLE" and a null byte
u64 title_id;
bool has_seed;
u8 padding;
u8 seed[0x10];
} __attribute__((packed));
// 0x20
struct finish_db_entry_v3 {
u8 magic[6]; // "TITLE" and a null byte
bool has_seed;
u64 title_id;
@@ -31,6 +52,13 @@ struct ticket_dumb {
u8 unused2[0x16C];
} __attribute__((packed));
// the 3 versions are put into this struct
struct finish_db_entry_final {
bool has_seed;
u64 title_id;
u8 seed[0x10];
};
// from FBI:
// https://github.com/Steveice10/FBI/blob/6e3a28e4b674e0d7a6f234b0419c530b358957db/source/core/http.c#L440-L453
static Result FSUSER_AddSeed(u64 titleId, const void* seed) {
@@ -48,66 +76,154 @@ static Result FSUSER_AddSeed(u64 titleId, const void* seed) {
return ret;
}
int load_cifinish(char* path, struct finish_db_entry_final **entries)
{
FILE *fp;
struct finish_db_header header;
struct finish_db_entry_v1 v1;
struct finish_db_entry_v2 v2;
struct finish_db_entry_v3 v3;
struct finish_db_entry_final *tmp;
int i;
size_t read;
printf("Reading %s...\n", path);
fp = fopen(path, "rb");
if (!fp)
{
printf("Failed to open file. Does it exist?\n");
return -1;
}
fread(&header, sizeof(header), 1, fp);
if (memcmp(header.magic, "CIFINISH", 8))
{
printf("CIFINISH magic not found.\n");
goto fail;
}
printf("CIFINISH version: %lu\n", header.version);
if (header.version > 3)
{
printf("This version of custom-install-finalize is\n");
printf(" too old. Please update to a new release.\n");
goto fail;
}
*entries = calloc(header.title_count, sizeof(struct finish_db_entry_final));
if (!*entries) {
printf("Couldn't allocate memory.\n");
printf("This should never happen.\n");
goto fail;
}
tmp = *entries;
if (header.version == 1)
{
for (i = 0; i < header.title_count; i++)
{
read = fread(&v1, sizeof(v1), 1, fp);
if (read != 1)
{
printf("Couldn't read a full entry.\n");
printf(" Is the file corrupt?\n");
goto fail;
}
if (memcmp(v1.magic, "TITLE", 6))
{
printf("Couldn't find TITLE magic for entry.\n");
printf(" Is the file corrupt?\n");
goto fail;
}
tmp[i].has_seed = v1.has_seed;
tmp[i].title_id = v1.title_id;
memcpy(tmp[i].seed, v1.seed, 16);
}
} else if (header.version == 2) {
for (i = 0; i < header.title_count; i++)
{
read = fread(&v2, sizeof(v2), 1, fp);
if (read != 1)
{
printf("Couldn't read a full entry.\n");
printf(" Is the file corrupt?\n");
goto fail;
}
if (memcmp(v2.magic, "TITLE", 6))
{
printf("Couldn't find TITLE magic for entry.\n");
printf(" Is the file corrupt?\n");
goto fail;
}
tmp[i].has_seed = v2.has_seed;
tmp[i].title_id = v2.title_id;
memcpy(tmp[i].seed, v2.seed, 16);
}
} else if (header.version == 3) {
for (i = 0; i < header.title_count; i++)
{
read = fread(&v3, sizeof(v3), 1, fp);
if (read != 1)
{
printf("Couldn't read a full entry.\n");
printf(" Is the file corrupt?\n");
goto fail;
}
if (memcmp(v3.magic, "TITLE", 6))
{
printf("Couldn't find TITLE magic for entry.\n");
printf(" Is the file corrupt?\n");
goto fail;
}
tmp[i].has_seed = v3.has_seed;
tmp[i].title_id = v3.title_id;
memcpy(tmp[i].seed, v3.seed, 16);
}
}
return header.title_count;
fail:
fclose(fp);
return -1;
}
void finalize_install(void)
{
Result res;
Handle ticketHandle;
struct ticket_dumb ticket_buf;
FILE *fp;
struct finish_db_entry_final *entries;
int title_count;
struct finish_db_header header;
struct finish_db_entry *entries;
title_count = load_cifinish(CIFINISH_PATH, &entries);
if (title_count == -1)
{
free(entries);
return;
}
if (title_count == 0)
{
printf("No titles to finalize.\n");
free(entries);
return;
}
//printf("Deleting %s...\n", CIFINISH_PATH);
//unlink(CIFINISH_PATH);
memcpy(&ticket_buf, basetik_bin, basetik_bin_size);
printf("Reading %s...\n", CIFINISH_PATH);
fp = fopen(CIFINISH_PATH, "rb");
if (!fp)
for (int i = 0; i < title_count; ++i)
{
puts("Failed to open file.");
return;
}
fread(&header, sizeof(struct finish_db_header), 1, fp);
if (memcmp(header.magic, "CIFINISH", 8))
{
printf("CIFINISH magic not found in %s.\n", CIFINISH_PATH);
fclose(fp);
return;
}
if (header.version != REQUIRED_VERSION)
{
printf("\n%s was created with a different\n", CIFINISH_PATH);
printf(" version of custom-install than this one\n");
printf(" supports.\n\n");
printf("Make sure you are using the latest version of\n");
printf(" custom-install and custom-install-finalize\n");
printf(" from the repository on GitHub.\n\n");
printf("When you run the script again, you can use\n");
printf(" --skip-contents to avoid re-writing the title\n");
printf(" contents, so only the Title Database and\n");
printf(" cifinish.bin will be modified.\n\n");
printf("Expected version %i, got %li\n", REQUIRED_VERSION, header.version);
fclose(fp);
return;
}
entries = calloc(header.title_count, sizeof(struct finish_db_entry));
fread(entries, sizeof(struct finish_db_entry), header.title_count, fp);
fclose(fp);
printf("Deleting %s...\n", CIFINISH_PATH);
unlink(CIFINISH_PATH);
for (int i = 0; i < header.title_count; ++i)
{
// this includes the null byte
if (memcmp(entries[i].magic, "TITLE", 6))
{
puts("Couldn't find TITLE magic for entry, skipping.");
continue;
}
printf("Finalizing %016llx...\n", entries[i].title_id);
ticket_buf.title_id_be = __builtin_bswap64(entries[i].title_id);
@@ -156,14 +272,16 @@ void finalize_install(void)
int main(int argc, char* argv[])
{
amInit();
sdmcInit();
gfxInitDefault();
consoleInit(GFX_TOP, NULL);
puts("custom-install-finalize v1.2");
printf("custom-install-finalize v1.4\n");
finalize_install();
puts("\nPress START or B to exit.");
// print this at the end in case it gets pushed off the screen
printf("\nRepository:\n");
printf(" https://github.com/ihaveamac/custom-install\n");
printf("\nPress START or B to exit.\n");
// Main loop
while (aptMainLoop())
@@ -179,7 +297,6 @@ int main(int argc, char* argv[])
}
gfxExit();
sdmcExit();
amExit();
return 0;
}

View File

@@ -1,199 +0,0 @@
from custominstall import CustomInstall
import tkinter as tk
from tkinter.filedialog import askopenfilenames
from tkinter.ttk import Progressbar
import os
import datetime
import threading
import queue
class CustomInstallGui(tk.Frame):
def __init__(self, master=None):
tk.Frame.__init__(self, master)
self.master = master
# Title name for window
self.window_title = "Custom-Install GUI"
# Config
self.skip_contents = False
self.cias = []
self.boot9 = None
self.movable = None
self.sd = None
self.skip_cont_var = tk.IntVar(self)
for x in range(8):
tk.Grid.rowconfigure(self, x, weight=1)
for x in range(1):
tk.Grid.columnconfigure(self, x, weight=1)
def set_cias(self, filename):
self.cias = filename
def set_boot9(self, filename):
self.boot9 = filename
def set_movable(self, filename):
self.movable = filename
def set_sd(self, directory):
self.sd = directory
def start_install(self, event):
self.progress['value'] = 0
error = False
# Checks
if len(self.cias) == 0:
self.add_log_msg("Error: Please select CIA file(s)")
error = True
if self.boot9 == None:
self.add_log_msg("Error: Please add your boot9 file")
error = True
if self.movable == None:
self.add_log_msg("Error: Please add your movable file")
if self.sd == None:
self.add_log_msg("Error: Please locate your SD card directory")
self.add_log_msg("Note: Linux usually mounts to /media/")
if error:
self.add_log_msg("--- Errors occured, aborting ---")
return False
# Start the job
if self.skip_cont_var.get() == 1: self.skip_contents = True
else: self.skip_contents = False
print(f'{self.cias}\n{self.boot9}\n{self.movable}\n{self.skip_contents}')
self.log.insert(tk.END, "Starting install...\n")
installer = CustomInstall(boot9=self.boot9,
movable=self.movable,
cias=self.cias,
sd=self.sd,
skip_contents=self.skip_contents)
# DEBUG
# self.debug_values()
def start_install():
def log_handle(message, end=None): self.add_log_msg(message)
def percentage_handle(percent, total_read, size): self.progress['value'] = percent
installer.event.on_log_msg += log_handle
installer.event.update_percentage += percentage_handle
installer.start()
print('--- Script is done ---')
t = threading.Thread(target=start_install)
t.start()
def debug_values(self):
self.add_log_msg(self.boot9)
self.add_log_msg(self.movable)
self.add_log_msg(self.cias)
self.add_log_msg(self.sd)
self.add_log_msg(self.skip_contents)
def start(self):
self.master.title(self.window_title)
self.pack(fill=tk.BOTH, expand=True)
self.log = tk.Text(self, height=10, width=40)
install = tk.Button(self, text="Install CIA")
skip_checkbox = tk.Checkbutton(self, text="Skip Contents", variable=self.skip_cont_var)
self.progress = Progressbar(self, orient=tk.HORIZONTAL, length=100, mode='determinate')
# File pickers
cia_picker = self.filepicker_option("CIA file(s)", True, self.set_cias)
boot9_picker = self.filepicker_option("Select boot9.bin...", False, self.set_boot9)
movable_picker = self.filepicker_option("Select movable.sed...", False, self.set_movable)
sd_picker = self.filepicker_option("Select SD card...", False, self.set_sd, True)
# Place widgets
self.log.grid(column=0, row=0, sticky=tk.N+tk.E+tk.W)
self.progress.grid(column=0, row=1, sticky=tk.E+tk.W)
sd_picker.grid(column=0, row=2, sticky=tk.E+tk.W)
boot9_picker.grid(column=0, row=3, sticky=tk.E+tk.W)
movable_picker.grid(column=0, row=4, sticky=tk.E+tk.W)
cia_picker.grid(column=0, row=5, sticky=tk.E+tk.W)
skip_checkbox.grid(column=0, row=6, sticky=tk.E+tk.W)
install.grid(column=0, row=7, sticky=tk.S+tk.E+tk.W)
# Events
install.bind('<Button-1>', self.start_install)
# Just a greeting :)
now = datetime.datetime.now()
time_short = "day!"
if now.hour < 12: time_short = "morning!"
elif now.hour > 12: time_short = "afternoon!"
self.add_log_msg(f'Good {time_short} Please pick your boot9, movable.sed, SD, and CIA file(s).\n---\nPress "Install CIA" when ready!')
def add_log_msg(self, message):
self.log.insert(tk.END, str(message)+"\n")
self.log.see(tk.END)
def filepicker_option(self, title, multiple_files, on_file_add, dir_only=False):
frame = tk.Frame(self)
browse_button = tk.Button(frame, text="Pick file")
filename_label = tk.Label(frame, text=title, wraplength=200)
browse_button.grid(column=0, row=0)
filename_label.grid(column=1, row=0)
# Wrapper for event
def file_add(event):
if dir_only:
folder = tk.filedialog.askdirectory()
if not folder:
return False
dir = os.path.basename(folder)
filename_label.config(text="SD => "+dir)
on_file_add(folder)
return True
# Returns multiple files in a tuple
filename = (tk.filedialog.askopenfilenames()
if multiple_files else
tk.filedialog.askopenfilename())
# User may select "cancel"
if not filename:
return False
if multiple_files:
basename = os.path.basename(filename[0])
if len(filename) <= 1:
more = ""
elif len(filename) > 1:
more = " + "+str(len(filename))+" more"
else:
basename = os.path.basename(filename)
more = ""
filename_label.config(text=basename+more)
# Runs callback provided
on_file_add(filename)
browse_button.bind('<Button-1>', file_add)
return frame
root = tk.Tk()
app = CustomInstallGui(root)
app.start()
root.mainloop()

435
gui.py Normal file
View File

@@ -0,0 +1,435 @@
# 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
# BUTTON_COLOR =
# BUTTON_HIGHLIGHT_COLOR =
# BUTTON_FONT =
# Custom button
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")
# -------------------------------------------------
cia_container = themedFrame(outer_frame, borderwidth=0, highlightthickness=0)
cia_container.place(y=90, relwidth=1, height=115)
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=92, 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=92, 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=92, 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=205, 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=230)
self.console = ScrolledText(outer_frame, background="black", foreground="white", highlightthickness=0)
self.console.place(relwidth=1, relheight=1, y=250, 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])
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', 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"""
try:
args = [sys.executable, '-u', os.path.join(os.path.dirname(__file__), "custominstall.py")]
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 - {argstring} | Exception - {e}\n")
t = threader_object()
window = gui(t)
window.mainloop()

View File

@@ -1,3 +1,3 @@
pycryptodomex==3.9.4
pycryptodomex<=3.9.4
events==0.3
pyctr==0.1.0

18
style.py Normal file
View File

@@ -0,0 +1,18 @@
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"