Initial commit
This commit is contained in:
47
src/about_tab.cpp
Normal file
47
src/about_tab.cpp
Normal 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
18
src/about_tab.h
Normal 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
98
src/app_config.cpp
Normal 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
44
src/app_config.h
Normal 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
119
src/file_browser.cpp
Normal 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
35
src/file_browser.h
Normal 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
310
src/ini_handler.cpp
Normal 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
64
src/ini_handler.h
Normal 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
65
src/logo.cpp
Normal 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
38
src/logo.h
Normal 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
53
src/main.cpp
Normal 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
48
src/main_frame.cpp
Normal 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
24
src/main_frame.h
Normal 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
115
src/oc_defs.h
Normal 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
279
src/os_config_tab.cpp
Normal 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
42
src/os_config_tab.h
Normal 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
70
src/settings_tab.cpp
Normal 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
18
src/settings_tab.h
Normal 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);
|
||||
};
|
||||
Reference in New Issue
Block a user