Initial commit

This commit is contained in:
Niklas080208
2026-02-11 20:33:01 +01:00
commit 617265f004
145 changed files with 45252 additions and 0 deletions

47
src/about_tab.cpp Normal file
View File

@@ -0,0 +1,47 @@
/*
SWR INI Tool - Switchroot INI Configuration Editor
Copyright (C) 2026 Switchroot
*/
#include "about_tab.h"
#include "logo.h"
AboutTab::AboutTab()
{
// Logo
this->addView(new Logo(LogoStyle::ABOUT));
// Subtitle
brls::Label *subTitle = new brls::Label(
brls::LabelStyle::REGULAR,
"Switchroot INI Configuration Editor\n"
"Edit your Linux, Android and Lakka OC settings without a PC!",
true
);
subTitle->setHorizontalAlign(NVG_ALIGN_CENTER);
this->addView(subTitle);
// Copyright
brls::Label *copyright = new brls::Label(
brls::LabelStyle::DESCRIPTION,
"Licensed under GPL-3.0\n"
"Powered by Borealis UI framework\n"
"Based on the work of Switchroot\n"
"\u00A9 2026 NiklasCFW",
true
);
copyright->setHorizontalAlign(NVG_ALIGN_CENTER);
this->addView(copyright);
// Links
this->addView(new brls::Header("Links and Resources"));
brls::Label *links = new brls::Label(
brls::LabelStyle::SMALL,
"\uE016 NiklasCFW Docs for setup guides and documentation\n"
"\uE016 NiklasCFW's Discord server for support and community\n"
"\uE016 Source code available on the OmniNX GitHub",
true
);
this->addView(links);
}

18
src/about_tab.h Normal file
View File

@@ -0,0 +1,18 @@
/*
SWR INI Tool - Switchroot INI Configuration Editor
Copyright (C) 2026 Switchroot
*/
#pragma once
#include <borealis.hpp>
class AboutTab : public brls::List
{
public:
AboutTab();
// Prevent focus from entering the tab content
// so the user doesn't get stuck (no focusable children)
brls::View* getDefaultFocus() override { return nullptr; }
};

98
src/app_config.cpp Normal file
View File

@@ -0,0 +1,98 @@
/*
SWR INI Tool - Switchroot INI Configuration Editor
Copyright (C) 2026 Switchroot
*/
#include "app_config.h"
#include "ini_handler.h"
#include <sys/stat.h>
#include <cstring>
// Create parent directories for a file path
static void ensureParentDir(const std::string& filePath)
{
std::string dir = filePath;
size_t pos = dir.find_last_of('/');
if (pos == std::string::npos)
return;
dir = dir.substr(0, pos);
// Simple recursive mkdir
for (size_t i = 1; i < dir.size(); i++)
{
if (dir[i] == '/')
{
std::string sub = dir.substr(0, i);
mkdir(sub.c_str(), 0755);
}
}
mkdir(dir.c_str(), 0755);
}
AppConfig& AppConfig::get()
{
static AppConfig instance;
return instance;
}
AppConfig::AppConfig()
{
// Defaults
paths[(int)OsTarget::ANDROID] = DEFAULT_ANDROID_INI;
paths[(int)OsTarget::LINUX] = DEFAULT_LINUX_INI;
paths[(int)OsTarget::LAKKA] = DEFAULT_LAKKA_INI;
}
void AppConfig::load()
{
IniHandler cfg;
if (!cfg.load(APP_CONFIG_PATH))
return; // use defaults
std::string val;
val = cfg.get("paths", "android");
if (!val.empty()) paths[(int)OsTarget::ANDROID] = val;
val = cfg.get("paths", "linux");
if (!val.empty()) paths[(int)OsTarget::LINUX] = val;
val = cfg.get("paths", "lakka");
if (!val.empty()) paths[(int)OsTarget::LAKKA] = val;
}
void AppConfig::save()
{
ensureParentDir(APP_CONFIG_PATH);
IniHandler cfg;
cfg.load(APP_CONFIG_PATH); // load existing or start fresh
cfg.set("paths", "android", paths[(int)OsTarget::ANDROID]);
cfg.set("paths", "linux", paths[(int)OsTarget::LINUX]);
cfg.set("paths", "lakka", paths[(int)OsTarget::LAKKA]);
cfg.save(APP_CONFIG_PATH);
}
std::string AppConfig::getPath(OsTarget target) const
{
return paths[(int)target];
}
void AppConfig::setPath(OsTarget target, const std::string& path)
{
paths[(int)target] = path;
}
const char* AppConfig::getLabel(OsTarget target)
{
switch (target)
{
case OsTarget::ANDROID: return "Android";
case OsTarget::LINUX: return "Linux";
case OsTarget::LAKKA: return "Lakka";
default: return "Unknown";
}
}

44
src/app_config.h Normal file
View File

@@ -0,0 +1,44 @@
/*
SWR INI Tool - Switchroot INI Configuration Editor
Copyright (C) 2026 Switchroot
*/
#pragma once
#include <string>
#include "oc_defs.h"
#ifdef __SWITCH__
#define APP_CONFIG_PATH "sdmc:/config/swr-ini-tool/config.ini"
#define DEFAULT_BROWSE_DIR "sdmc:/bootloader/ini/"
#else
#define APP_CONFIG_PATH "./swr-ini-tool.cfg"
#define DEFAULT_BROWSE_DIR "./"
#endif
enum class OsTarget
{
ANDROID = 0,
LINUX,
LAKKA,
COUNT
};
class AppConfig
{
public:
static AppConfig& get();
void load();
void save();
std::string getPath(OsTarget target) const;
void setPath(OsTarget target, const std::string& path);
static const char* getLabel(OsTarget target);
private:
AppConfig();
std::string paths[(int)OsTarget::COUNT];
};

119
src/file_browser.cpp Normal file
View File

@@ -0,0 +1,119 @@
/*
SWR INI Tool - Switchroot INI Configuration Editor
Copyright (C) 2026 Switchroot
*/
#include "file_browser.h"
#include <dirent.h>
#include <sys/stat.h>
#include <algorithm>
#include <cstring>
#ifdef __SWITCH__
#define BROWSE_DIR "sdmc:/bootloader/ini/"
#else
#define BROWSE_DIR "./"
#endif
FileBrowser::FileBrowser(const std::string& startDir, FileSelectedCallback callback)
: brls::AppletFrame(true, true), list(nullptr), callback(callback)
{
// Always show the fixed directory
navigate(BROWSE_DIR);
}
std::vector<FileBrowser::DirEntry> FileBrowser::listDirectory(const std::string& dir)
{
std::vector<DirEntry> entries;
DIR* dp = opendir(dir.c_str());
if (!dp)
return entries;
struct dirent* ep;
while ((ep = readdir(dp)) != nullptr)
{
std::string name = ep->d_name;
// Skip . and ..
if (name == "." || name == "..")
continue;
DirEntry entry;
entry.name = name;
// Check if directory
std::string fullPath = dir;
if (fullPath.back() != '/')
fullPath += '/';
fullPath += name;
struct stat st;
if (stat(fullPath.c_str(), &st) == 0)
entry.isDir = S_ISDIR(st.st_mode);
else
entry.isDir = (ep->d_type == DT_DIR);
// Only show .ini files (no directories)
if (!entry.isDir && name.size() >= 4)
{
std::string ext = name.substr(name.size() - 4);
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
if (ext == ".ini")
entries.push_back(entry);
}
}
closedir(dp);
// Sort alphabetically
std::sort(entries.begin(), entries.end(), [](const DirEntry& a, const DirEntry& b) {
return a.name < b.name;
});
return entries;
}
void FileBrowser::navigate(const std::string& dir)
{
this->currentDir = dir;
this->setTitle("Select INI \u2014 " + dir);
// Create a fresh list (setContentView frees the old one)
this->list = new brls::List();
this->setContentView(this->list);
// List .ini files
auto entries = listDirectory(dir);
if (entries.empty())
{
brls::ListItem* emptyItem = new brls::ListItem("No .ini files found in " + dir);
this->list->addView(emptyItem);
return;
}
for (const auto& entry : entries)
{
std::string fullPath = dir;
if (fullPath.back() != '/')
fullPath += '/';
fullPath += entry.name;
brls::ListItem* item = new brls::ListItem("\uE873 " + entry.name);
std::string pathCopy = fullPath;
item->getClickEvent()->subscribe([this, pathCopy](brls::View* view) {
this->selectFile(pathCopy);
});
this->list->addView(item);
}
}
void FileBrowser::selectFile(const std::string& path)
{
if (this->callback)
this->callback(path);
brls::Application::popView();
}

35
src/file_browser.h Normal file
View File

@@ -0,0 +1,35 @@
/*
SWR INI Tool - Switchroot INI Configuration Editor
Copyright (C) 2026 Switchroot
*/
#pragma once
#include <borealis.hpp>
#include <string>
#include <vector>
#include <functional>
using FileSelectedCallback = std::function<void(const std::string& path)>;
class FileBrowser : public brls::AppletFrame
{
public:
FileBrowser(const std::string& startDir, FileSelectedCallback callback);
private:
brls::List* list;
FileSelectedCallback callback;
std::string currentDir;
void navigate(const std::string& dir);
void selectFile(const std::string& path);
struct DirEntry
{
std::string name;
bool isDir;
};
std::vector<DirEntry> listDirectory(const std::string& dir);
};

310
src/ini_handler.cpp Normal file
View File

@@ -0,0 +1,310 @@
/*
SWR INI Tool - Switchroot INI Configuration Editor
Copyright (C) 2026 Switchroot
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.
*/
#include "ini_handler.h"
#include <fstream>
#include <sstream>
#include <algorithm>
#include <cstring>
static std::string trim(const std::string& str)
{
size_t first = str.find_first_not_of(" \t\r\n");
if (first == std::string::npos)
return "";
size_t last = str.find_last_not_of(" \t\r\n");
return str.substr(first, last - first + 1);
}
static std::string toLower(const std::string& str)
{
std::string result = str;
std::transform(result.begin(), result.end(), result.begin(), ::tolower);
return result;
}
IniHandler::IniHandler() : loaded(false)
{
}
bool IniHandler::load(const std::string& path)
{
this->filePath = path;
this->lines.clear();
this->loaded = false;
std::ifstream file(path);
if (!file.is_open())
return false;
std::string currentSection;
std::string rawLine;
while (std::getline(file, rawLine))
{
Line line;
line.raw = rawLine;
std::string trimmed = trim(rawLine);
if (trimmed.empty())
{
line.type = Line::BLANK;
}
else if (trimmed[0] == '#' || trimmed[0] == ';')
{
line.type = Line::COMMENT;
}
else if (trimmed[0] == '[' && trimmed.back() == ']')
{
line.type = Line::SECTION;
line.section = trim(trimmed.substr(1, trimmed.size() - 2));
currentSection = line.section;
}
else
{
size_t eqPos = trimmed.find('=');
if (eqPos != std::string::npos)
{
line.type = Line::KEY_VALUE;
line.section = currentSection;
line.key = trim(trimmed.substr(0, eqPos));
line.value = trim(trimmed.substr(eqPos + 1));
}
else
{
line.type = Line::COMMENT; // treat unparseable lines as comments
}
}
this->lines.push_back(line);
}
file.close();
this->loaded = true;
return true;
}
bool IniHandler::save()
{
return save(this->filePath);
}
bool IniHandler::save(const std::string& path)
{
std::ofstream file(path);
if (!file.is_open())
return false;
for (size_t i = 0; i < this->lines.size(); i++)
{
const Line& line = this->lines[i];
if (line.type == Line::KEY_VALUE)
{
file << line.key << "=" << line.value;
}
else
{
file << line.raw;
}
if (i < this->lines.size() - 1)
file << "\n";
}
file.close();
return true;
}
std::string IniHandler::get(const std::string& section, const std::string& key, const std::string& defaultValue) const
{
int idx = findLine(section, key);
if (idx >= 0)
return this->lines[idx].value;
return defaultValue;
}
int IniHandler::getInt(const std::string& section, const std::string& key, int defaultValue) const
{
std::string val = get(section, key, "");
if (val.empty())
return defaultValue;
try
{
return std::stoi(val);
}
catch (...)
{
return defaultValue;
}
}
bool IniHandler::getBool(const std::string& section, const std::string& key, bool defaultValue) const
{
return getInt(section, key, defaultValue ? 1 : 0) != 0;
}
void IniHandler::set(const std::string& section, const std::string& key, const std::string& value)
{
int idx = findLine(section, key);
if (idx >= 0)
{
this->lines[idx].value = value;
}
else
{
// Key doesn't exist - add it at the end of the section
int sectionEnd = findSectionEnd(section);
if (sectionEnd < 0)
{
// Section doesn't exist - create it
Line sectionLine;
sectionLine.type = Line::SECTION;
sectionLine.section = section;
sectionLine.raw = "[" + section + "]";
Line blankLine;
blankLine.type = Line::BLANK;
blankLine.raw = "";
Line kvLine;
kvLine.type = Line::KEY_VALUE;
kvLine.section = section;
kvLine.key = key;
kvLine.value = value;
this->lines.push_back(blankLine);
this->lines.push_back(sectionLine);
this->lines.push_back(kvLine);
}
else
{
Line kvLine;
kvLine.type = Line::KEY_VALUE;
kvLine.section = section;
kvLine.key = key;
kvLine.value = value;
this->lines.insert(this->lines.begin() + sectionEnd, kvLine);
}
}
}
void IniHandler::setInt(const std::string& section, const std::string& key, int value)
{
set(section, key, std::to_string(value));
}
void IniHandler::setBool(const std::string& section, const std::string& key, bool value)
{
setInt(section, key, value ? 1 : 0);
}
bool IniHandler::hasSection(const std::string& section) const
{
for (const Line& line : this->lines)
{
if (line.type == Line::SECTION && toLower(line.section) == toLower(section))
return true;
}
return false;
}
bool IniHandler::hasKey(const std::string& section, const std::string& key) const
{
return findLine(section, key) >= 0;
}
std::vector<std::string> IniHandler::getSections() const
{
std::vector<std::string> sections;
for (const Line& line : this->lines)
{
if (line.type == Line::SECTION)
sections.push_back(line.section);
}
return sections;
}
std::vector<std::pair<std::string, std::string>> IniHandler::getKeys(const std::string& section) const
{
std::vector<std::pair<std::string, std::string>> keys;
for (const Line& line : this->lines)
{
if (line.type == Line::KEY_VALUE && toLower(line.section) == toLower(section))
keys.push_back({line.key, line.value});
}
return keys;
}
std::string IniHandler::findOsSection() const
{
for (const Line& line : this->lines)
{
if (line.type == Line::SECTION && toLower(line.section) != "config")
return line.section;
}
return "";
}
int IniHandler::findLine(const std::string& section, const std::string& key) const
{
std::string sectionLower = toLower(section);
std::string keyLower = toLower(key);
for (size_t i = 0; i < this->lines.size(); i++)
{
const Line& line = this->lines[i];
if (line.type == Line::KEY_VALUE &&
toLower(line.section) == sectionLower &&
toLower(line.key) == keyLower)
{
return (int)i;
}
}
return -1;
}
int IniHandler::findSectionEnd(const std::string& section) const
{
std::string sectionLower = toLower(section);
bool inSection = false;
int lastLine = -1;
for (size_t i = 0; i < this->lines.size(); i++)
{
const Line& line = this->lines[i];
if (line.type == Line::SECTION)
{
if (toLower(line.section) == sectionLower)
{
inSection = true;
lastLine = (int)i + 1;
}
else if (inSection)
{
return lastLine;
}
}
else if (inSection && line.type != Line::BLANK)
{
lastLine = (int)i + 1;
}
}
if (inSection)
return lastLine;
return -1;
}

64
src/ini_handler.h Normal file
View File

@@ -0,0 +1,64 @@
/*
SWR INI Tool - Switchroot INI Configuration Editor
Copyright (C) 2026 Switchroot
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.
*/
#pragma once
#include <string>
#include <vector>
#include <map>
class IniHandler
{
public:
IniHandler();
bool load(const std::string& path);
bool save();
bool save(const std::string& path);
std::string get(const std::string& section, const std::string& key, const std::string& defaultValue = "") const;
int getInt(const std::string& section, const std::string& key, int defaultValue = 0) const;
bool getBool(const std::string& section, const std::string& key, bool defaultValue = false) const;
void set(const std::string& section, const std::string& key, const std::string& value);
void setInt(const std::string& section, const std::string& key, int value);
void setBool(const std::string& section, const std::string& key, bool value);
bool hasSection(const std::string& section) const;
bool hasKey(const std::string& section, const std::string& key) const;
std::vector<std::string> getSections() const;
std::vector<std::pair<std::string, std::string>> getKeys(const std::string& section) const;
// Find the first section that is not "config" (the OS-specific section)
std::string findOsSection() const;
const std::string& getFilePath() const { return filePath; }
bool isLoaded() const { return loaded; }
private:
struct Line
{
enum Type { BLANK, COMMENT, SECTION, KEY_VALUE };
Type type;
std::string raw;
std::string section;
std::string key;
std::string value;
};
std::vector<Line> lines;
std::string filePath;
bool loaded;
std::string rebuildLine(const Line& line) const;
int findLine(const std::string& section, const std::string& key) const;
int findSectionEnd(const std::string& section) const;
};

65
src/logo.cpp Normal file
View File

@@ -0,0 +1,65 @@
/*
SWR INI Tool - Switchroot INI Configuration Editor
Copyright (C) 2026 Switchroot
*/
#include "logo.h"
Logo::Logo(LogoStyle style)
{
this->logoLabel = new brls::Label(brls::LabelStyle::LIST_ITEM, "SWR", style == LogoStyle::ABOUT);
this->logoLabel->setParent(this);
int logoFont = brls::Application::findFont(LOGO_FONT_NAME);
if (logoFont >= 0)
{
this->logoLabel->setFont(logoFont);
}
if (style == LogoStyle::ABOUT)
{
this->logoLabel->setFontSize(LOGO_ABOUT_FONT_SIZE);
this->logoLabel->setHorizontalAlign(NVG_ALIGN_CENTER);
}
if (style == LogoStyle::HEADER)
{
this->logoLabel->setFontSize(LOGO_HEADER_FONT_SIZE);
this->descLabel = new brls::Label(brls::LabelStyle::LIST_ITEM, "INI Tool");
this->descLabel->setParent(this);
this->descLabel->setFontSize(LOGO_DESC_FONT_SIZE);
}
}
Logo::~Logo()
{
delete this->logoLabel;
if (this->descLabel)
delete this->descLabel;
}
void Logo::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, brls::Style* style, brls::FrameContext* ctx)
{
this->logoLabel->frame(ctx);
if (this->descLabel)
this->descLabel->frame(ctx);
}
void Logo::layout(NVGcontext* vg, brls::Style* style, brls::FontStash* stash)
{
this->logoLabel->setBoundaries(this->x, this->y + LOGO_OFFSET, this->width, this->height);
this->logoLabel->layout(vg, style, stash);
this->height = this->logoLabel->getHeight();
if (this->descLabel)
{
this->descLabel->layout(vg, style, stash);
this->descLabel->setBoundaries(
this->x + LOGO_HEADER_SPACING + this->logoLabel->getWidth(),
this->y + style->AppletFrame.titleOffset - 1,
this->descLabel->getWidth(),
height);
}
}

38
src/logo.h Normal file
View File

@@ -0,0 +1,38 @@
/*
SWR INI Tool - Switchroot INI Configuration Editor
Copyright (C) 2026 Switchroot
*/
#pragma once
#include <borealis.hpp>
#define APP_ASSET(p) APP_RESOURCES p
enum class LogoStyle
{
HEADER = 0,
ABOUT
};
#define LOGO_FONT_NAME "logo"
#define LOGO_FONT_PATH APP_ASSET("fira/FiraSans-Medium-rnx.ttf")
#define LOGO_HEADER_FONT_SIZE 45
#define LOGO_HEADER_SPACING 12
#define LOGO_ABOUT_FONT_SIZE 55
#define LOGO_DESC_FONT_SIZE 28
#define LOGO_OFFSET 2
class Logo : public brls::View
{
protected:
brls::Label* logoLabel = nullptr;
brls::Label* descLabel = nullptr;
void layout(NVGcontext* vg, brls::Style* style, brls::FontStash* stash);
void draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, brls::Style* style, brls::FrameContext* ctx) override;
public:
Logo(LogoStyle style);
virtual ~Logo();
};

53
src/main.cpp Normal file
View File

@@ -0,0 +1,53 @@
/*
SWR INI Tool - Switchroot INI Configuration Editor
Copyright (C) 2026 Switchroot
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.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <borealis.hpp>
#include "main_frame.h"
#include "logo.h"
#include "app_config.h"
int main(int argc, char* argv[])
{
// Init the app
if (!brls::Application::init("SWR INI Tool"))
{
brls::Logger::error("Unable to init Borealis application");
return EXIT_FAILURE;
}
// Verbose logging on PC
#ifndef __SWITCH__
brls::Logger::setLogLevel(brls::LogLevel::DEBUG);
#endif
// Load saved path configuration
AppConfig::get().load();
// Load custom font for logo
if (brls::Application::loadFont(LOGO_FONT_NAME, LOGO_FONT_PATH) < 0)
{
brls::Logger::error("Failed to load logo font");
}
// Create and push the main frame
MainFrame *mainFrame = new MainFrame();
brls::Application::pushView(mainFrame);
// Run the app
while (brls::Application::mainLoop())
;
return EXIT_SUCCESS;
}

48
src/main_frame.cpp Normal file
View File

@@ -0,0 +1,48 @@
/*
SWR INI Tool - Switchroot INI Configuration Editor
Copyright (C) 2026 Switchroot
*/
#include "main_frame.h"
#include "os_config_tab.h"
#include "settings_tab.h"
#include "about_tab.h"
#include "logo.h"
#include "app_config.h"
OsConfigTab* MainFrame::osTabs[(int)OsTarget::COUNT] = {};
MainFrame::MainFrame() : TabFrame()
{
AppConfig& cfg = AppConfig::get();
// Header logo
this->setIcon(new Logo(LogoStyle::HEADER));
// OS configuration tabs — paths from config
osTabs[(int)OsTarget::ANDROID] = new OsConfigTab("Android", cfg.getPath(OsTarget::ANDROID));
osTabs[(int)OsTarget::LINUX] = new OsConfigTab("Linux", cfg.getPath(OsTarget::LINUX));
osTabs[(int)OsTarget::LAKKA] = new OsConfigTab("Lakka", cfg.getPath(OsTarget::LAKKA));
this->addTab("Android", osTabs[(int)OsTarget::ANDROID]);
this->addTab("Linux", osTabs[(int)OsTarget::LINUX]);
this->addTab("Lakka", osTabs[(int)OsTarget::LAKKA]);
this->addTab("Settings", new SettingsTab());
this->addSeparator();
this->addTab("About", new AboutTab());
}
MainFrame::~MainFrame()
{
}
OsConfigTab* MainFrame::getOsTab(OsTarget target)
{
int idx = (int)target;
if (idx >= 0 && idx < (int)OsTarget::COUNT)
return osTabs[idx];
return nullptr;
}

24
src/main_frame.h Normal file
View File

@@ -0,0 +1,24 @@
/*
SWR INI Tool - Switchroot INI Configuration Editor
Copyright (C) 2026 Switchroot
*/
#pragma once
#include <borealis.hpp>
#include "app_config.h"
class OsConfigTab;
class MainFrame : public brls::TabFrame
{
public:
MainFrame();
~MainFrame();
// Access OS config tabs so they can be reloaded from other tabs
static OsConfigTab* getOsTab(OsTarget target);
private:
static OsConfigTab* osTabs[(int)OsTarget::COUNT];
};

115
src/oc_defs.h Normal file
View File

@@ -0,0 +1,115 @@
/*
SWR INI Tool - Switchroot INI Configuration Editor
Copyright (C) 2026 Switchroot
OC parameter definitions and frequency tables.
*/
#pragma once
#include <string>
#include <vector>
#include <cstdint>
// Format a frequency in kHz for display
static inline std::string formatFreqKHz(uint32_t kHz)
{
char buf[32];
if (kHz % 1000 == 0)
snprintf(buf, sizeof(buf), "%u MHz", kHz / 1000);
else
snprintf(buf, sizeof(buf), "%.1f MHz", kHz / 1000.0);
return std::string(buf);
}
// Format a voltage in mV for display
static inline std::string formatVoltageMv(uint32_t mV)
{
char buf[32];
snprintf(buf, sizeof(buf), "%u mV", mV);
return std::string(buf);
}
enum class OcKeyType
{
BOOLEAN,
FREQ_KHZ,
VOLTAGE_MV,
};
struct OcKeyDef
{
std::string key;
std::string label;
std::string description;
OcKeyType type;
std::vector<uint32_t> options;
};
// Boolean OC parameters
static const std::vector<OcKeyDef> OC_BOOL_KEYS = {
{"oc", "Overclocking", "Master OC enable switch", OcKeyType::BOOLEAN, {}},
{"dvfsb", "CPU DVFS Boost", "Enable extended CPU DVFS boost frequency table", OcKeyType::BOOLEAN, {}},
{"gpu_dvfsc", "GPU DVFS Scaling", "Enable extended GPU DVFS frequency scaling", OcKeyType::BOOLEAN, {}},
{"usb3force", "Force USB 3.0", "Force USB 3.0 mode on the dock", OcKeyType::BOOLEAN, {}},
{"ddr200_enable", "DDR200 Enable", "Enable DDR200 mode for eMMC (Android)", OcKeyType::BOOLEAN, {}},
};
// Frequency OC parameters (dropdowns)
static const std::vector<OcKeyDef> OC_FREQ_KEYS = {
{"max_cpu_freq", "Max CPU Frequency", "Maximum CPU clock speed",
OcKeyType::FREQ_KHZ,
{1020000, 1224000, 1428000, 1581000, 1683000, 1785000,
1887000, 1963500, 2091000, 2193000, 2295000, 2397000}},
{"max_gpu_freq", "Max GPU Frequency", "Maximum GPU clock speed",
OcKeyType::FREQ_KHZ,
{768000, 844800, 921600, 998400, 1075200, 1152000, 1267200}},
{"ram_oc", "RAM Frequency", "RAM overclock frequency",
OcKeyType::FREQ_KHZ,
{1600000, 1862400, 1996800, 2131200, 2265600, 2332800, 2400000, 2466000,
2534400, 2600000, 2665600, 2732000, 2800000, 2864000, 2932800, 3000000,
3065600, 3132800, 3200000}},
};
// Voltage OC parameters (dropdowns)
static const std::vector<OcKeyDef> OC_VOLTAGE_KEYS = {
{"ram_oc_vdd2", "RAM VDD2 Voltage", "DRAM VDD2 supply voltage",
OcKeyType::VOLTAGE_MV,
{1100, 1125, 1150, 1175, 1200, 1225, 1250}},
{"ram_oc_vddq", "RAM VDDQ Voltage", "DRAM VDDQ supply voltage",
OcKeyType::VOLTAGE_MV,
{550, 575, 600, 625, 650}},
};
// Hekate [config] section boolean keys
struct ConfigKeyDef
{
std::string key;
std::string label;
std::string description;
};
static const std::vector<ConfigKeyDef> CONFIG_BOOL_KEYS = {
{"autoboot", "Autoboot", "Automatically boot the default entry on startup"},
{"autoboot_list", "Autoboot List", "Automatically boot from the ini list on startup"},
{"bootwait", "Boot Wait", "Wait for user input before auto-booting"},
{"autohosoff", "Auto HOS Off", "Automatically shut down when booting Horizon OS"},
{"autonogc", "Auto NoGC", "Automatically apply the NoGC patch"},
{"updater2p", "Updater to Payload", "Reboot from updater to payload instead of OFW"},
{"bootprotect", "Boot Protect", "Protect boot configuration from accidental changes"},
{"noticker", "No Ticker", "Disable the ticker animation in hekate"},
};
// INI file default paths
#ifdef __SWITCH__
#define DEFAULT_ANDROID_INI "sdmc:/bootloader/ini/Android_OC.ini"
#define DEFAULT_LINUX_INI "sdmc:/bootloader/ini/Ubuntu_OC.ini"
#define DEFAULT_LAKKA_INI "sdmc:/bootloader/ini/Lakka_OC.ini"
#else
#define DEFAULT_ANDROID_INI "./Android_OC.ini"
#define DEFAULT_LINUX_INI "./Ubuntu_OC.ini"
#define DEFAULT_LAKKA_INI "./Lakka_OC.ini"
#endif

279
src/os_config_tab.cpp Normal file
View File

@@ -0,0 +1,279 @@
/*
SWR INI Tool - Switchroot INI Configuration Editor
Copyright (C) 2026 Switchroot
*/
#include "os_config_tab.h"
#include "oc_defs.h"
#include <algorithm>
OsConfigTab::OsConfigTab(const std::string& osName, const std::string& iniPath)
: osName(osName), iniPath(iniPath)
{
if (!this->ini.load(iniPath))
{
buildErrorUI("Could not load " + iniPath + "\n\n"
"Make sure the INI file exists at the expected path.\n"
"You can configure paths in the Settings tab.");
return;
}
this->osSection = this->ini.findOsSection();
if (this->osSection.empty())
{
buildErrorUI("No OS section found in " + iniPath + "\n\n"
"The INI file should contain a section like\n"
"[" + osName + " OC] with your configuration.");
return;
}
buildUI();
}
void OsConfigTab::reload(const std::string& newIniPath)
{
this->iniPath = newIniPath;
this->osSection.clear();
// Remove all child views
this->clear();
if (!this->ini.load(iniPath))
{
buildErrorUI("Could not load " + iniPath + "\n\n"
"Make sure the INI file exists at the expected path.\n"
"You can configure paths in the Settings tab.");
return;
}
this->osSection = this->ini.findOsSection();
if (this->osSection.empty())
{
buildErrorUI("No OS section found in " + iniPath + "\n\n"
"The INI file should contain a section like\n"
"[" + osName + " OC] with your configuration.");
return;
}
buildUI();
}
void OsConfigTab::buildErrorUI(const std::string& message)
{
this->setSpacing(15);
// Use a ListItem (focusable) so borealis doesn't crash on controller nav
brls::ListItem *errorItem = new brls::ListItem("\uE150 INI file not found", message);
this->addView(errorItem);
}
void OsConfigTab::buildUI()
{
this->setSpacing(2);
this->setMarginBottom(20);
// Section name info
brls::Label *sectionInfo = new brls::Label(
brls::LabelStyle::DESCRIPTION,
"Editing section [" + this->osSection + "] from " + this->iniPath,
true
);
this->addView(sectionInfo);
// ── Toggle Options ──
this->addView(new brls::Header("Toggle Options"));
for (const auto& def : OC_BOOL_KEYS)
{
if (this->ini.hasKey(this->osSection, def.key))
{
bool val = this->ini.getBool(this->osSection, def.key);
addBooleanToggle(def.label, "", def.key, val);
}
}
// ── Frequency Settings ──
brls::Rectangle* spacer1 = new brls::Rectangle(nvgRGBA(0, 0, 0, 0));
spacer1->setHeight(30);
this->addView(spacer1);
this->addView(new brls::Header("Frequency Settings"));
for (const auto& def : OC_FREQ_KEYS)
{
if (this->ini.hasKey(this->osSection, def.key))
{
uint32_t val = (uint32_t)this->ini.getInt(this->osSection, def.key, 0);
addFreqDropdown(def.label, "", def.key, val, def.options);
}
}
// ── Voltage Settings ──
brls::Rectangle* spacer2 = new brls::Rectangle(nvgRGBA(0, 0, 0, 0));
spacer2->setHeight(30);
this->addView(spacer2);
this->addView(new brls::Header("Voltage Settings"));
for (const auto& def : OC_VOLTAGE_KEYS)
{
if (this->ini.hasKey(this->osSection, def.key))
{
uint32_t val = (uint32_t)this->ini.getInt(this->osSection, def.key, 0);
addVoltageDropdown(def.label, "", def.key, val, def.options);
}
}
// ── Other Keys (read-only info) ──
// Show any keys that aren't covered by the known definitions above
auto allKeys = this->ini.getKeys(this->osSection);
bool hasOtherKeys = false;
for (const auto& kv : allKeys)
{
bool isKnown = false;
for (const auto& def : OC_BOOL_KEYS)
if (def.key == kv.first) { isKnown = true; break; }
if (!isKnown)
for (const auto& def : OC_FREQ_KEYS)
if (def.key == kv.first) { isKnown = true; break; }
if (!isKnown)
for (const auto& def : OC_VOLTAGE_KEYS)
if (def.key == kv.first) { isKnown = true; break; }
if (!isKnown)
{
if (!hasOtherKeys)
{
brls::Rectangle* spacerOther = new brls::Rectangle(nvgRGBA(0, 0, 0, 0));
spacerOther->setHeight(30);
this->addView(spacerOther);
this->addView(new brls::Header("Other"));
hasOtherKeys = true;
}
brls::ListItem *item = new brls::ListItem(kv.first);
item->setValue(kv.second);
this->addView(item);
}
}
}
void OsConfigTab::saveAndNotify()
{
if (this->ini.save())
{
brls::Application::notify("\uE14B Configuration saved");
}
else
{
brls::Application::notify("\uE150 Error saving configuration!");
}
}
void OsConfigTab::addBooleanToggle(const std::string& label, const std::string& description,
const std::string& key, bool currentValue)
{
brls::ToggleListItem *toggle = new brls::ToggleListItem(
label, currentValue, description, "ON", "OFF"
);
std::string keyCopy = key;
toggle->getClickEvent()->subscribe([this, toggle, keyCopy](brls::View* view) {
bool newVal = toggle->getToggleState();
this->ini.setBool(this->osSection, keyCopy, newVal);
this->saveAndNotify();
});
this->addView(toggle);
}
void OsConfigTab::addFreqDropdown(const std::string& label, const std::string& description,
const std::string& key, uint32_t currentValue,
const std::vector<uint32_t>& options)
{
// Build option list, including the current value if not in the predefined list
std::vector<uint32_t> allOptions = options;
bool currentFound = false;
for (uint32_t opt : allOptions)
{
if (opt == currentValue) { currentFound = true; break; }
}
if (!currentFound && currentValue > 0)
{
allOptions.push_back(currentValue);
std::sort(allOptions.begin(), allOptions.end());
}
// Build display strings
std::vector<std::string> displayOptions;
size_t selectedIdx = 0;
for (size_t i = 0; i < allOptions.size(); i++)
{
displayOptions.push_back(formatFreqKHz(allOptions[i]));
if (allOptions[i] == currentValue)
selectedIdx = i;
}
brls::SelectListItem *item = new brls::SelectListItem(label, displayOptions, selectedIdx);
std::string keyCopy = key;
std::vector<uint32_t> optionsCopy = allOptions;
item->getValueSelectedEvent()->subscribe([this, keyCopy, optionsCopy](int result) {
if (result >= 0 && result < (int)optionsCopy.size())
{
this->ini.setInt(this->osSection, keyCopy, (int)optionsCopy[result]);
this->saveAndNotify();
}
});
this->addView(item);
}
void OsConfigTab::addVoltageDropdown(const std::string& label, const std::string& description,
const std::string& key, uint32_t currentValue,
const std::vector<uint32_t>& options)
{
// Build option list, including the current value if not in the predefined list
std::vector<uint32_t> allOptions = options;
bool currentFound = false;
for (uint32_t opt : allOptions)
{
if (opt == currentValue) { currentFound = true; break; }
}
if (!currentFound && currentValue > 0)
{
allOptions.push_back(currentValue);
std::sort(allOptions.begin(), allOptions.end());
}
// Build display strings
std::vector<std::string> displayOptions;
size_t selectedIdx = 0;
for (size_t i = 0; i < allOptions.size(); i++)
{
displayOptions.push_back(formatVoltageMv(allOptions[i]));
if (allOptions[i] == currentValue)
selectedIdx = i;
}
brls::SelectListItem *item = new brls::SelectListItem(label, displayOptions, selectedIdx);
std::string keyCopy = key;
std::vector<uint32_t> optionsCopy = allOptions;
item->getValueSelectedEvent()->subscribe([this, keyCopy, optionsCopy](int result) {
if (result >= 0 && result < (int)optionsCopy.size())
{
this->ini.setInt(this->osSection, keyCopy, (int)optionsCopy[result]);
this->saveAndNotify();
}
});
this->addView(item);
}

42
src/os_config_tab.h Normal file
View File

@@ -0,0 +1,42 @@
/*
SWR INI Tool - Switchroot INI Configuration Editor
Copyright (C) 2026 Switchroot
*/
#pragma once
#include <borealis.hpp>
#include "ini_handler.h"
class OsConfigTab : public brls::List
{
public:
OsConfigTab(const std::string& osName, const std::string& iniPath);
// Reload the tab with a new INI path (clears and rebuilds all UI)
void reload(const std::string& newIniPath);
private:
IniHandler ini;
std::string osName;
std::string iniPath;
std::string osSection;
void buildUI();
void buildErrorUI(const std::string& message);
void saveAndNotify();
// Create a toggle for a boolean key
void addBooleanToggle(const std::string& label, const std::string& description,
const std::string& key, bool currentValue);
// Create a dropdown for a frequency key (values in kHz displayed as MHz)
void addFreqDropdown(const std::string& label, const std::string& description,
const std::string& key, uint32_t currentValue,
const std::vector<uint32_t>& options);
// Create a dropdown for a voltage key (values in mV)
void addVoltageDropdown(const std::string& label, const std::string& description,
const std::string& key, uint32_t currentValue,
const std::vector<uint32_t>& options);
};

70
src/settings_tab.cpp Normal file
View File

@@ -0,0 +1,70 @@
/*
SWR INI Tool - Switchroot INI Configuration Editor
Copyright (C) 2026 Switchroot
*/
#include "settings_tab.h"
#include "file_browser.h"
#include "main_frame.h"
#include "os_config_tab.h"
SettingsTab::SettingsTab()
{
this->setSpacing(2);
this->setMarginBottom(20);
brls::Label *info = new brls::Label(
brls::LabelStyle::DESCRIPTION,
"Select which INI file each OS tab reads and writes.\n"
"Changes take effect immediately.",
true
);
this->addView(info);
// ── INI File Paths ──
this->addView(new brls::Header("INI File Paths"));
addPathItem(OsTarget::ANDROID);
addPathItem(OsTarget::LINUX);
addPathItem(OsTarget::LAKKA);
}
void SettingsTab::addPathItem(OsTarget target)
{
AppConfig& cfg = AppConfig::get();
std::string currentPath = cfg.getPath(target);
std::string label = std::string(AppConfig::getLabel(target)) + " INI";
std::string description = (target == OsTarget::LAKKA) ? "Tap to browse for an INI file" : "";
brls::ListItem* item = new brls::ListItem(label, description);
item->setValue(currentPath);
item->getClickEvent()->subscribe([target, item](brls::View* view) {
// Determine start directory from current path
std::string curPath = AppConfig::get().getPath(target);
std::string startDir = DEFAULT_BROWSE_DIR;
size_t lastSlash = curPath.find_last_of('/');
if (lastSlash != std::string::npos && lastSlash > 0)
startDir = curPath.substr(0, lastSlash + 1);
FileBrowser* browser = new FileBrowser(startDir,
[target, item](const std::string& selectedPath) {
AppConfig::get().setPath(target, selectedPath);
AppConfig::get().save();
item->setValue(selectedPath);
// Reload the corresponding OS tab immediately
OsConfigTab* tab = MainFrame::getOsTab(target);
if (tab)
tab->reload(selectedPath);
brls::Application::notify("\uE14B Path updated");
}
);
brls::Application::pushView(browser);
});
this->addView(item);
}

18
src/settings_tab.h Normal file
View File

@@ -0,0 +1,18 @@
/*
SWR INI Tool - Switchroot INI Configuration Editor
Copyright (C) 2026 Switchroot
*/
#pragma once
#include <borealis.hpp>
#include "app_config.h"
class SettingsTab : public brls::List
{
public:
SettingsTab();
private:
void addPathItem(OsTarget target);
};