mirror of
https://github.com/ihaveamac/custom-install.git
synced 2026-01-21 14:06:02 +00:00
Compare commits
28 Commits
module-rew
...
finalize-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
272cc544cd | ||
|
|
443498d706 | ||
|
|
17404231d3 | ||
|
|
393fd03da1 | ||
|
|
26c21137ec | ||
|
|
9c1709922a | ||
|
|
43ae023000 | ||
|
|
e7c6ff7344 | ||
|
|
5c41f03784 | ||
|
|
4693935d87 | ||
|
|
11cbbcdf1e | ||
|
|
8f4b3d1134 | ||
|
|
d5a4cbd8f8 | ||
|
|
625f1f9db5 | ||
|
|
61b27f33ed | ||
|
|
13d0cbd796 | ||
|
|
14e5692cac | ||
|
|
b799e3af1a | ||
|
|
e8787a2d9a | ||
|
|
a08654160a | ||
|
|
6922c0d209 | ||
|
|
29e17bec6d | ||
|
|
8ee248c793 | ||
|
|
12d59cad5d | ||
|
|
ff196a667d | ||
|
|
f21b63f9dd | ||
|
|
c0e1d45054 | ||
|
|
0195ea75d4 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -15,3 +15,6 @@ venv/
|
||||
|
||||
# JetBrains
|
||||
.idea/
|
||||
=======
|
||||
|
||||
*.pyc
|
||||
|
||||
23
README.md
23
README.md
@@ -1,3 +1,5 @@
|
||||
[]() 
|
||||
|
||||
# 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 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).
|
||||
|
||||
@@ -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
BIN
docu/main.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -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,146 @@ 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;
|
||||
|
||||
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 (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;
|
||||
}
|
||||
entries[i]->has_seed = v1.has_seed;
|
||||
entries[i]->title_id = v1.title_id;
|
||||
memcpy(entries[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;
|
||||
}
|
||||
entries[i]->has_seed = v2.has_seed;
|
||||
entries[i]->title_id = v2.title_id;
|
||||
memcpy(entries[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;
|
||||
}
|
||||
entries[i]->has_seed = v3.has_seed;
|
||||
entries[i]->title_id = v3.title_id;
|
||||
memcpy(entries[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 +264,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.3\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 +289,6 @@ int main(int argc, char* argv[])
|
||||
}
|
||||
|
||||
gfxExit();
|
||||
sdmcExit();
|
||||
amExit();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -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
435
gui.py
Normal 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):
|
||||
argstring = ""
|
||||
|
||||
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")
|
||||
argstring += f"-b {boot9} "
|
||||
|
||||
sed = self.sed_box.get()
|
||||
if not sed:
|
||||
self.output_to_console("Failed to run - No movable.sed selected.\n")
|
||||
return
|
||||
argstring += f"-m {sed} "
|
||||
|
||||
sd = self.sd_box.get().strip()
|
||||
if not sd:
|
||||
self.output_to_console("Failed to run - SD path not selected.\n")
|
||||
return
|
||||
argstring += f"--sd {sd} "
|
||||
|
||||
cias = []
|
||||
for i in range(0, self.cia_box.size()):
|
||||
cias.append(self.cia_box.get(i).strip())
|
||||
for cia in cias:
|
||||
argstring += f" {cia}"
|
||||
|
||||
if self.skip_contents.get():
|
||||
argstring += "--skip-contents "
|
||||
|
||||
print(f"Running custom-install.py with args {args}\n")
|
||||
|
||||
self.threader.do_async(execute_script, [argstring, 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(argstring, 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")]
|
||||
for arg in argstring.split():
|
||||
args.append(arg.strip())
|
||||
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()
|
||||
@@ -1,3 +1,3 @@
|
||||
pycryptodomex==3.9.4
|
||||
pycryptodomex<=3.9.4
|
||||
events==0.3
|
||||
pyctr==0.1.0
|
||||
|
||||
18
style.py
Normal file
18
style.py
Normal 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"
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user