Newer
Older
uwuboot / source / arm11 / menu / menu_fsel.c
@profi200 profi200 on 26 Apr 2019 12 KB HID refactor.
/*
 *   This file is part of fastboot 3DS
 *   Copyright (C) 2017 derrek, profi200, d0k3
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "types.h"
#include "fs.h"
#include "util.h"
#include "fsutils.h"
#include "arm11/menu/menu_fsel.h"
#include "arm11/menu/menu_util.h"
#include "arm11/menu/menu_color.h"
#include "arm11/hardware/hid.h"
#include "arm11/hardware/timer.h"
#include "arm11/config.h"
#include "arm11/console.h"
#include "arm11/debug.h"
#include "arm11/fmt.h"

#define MAX_DIR_ENTRIES	0x100 // 256 -> worst case approx 64kiB (yes, this is limited)
#define N_DIR_READ		0x10  // 16 at a time

typedef struct {
	u32 fsize;		// size of the file
	u8  is_dir;		// > 0 if is directory
	char* fname;	// filename (handle via malloc)
} DirBufferEntry;



// inspired by http://www.geeksforgeeks.org/wildcard-character-matching/
static bool matchName(const char* path, const char* pattern)
{
	// handling non asterisk chars
	for (; *pattern != '*'; pattern++, path++)
	{
		if ((*pattern == '\0') && (*path == '\0'))
		{
			// end reached simultaneously, match found
			return true;
		}
		else if ((*pattern == '\0') || (*path == '\0'))
		{
			// end reached on only one, failure
			return false;
		}
		else if ((*pattern != '?') && (tolower(*pattern) != tolower(*path)))
		{
			// chars don't match, failure
			return false;
		}
	}
	
	// handling the asterisk (matches any # of chars in path)
	if ((*(pattern+1) == '?') || (*(pattern+1) == '*'))
	{
		// stupid user/dev shenanigans, failure
		return false;
	}
	else if (*(pattern+1) == '\0')
	{
		// nothing after the asterisk, match found
		return true;
	}
	else
	{
		// we couldn't really go without recursion here
		for (; *path != '\0'; path++)
		{
			if (matchName(path, pattern + 1) == true) return true;
		}
	}
	
	return false;
}


static void freeDirBufferContent(DirBufferEntry* dir_buffer, s32 n_entries)
{
	for(s32 i = 0; i < n_entries; i++)
	{
		if(dir_buffer[i].fname == NULL)
			break; // this should never happen
		
		free (dir_buffer[i].fname);
		dir_buffer[i].fname = NULL;
	}
}


static void sortDirBuffer(DirBufferEntry* dir_buffer, s32 n_entries)
{
	for(s32 s = 0; s < n_entries; s++)
	{
		DirBufferEntry* cmp0 = &(dir_buffer[s]);
		DirBufferEntry* min0 = cmp0;
		
		for(s32 c = s + 1; c < n_entries; c++)
		{
			DirBufferEntry* cmp1 = &(dir_buffer[c]);
			if(min0->is_dir != cmp1->is_dir)
			{
				if (cmp1->is_dir)
					min0 = cmp1;
				continue;
			}
			if(strnicmp(min0->fname, cmp1->fname, FF_MAX_LFN + 1) > 0)
			{
				min0 = cmp1;
			}
		}
		
		if(min0 != cmp0)
		{
			DirBufferEntry swap; // swap entries
			memcpy(&swap, cmp0, sizeof(DirBufferEntry));
			memcpy(cmp0, min0, sizeof(DirBufferEntry));
			memcpy(min0, &swap, sizeof(DirBufferEntry));
		}
	}
}


static s32 readDirToBuffer(DirBufferEntry* dir_buffer, const char* path, const char* pattern)
{
	s32 n_entries = 0;
	
	// set dir_buffer to all zeroes
	memset(dir_buffer, 0, sizeof(DirBufferEntry) * MAX_DIR_ENTRIES);
	
	// special handling when in root
	if(!*path)
	{
		const char* root_paths[] = { "sdmc:", "twln:", "twlp:", "nand:" };
		const char* firm_paths[] = { "firm1:" };
		const u32 firm_size = 0x400000; // 4MB
		bool devmode = configDevModeEnabled();
		
		for(u32 i = 0; i < sizeof(root_paths) / sizeof(const char*); i++)
		{
			if (!fsEnsureMounted(root_paths[i]))
			 	continue;
			
			dir_buffer[n_entries].fsize = 0;
			dir_buffer[n_entries].is_dir = 1;
			dir_buffer[n_entries].fname = strdup(root_paths[i]);
			n_entries++;
		}
		
		if(devmode)
		{
			for(u32 i = 0; i < sizeof(firm_paths) / sizeof(const char*); i++)
			{
				if (!pattern || !matchName(firm_paths[i], pattern))
					continue;
				
				dir_buffer[n_entries].fsize = firm_size;
				dir_buffer[n_entries].is_dir = 0;
				dir_buffer[n_entries].fname = strdup(firm_paths[i]);
				n_entries++;
			}
		}
		
		return n_entries;
	}

	// open directory
	s32 dhandle = fOpenDir(path);
	if(dhandle < 0) return -1;
    
	FsFileInfo* finfo = (FsFileInfo*) malloc(N_DIR_READ * sizeof(FsFileInfo));
	if (!finfo) // out of memory
	{
		fCloseDir(dhandle);
		return -1; 
	}
        
	s32 n_read = 0;
	while((n_read = fReadDir(dhandle, finfo, N_DIR_READ)) != 0)
	{
		if (n_read < 0) // error reading dir
			goto fail;
		
		for(s32 i = 0; i < n_read; i++)
		{
			if(!(finfo[i].fattrib & AM_DIR) && (!pattern || !matchName(finfo[i].fname, pattern)))
				continue; // not a match with the provided pattern and not a dir
			
			// take over data
			dir_buffer[n_entries].fsize = finfo[i].fsize;
			dir_buffer[n_entries].is_dir = finfo[i].fattrib & AM_DIR;
			dir_buffer[n_entries].fname = strdup(finfo[i].fname);
			
			// check for out of memory
			if (!dir_buffer[n_entries].fname)
				goto fail;
			
			// max dir buffer size reached?
			if (++n_entries >= MAX_DIR_ENTRIES)
				break;
		}
		
		// max dir buffer size reached?
		if (n_entries >= MAX_DIR_ENTRIES)
			break; // WARNING: no errors here - list just isn't complete
	}
	
	fCloseDir(dhandle);
	free(finfo);
	
	if (n_entries > 0)
		sortDirBuffer(dir_buffer, n_entries);
	
	return n_entries;
	
	fail:
	
	fCloseDir(dhandle);
	free(finfo);
	
	return -1;	
}


/**
 * @brief Draws the file listing to the given console.
 * @param curr_path Current path displayed on screen.
 * @param dir_buffer An array of structs containing the file listing.
 * @param n_entries Number of elements in the above struct.
 * @param menu_con Console that the file browser is displayed on.
 * @param index Current placement of the cursor.
 * @param scroll Current scroll offset, will be written to by this function.
 */
void browserDraw(const char* curr_path, DirBufferEntry* dir_buffer, s32 n_entries, PrintConsole* menu_con, s32 index, s32* scroll)
{
	int brws_x = (menu_con->windowWidth - BRWS_WIDTH) >> 1;
	int brws_y = BRWS_OFFSET_TITLE;
	char temp_str[BRWS_WIDTH + 1];
	char byte_str[32];
	
	// fix scroll (if required)
	if (index < *scroll)
	{
		*scroll = index;
	} else if (*scroll + BRWS_MAX_ENTRIES <= index)
	{
		*scroll = index - (BRWS_MAX_ENTRIES - 1);
	}
	
	// select menu console
	consoleSelect(menu_con);
	consoleClear();
	
	// browser title
	consoleSetCursor(menu_con, brws_x, brws_y++);
	truncateString(temp_str, *curr_path ? curr_path : "root", BRWS_WIDTH, 8);
	ee_printf(ESC_SCHEME_ACCENT1 "%-*s" ESC_RESET, BRWS_WIDTH, temp_str);
	brws_y++;
	
	// menu entries
	for (s32 i = 0; i < BRWS_MAX_ENTRIES; i++)
	{
		s32 pos = i + *scroll;
		if (pos >= n_entries)
			break;
		
		DirBufferEntry* entry = &(dir_buffer[pos]);
		bool is_selected = (pos == index);
		
		if(!entry->is_dir)
			formatBytes(byte_str, entry->fsize);
		else if (!*curr_path)
			strncpy(byte_str, (strncmp(entry->fname, "sdmc:", 5+1) == 0) ? "(SD Card)" : "(System)", 31);
		else
			strncpy(byte_str, "(DIR)", 31);
		
		consoleSetCursor(menu_con, brws_x, brws_y++);
		truncateString(temp_str, entry->fname, BRWS_WIDTH-13, 8);
		ee_printf(entry->is_dir ? ESC_SCHEME_WEAK : ESC_SCHEME_STD);
		if (is_selected)ee_printf(ESC_INVERT);
		ee_printf(" %-*.*s %10.10s ", BRWS_WIDTH-13, BRWS_WIDTH-13, temp_str, byte_str);
		ee_printf(ESC_RESET);
	}
	
	// button instructions
	brws_y = BRWS_OFFSET_BUTTONS;
	ee_printf(ESC_SCHEME_WEAK);
	consoleSetCursor(menu_con, brws_x, brws_y++);
	ee_printf("%-*.*s", BRWS_WIDTH, BRWS_WIDTH, "[A]:Choose [B]:Back");
	consoleSetCursor(menu_con, brws_x, brws_y++);
	ee_printf("%-*.*s", BRWS_WIDTH, BRWS_WIDTH, "[HOME] to cancel");
	ee_printf(ESC_RESET);
}


/**
 * @brief Shows a file select dialogue.
 * @param res_path The selected path will end up here.
 * @param menu_con Console that the file browser is displayed on.
 * @param start Starting path (must be a dir).
 * @param pattern Only files matching this wildcard pattern will be displayed.
 */
bool menuFileSelector(char* res_path, PrintConsole* menu_con, const char* start, const char* pattern, bool allow_root, bool select_dirs)
{
	DirBufferEntry* dir_buffer;
	bool result = true; // <--- should be handled differently 
	
	dir_buffer = (DirBufferEntry*) malloc(MAX_DIR_ENTRIES * sizeof(DirBufferEntry));
	if (!dir_buffer) return false;
	
	// res_path has to be at least 256 byte long (including '\0') and
	// is also used as temporary buffer
	*res_path = '\0'; // root dir if start is NULL
	if(start) strncpy(res_path, start, FF_MAX_LFN + 1);
    
    // handle allow_root
    if (!allow_root && (strncmp(res_path, "sdmc:", 5) != 0))
         strncpy(res_path, "sdmc:", FF_MAX_LFN + 1);
	
	// check res_path, fix if required
	char* lastname = NULL;
	s32 dhandle;
	// is this a dir?
	if ((dhandle = fOpenDir(res_path)) >= 0)
	{
		fCloseDir(dhandle);
	}
	// not a dir -> must be a file
	else if (fStat(res_path, NULL) == FR_OK)
	{
		lastname = strrchr(res_path, '/');
		if (!lastname)
			panicMsg("Invalid path");
		*(lastname++) = '\0';
		
		dhandle = fOpenDir(res_path);
		if (dhandle < 0)
			panicMsg("Filesystem corruption");
		fCloseDir(dhandle);
	}
	// neither file nor dir
	else
	{
		*res_path = '\0';
	}
	
	bool is_dir = true; // we are not finished while we have a dir in res_path
	while(is_dir && result)
	{
		s32 n_entries = readDirToBuffer(dir_buffer, res_path, pattern);
		s32 last_index = (u32) -1;
		s32 scroll = 0;
		s32 index = 0;
		
		if (n_entries < 0)
		{
			if (*res_path)
			{
				// return to root if error encountered
				*res_path = '\0';
				lastname = NULL;
				continue;
			} else panicMsg("Root filesystem failure!");
		}
		
		// find lastname in listing
		if (lastname)
		{
			for (s32 i = 0; i < n_entries; i++)
			{
				if (strncmp(dir_buffer[i].fname, lastname, FF_MAX_LFN + 1) == 0)
				{
					index = i;
					break;
				}
			}
			lastname = NULL;
		}
		
		u32 dbutton_cooldown = 0;
		while(result)
		{
			// update file browser (on demand)
			if (index != last_index) {
				browserDraw(res_path, dir_buffer, n_entries, menu_con, index, &scroll);
				last_index = index;
				updateScreens(); // update screens (VBlank included)
			} else GFX_waitForEvent(GFX_EVENT_PDC0, true); // VBlank
			
			// directional button cooldown
			for (u32 i = dbutton_cooldown; i > 0; i--)
			{
				hidScanInput();
				GFX_waitForEvent(GFX_EVENT_PDC0, true); // VBlank
				if (!(hidKeysHeld() & (KEY_DDOWN|KEY_DUP|KEY_DLEFT|KEY_DRIGHT)))
					break;
			}
			dbutton_cooldown = 0;
			
			// handle power button
			if(hidGetExtraKeys(0) & (KEY_POWER | KEY_POWER_HELD))
				result = false;
			
			hidScanInput();
			const u32 kDown = hidKeysDown();
			const u32 kHeld = hidKeysHeld();
			const u32 extraKeys = hidGetExtraKeys(0);
			
			if (extraKeys & KEY_SHELL)
			{
				sleepmode();
			}
			else if (extraKeys & KEY_HOME)
			{
				result = false;
			}
			else if ((kDown & (KEY_A|KEY_X)) && n_entries)
			{
				// build new res_path
				char* name = &(res_path[strlen(res_path)]);
				if (name > res_path) *(name++) = '/';
				strncpy(name, dir_buffer[index].fname, ((FF_MAX_LFN + 1) - (name - res_path)));
				
				// is this a dir? (override when X is detected)
				is_dir = !(select_dirs && (kDown & KEY_X)) && dir_buffer[index].is_dir;
				
				lastname = NULL;
				break;
			}
			else if ((kDown & KEY_X) && !n_entries && select_dirs)
			{
				// override when inside a empty DIR in dirselect mode
				is_dir = false;
				lastname = NULL;
				break;
			}
			else if (kDown & KEY_B)
			{
				// if in root: return to menu
				if (!*res_path)
				{
					result = false;
					break;
				}
				
				// if not in root: return to previous path
				// (the name of this path will be stored in lastname)
				lastname = strrchr(res_path, '/');
				
				if (lastname)
					*(lastname++) = '\0';
				else if (allow_root)
					*res_path = '\0';
				else
					result = false;
				break;
			}
			else if (kHeld & (KEY_DDOWN|KEY_DUP|KEY_DLEFT|KEY_DRIGHT) && n_entries)
			{
				if (kHeld & KEY_DDOWN)
				{
					// cursor down
					index = (index == n_entries - 1) ? 0 : index + 1;
				}
				else if (kHeld & KEY_DUP)
				{
					// cursor up
					index = (index == 0) ? n_entries - 1 : index - 1;
				}
				else if (kHeld & KEY_DRIGHT)
				{
					// cursor down a page
					index += BRWS_MAX_ENTRIES;
					if (index >= n_entries) index = n_entries - 1;
				}
				else if (kHeld & KEY_DLEFT)
				{
					// cursor up a page
					index -= BRWS_MAX_ENTRIES;
					if (index < 0) index = 0;
				}
				
				// set directional button cooldown
				dbutton_cooldown = 10;
			}
		}
		
		freeDirBufferContent(dir_buffer, n_entries);
	}
	
	free(dir_buffer);
	
	return result;
}