/*
* This file is part of fastboot 3DS
* Copyright (C) 2017 derrek, profi200
*
* 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 "types.h"
#include "mem_map.h"
#include "fatfs/ff.h"
#include "arm9/console.h"
#include "arm9/main.h"
#include "arm9/menu.h"
#include "arm9/interrupt.h"
#include "arm9/dev.h"
#include "util.h"
#include "arm9/hid.h"
#define MAX_CACHED_ENTRIES 0x400
#define MAX_ENTRY_SIZE FF_MAX_LFN + 2
#define MAX_NEW_ENTRIES 0x20
#define MAX_PATH_LENGTH 0x400
#define MAX_PATH_DEPTH 0x20
typedef struct {
char name[MAX_ENTRY_SIZE];
bool isDir;
} EntryType;
static EntryType *dirEntries; // [MAX_ENTRY_SIZE][MAX_CACHED_ENTRIES];
static DIR curDirectory;
static char curPath[MAX_PATH_LENGTH];
static u32 curEntriesCount;
// This stack is used to recover the original cursor position
// in the dir entries list when returning from a subdir.
static u32 indexStack[MAX_PATH_DEPTH];
static u32 *indexStackPtr = indexStack;
/* This scans the current directory for entries.
Only MAX_NEW_ENTRIES will be added at maximum.
This also updates the curEntriesCount.
Returns true if new entries were added. */
static bool scanDirectory()
{
FRESULT res;
static FILINFO fno;
unsigned i = 0;
for(i=0; i<MAX_NEW_ENTRIES; i++)
{
if(curEntriesCount + i == MAX_CACHED_ENTRIES) break;
res = f_readdir(&curDirectory, &fno);
if(res != FR_OK || fno.fname[0] == 0) break;
dirEntries[curEntriesCount + i].isDir = (fno.fattrib & AM_DIR) != 0;
if(fno.fattrib & AM_DIR) // directory
snprintf(dirEntries[curEntriesCount + i].name, FF_MAX_LFN + 1, "/%s", fno.fname);
else // file
strncpy_s(dirEntries[curEntriesCount + i].name, fno.fname, FF_MAX_LFN + 1, FF_MAX_LFN + 2);
}
curEntriesCount += i;
return i != 0;
}
static u32 ascendPath()
{
char *end = curPath + strlen(curPath);
for(; end != curPath + 1; end--)
if(*end == '/' || *(end - 1) == ':') break;
*end = '\0';
if(indexStackPtr != indexStack) indexStackPtr--;
return *indexStackPtr;
}
static bool descendPath(u32 savedIndex, char *subdir)
{
if(indexStackPtr < indexStack + MAX_PATH_DEPTH)
*indexStackPtr++ = savedIndex;
else return false; // out of stack
strncat(curPath, subdir, sizeof(curPath) - strlen(curPath) + 1);
return true;
}
static void formatEntry(char *out, const char* in, const u32 bufSize, bool shortenEnd)
{
const char *end;
const u32 entryLen = strlen(in);
if(!shortenEnd)
{
if(entryLen > bufSize - 1)
{
end = in + entryLen;
strcpy(&out[3], end - bufSize + 3);
memcpy(out, "...", 3);
}
else strcpy(out, in);
}
else
{
if(entryLen > bufSize - 1)
{
memcpy(out, in, bufSize - 4);
memcpy(&out[bufSize - 5], "...", 4);
}
else strcpy(out, in);
}
}
static void updateCursor(int cursor_pos, int old_cursor_pos, int maxEntries)
{
// clear old '*' char
consoleSetCursor(&con_bottom, 0, (old_cursor_pos % (maxEntries)) + 1);
uiPrintInfo(" ");
consoleSetCursor(&con_bottom, 0, (cursor_pos % (maxEntries)) + 1);
uiPrintInfo("*");
}
static void updateScreen(int cursor_pos, bool dirChange)
{
// we don't want ugly line breaks in our list
const int maxLen = con_bottom.windowWidth - 1;
const int maxEntries = con_bottom.windowHeight - 1;
static int old_cursor_pos;
consoleSelect(&con_bottom);
if(!dirChange && (cursor_pos < maxEntries) && (old_cursor_pos < maxEntries))
{
updateCursor(cursor_pos, old_cursor_pos, maxEntries);
old_cursor_pos = cursor_pos;
return;
}
old_cursor_pos = cursor_pos;
char entry [maxLen]; // should be safe
unsigned start, end;
consoleClear();
// print the current path in the first line
formatEntry(entry, curPath, (u32) maxLen - 3, false);
uiPrintInfo("%s :", entry);
if(cursor_pos < (int) maxEntries)
{
start = 0;
end = min(maxEntries, curEntriesCount);
}
else
{
start = cursor_pos - maxEntries + 1;
end = min(cursor_pos + 1, curEntriesCount);
}
for(unsigned i = start; i < end; i++)
{
formatEntry(entry, dirEntries[i].name, (u32) maxLen - 2, true);
uiPrintInfo("\n%s %s", i == (unsigned) cursor_pos ? "*" : " ", entry);
}
}
const char *browseForFile(const char *basePath)
{
FRESULT res;
u32 keys;
u32 cursor_pos = 0;
bool fileSelected = false;
int returnState = STATE_PREVIOUS;
curEntriesCount = 0;
indexStackPtr = indexStack;
uiClearConsoles();
dirEntries = (EntryType *) malloc(MAX_CACHED_ENTRIES * sizeof(EntryType));
if(!dirEntries)
{
menuSetReturnToState(STATE_PREVIOUS);
menuUpdateGlobalState();
menuActState();
return NULL;
}
if(basePath)
strncpy_s(curPath, basePath, sizeof(curPath) - 1, sizeof(curPath));
else
strncpy_s(curPath, "sdmc:", sizeof(curPath) - 1, sizeof(curPath));
/* check if we have to wait for the user */
if(!dev_sdcard->is_active())
{
menuPrintPrompt("Waiting for SD card to be inserted...\nPress B to exit.\n");
do {
hidScanInput();
keys = hidKeysDown();
if(keys & KEY_B)
goto fail;
switch(menuUpdateGlobalState())
{
case MENU_EVENT_HOME_PRESSED:
case MENU_EVENT_POWER_PRESSED:
goto fail;
default:
break;
}
menuActState();
} while(!dev_sdcard->is_active());
uiClearConsoles();
}
res = f_opendir(&curDirectory, curPath);
if (res != FR_OK)
{
menuPrintPrompt("Failed to enter directory.\n");
menuWaitForAnyPadkey();
free(dirEntries);
menuSetReturnToState(STATE_PREVIOUS);
menuUpdateGlobalState();
menuActState();
return NULL;
}
// do an initial scan
scanDirectory();
updateScreen((int)cursor_pos, true);
for(;;)
{
hidScanInput();
keys = hidKeysDown()/* | hidKeysHeld()*/;
if(curEntriesCount != 0)
{
if(keys & KEY_DUP || keys & KEY_DLEFT)
{
if(cursor_pos != 0)
{
cursor_pos -= keys & KEY_DLEFT ? min(cursor_pos, 8) : 1;
updateScreen((int)cursor_pos, false);
}
}
else if(keys & KEY_DDOWN || keys & KEY_DRIGHT)
{
// we reached the end of our entries list?
u32 old_pos = cursor_pos;
cursor_pos += keys & KEY_DRIGHT ? 8 : 1;
if(cursor_pos >= curEntriesCount)
{
scanDirectory();
// we got more entries?
if(curEntriesCount > cursor_pos)
cursor_pos = min(cursor_pos, curEntriesCount - 1);
else
cursor_pos = curEntriesCount - 1;
if(cursor_pos != old_pos)
updateScreen((int)cursor_pos, false);
}
else
{
updateScreen((int)cursor_pos, false);
}
}
else if(keys & KEY_A) // select entry
{
if(dirEntries[cursor_pos].isDir) // it's a dir
{
if(descendPath(cursor_pos, dirEntries[cursor_pos].name))
{
f_closedir(&curDirectory);
res = f_opendir(&curDirectory, curPath);
if (res != FR_OK)
ascendPath();
else
{
// fetch new entries
curEntriesCount = 0;
scanDirectory();
cursor_pos = 0;
updateScreen((int)cursor_pos, true);
}
}
}
else // we got our file!
{
if(strlen(dirEntries[cursor_pos].name) + strlen(curPath) + 2 > sizeof(curPath))
goto fail; // TODO? Path too long.
strcat(curPath, "/");
strcat(curPath, dirEntries[cursor_pos].name);
fileSelected = true;
menuSetReturnToState(STATE_PREVIOUS);
}
}
}
if(keys & KEY_B) // go back
{
if(indexStackPtr != indexStack)
{
cursor_pos = ascendPath();
f_closedir(&curDirectory);
res = f_opendir(&curDirectory, curPath);
if (res != FR_OK)
goto fail;
curEntriesCount = 0;
do {
if(!scanDirectory())
goto fail;
} while(curEntriesCount <= cursor_pos);
updateScreen((int)cursor_pos, true);
}
else
menuSetReturnToState(STATE_PREVIOUS);
}
switch(menuUpdateGlobalState())
{
case MENU_EVENT_HOME_PRESSED:
returnState = MENU_STATE_MAIN;
goto fail;
case MENU_EVENT_POWER_PRESSED:
case MENU_EVENT_SD_CARD_REMOVED:
goto fail;
case MENU_EVENT_STATE_CHANGE:
if(!fileSelected) goto fail;
else goto done;
default:
break;
}
menuActState();
}
done:
f_closedir(&curDirectory);
free(dirEntries);
menuSetReturnToState(returnState);
menuActState();
return curPath;
fail:
f_closedir(&curDirectory);
free(dirEntries);
menuSetReturnToState(returnState);
menuActState();
return NULL;
}