Compare commits

...

4 Commits

8 changed files with 236 additions and 84 deletions

View File

@ -1,5 +1,6 @@
# d2025.06.0 # d2025.06.0
* Add ability to remember recent projects in config
* PaletteEditor: Add RGB key shortcuts for focusing color channels * PaletteEditor: Add RGB key shortcuts for focusing color channels
# d2025.05.1 # d2025.05.1

View File

@ -67,10 +67,10 @@ static ox::Vector<uint32_t> normalizePixelArrangement(
} }
static ox::Error toPngFile( static ox::Error toPngFile(
ox::CStringView const&path, ox::CStringViewCR path,
ox::Vector<uint32_t> &&pixels, ox::Vector<uint32_t> &&pixels,
Palette const &pal, Palette const &pal,
size_t page, size_t const page,
unsigned const width, unsigned const width,
unsigned const height) noexcept { unsigned const height) noexcept {
for (auto &c : pixels) { for (auto &c : pixels) {
@ -88,7 +88,7 @@ static ox::Error toPngFile(
} }
TileSheetEditorImGui::TileSheetEditorImGui(studio::Context &sctx, ox::StringParam path): TileSheetEditorImGui::TileSheetEditorImGui(studio::Context &sctx, ox::StringParam path):
Editor(sctx, std::move(path)), Editor{sctx, std::move(path)},
m_sctx{sctx}, m_sctx{sctx},
m_tctx{m_sctx.tctx}, m_tctx{m_sctx.tctx},
m_palPicker{"Palette Chooser", keelCtx(sctx), FileExt_npal}, m_palPicker{"Palette Chooser", keelCtx(sctx), FileExt_npal},
@ -98,8 +98,11 @@ TileSheetEditorImGui::TileSheetEditorImGui(studio::Context &sctx, ox::StringPara
m_subsheetEditor.inputSubmitted.connect(this, &TileSheetEditorImGui::updateActiveSubsheet); m_subsheetEditor.inputSubmitted.connect(this, &TileSheetEditorImGui::updateActiveSubsheet);
m_exportMenu.inputSubmitted.connect(this, &TileSheetEditorImGui::exportSubsheetToPng); m_exportMenu.inputSubmitted.connect(this, &TileSheetEditorImGui::exportSubsheetToPng);
// load config // load config
auto const&config = studio::readConfig<TileSheetEditorConfig>( auto &kctx = keelCtx(m_sctx);
keelCtx(m_sctx), itemPath()); auto const ip = itemPath();
oxLogError(studio::headerizeConfigFile<TileSheetEditorConfig>(kctx, ip));
auto const &config =
studio::readConfig<TileSheetEditorConfig>(kctx, ip);
if (config.ok()) { if (config.ok()) {
m_model.setActiveSubsheet(validateSubSheetIdx(m_model.img(), config.value.activeSubsheet)); m_model.setActiveSubsheet(validateSubSheetIdx(m_model.img(), config.value.activeSubsheet));
} }
@ -594,7 +597,7 @@ void TileSheetEditorImGui::drawPaletteMenu() noexcept {
} }
ox::Error TileSheetEditorImGui::updateActiveSubsheet( ox::Error TileSheetEditorImGui::updateActiveSubsheet(
ox::StringView const&name, int const cols, int const rows) noexcept { ox::StringViewCR name, int const cols, int const rows) noexcept {
return m_model.updateSubsheet(m_model.activeSubSheetIdx(), name, cols, rows); return m_model.updateSubsheet(m_model.activeSubSheetIdx(), name, cols, rows);
} }

View File

@ -98,7 +98,7 @@ class TileSheetEditorImGui: public studio::Editor {
void drawPaletteMenu() noexcept; void drawPaletteMenu() noexcept;
ox::Error updateActiveSubsheet(ox::StringView const&name, int cols, int rows) noexcept; ox::Error updateActiveSubsheet(ox::StringViewCR name, int cols, int rows) noexcept;
void setActiveSubsheet(TileSheet::SubSheetIdx path) noexcept; void setActiveSubsheet(TileSheet::SubSheetIdx path) noexcept;

View File

@ -260,7 +260,7 @@ ox::Result<DstType> convertObjToObj(
template<typename DstType> template<typename DstType>
ox::Result<DstType> convert(Context &ctx, ox::BufferView const&src) noexcept { ox::Result<DstType> convert(Context &ctx, ox::BufferView const&src) noexcept {
OX_REQUIRE(out, convert(ctx, src, ox::ModelTypeName_v<DstType>, ox::ModelTypeVersion_v<DstType>)); OX_REQUIRE(out, convert(ctx, src, ox::ModelTypeName_v<DstType>, ox::ModelTypeVersion_v<DstType>));
return std::move(wrapCast<DstType>(out)); return std::move(wrapCast<DstType>(*out));
} }
template<typename DstType> template<typename DstType>

View File

@ -71,9 +71,9 @@ static ox::Error run(
} }
ox::Error run( ox::Error run(
ox::StringView project, ox::StringView const project,
ox::StringView appName, ox::StringView const appName,
ox::StringView projectDataDir, ox::StringView const projectDataDir,
ox::SpanView<ox::CString> args) noexcept { ox::SpanView<ox::CString> args) noexcept {
return studio::run(ox::sfmt("{} {}", project, appName), projectDataDir, args); return studio::run(ox::sfmt("{} {}", project, appName), projectDataDir, args);
} }

View File

@ -7,6 +7,8 @@
#include <imgui.h> #include <imgui.h>
#include <ox/std/def.hpp>
#include <keel/media.hpp> #include <keel/media.hpp>
#include <glutils/glutils.hpp> #include <glutils/glutils.hpp>
#include <turbine/turbine.hpp> #include <turbine/turbine.hpp>
@ -17,6 +19,8 @@
#include "font.hpp" #include "font.hpp"
#include "studioui.hpp" #include "studioui.hpp"
#include <complex>
#ifdef OX_OS_Darwin #ifdef OX_OS_Darwin
#define STUDIO_CTRL "Cmd" #define STUDIO_CTRL "Cmd"
#else #else
@ -54,7 +58,7 @@ void registerModule(Module const*mod) noexcept {
} }
struct StudioConfig { struct StudioConfigV1 {
static constexpr auto TypeName = "net.drinkingtea.studio.StudioConfig"; static constexpr auto TypeName = "net.drinkingtea.studio.StudioConfig";
static constexpr auto TypeVersion = 1; static constexpr auto TypeVersion = 1;
ox::String projectPath; ox::String projectPath;
@ -63,13 +67,65 @@ struct StudioConfig {
bool showProjectExplorer = true; bool showProjectExplorer = true;
}; };
OX_MODEL_BEGIN(StudioConfig) OX_MODEL_BEGIN(StudioConfigV1)
OX_MODEL_FIELD_RENAME(activeTabItemName, active_tab_item_name) OX_MODEL_FIELD_RENAME(activeTabItemName, active_tab_item_name)
OX_MODEL_FIELD_RENAME(projectPath, project_path) OX_MODEL_FIELD_RENAME(projectPath, project_path)
OX_MODEL_FIELD_RENAME(openFiles, open_files) OX_MODEL_FIELD_RENAME(openFiles, open_files)
OX_MODEL_FIELD_RENAME(showProjectExplorer, show_project_explorer) OX_MODEL_FIELD_RENAME(showProjectExplorer, show_project_explorer)
OX_MODEL_END() OX_MODEL_END()
struct StudioConfigV2 {
static constexpr auto TypeName = "net.drinkingtea.studio.StudioConfig";
static constexpr auto TypeVersion = 2;
struct ProjectConfig {
static constexpr auto TypeName = "net.drinkingtea.studio.ProjectConfig";
static constexpr auto TypeVersion = 2;
ox::String projectPath;
ox::String activeTabItemName;
ox::Vector<ox::String> openFiles;
};
ox::Vector<ProjectConfig> projects;
bool showProjectExplorer = true;
[[nodiscard]]
constexpr ProjectConfig const *project() const {
return projects.empty() ? nullptr : &projects[0];
}
[[nodiscard]]
constexpr ProjectConfig *project() {
return projects.empty() ? nullptr : &projects[0];
}
};
OX_MODEL_BEGIN(StudioConfigV2::ProjectConfig)
OX_MODEL_FIELD_RENAME(activeTabItemName, active_tab_item_name)
OX_MODEL_FIELD_RENAME(projectPath, project_path)
OX_MODEL_FIELD_RENAME(openFiles, open_files)
OX_MODEL_END()
OX_MODEL_BEGIN(StudioConfigV2)
OX_MODEL_FIELD(projects)
OX_MODEL_FIELD_RENAME(showProjectExplorer, show_project_explorer)
OX_MODEL_END()
static ox::Error convertStudioConfigV1ToStudioConfigV2(
keel::Context&,
StudioConfigV1 &src,
StudioConfigV2 &dst) noexcept {
dst.projects.emplace_back(StudioConfigV2::ProjectConfig{
.projectPath = std::move(src.projectPath),
.activeTabItemName = std::move(src.activeTabItemName),
.openFiles = std::move(src.openFiles),
});
dst.showProjectExplorer = src.showProjectExplorer;
return {};
}
using StudioConfig = StudioConfigV2;
StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexcept: StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexcept:
m_sctx{*this, ctx}, m_sctx{*this, ctx},
m_tctx{ctx}, m_tctx{ctx},
@ -87,6 +143,9 @@ StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexce
// and hopefully it will change at some point. // and hopefully it will change at some point.
io.Fonts->AddFontFromMemoryTTF(const_cast<uint8_t*>(font.data()), static_cast<int>(font.size()), 13, &fontCfg); io.Fonts->AddFontFromMemoryTTF(const_cast<uint8_t*>(font.data()), static_cast<int>(font.size()), 13, &fontCfg);
} }
auto &kctx = keelCtx(m_tctx);
kctx.converters.emplace_back(keel::Converter::make<convertStudioConfigV1ToStudioConfigV2>());
oxLogError(headerizeConfigFile<StudioConfigV1>(kctx));
turbine::setApplicationData(m_tctx, &m_sctx); turbine::setApplicationData(m_tctx, &m_sctx);
turbine::setShutdownHandler(m_tctx, shutdownHandler); turbine::setShutdownHandler(m_tctx, shutdownHandler);
m_projectExplorer.fileChosen.connect(this, &StudioUI::openFile); m_projectExplorer.fileChosen.connect(this, &StudioUI::openFile);
@ -102,21 +161,14 @@ StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexce
m_newMenu.finished.connect(this, &StudioUI::openFile); m_newMenu.finished.connect(this, &StudioUI::openFile);
m_closeAppConfirm.response.connect(this, &StudioUI::handleCloseAppResponse); m_closeAppConfirm.response.connect(this, &StudioUI::handleCloseAppResponse);
m_closeFileConfirm.response.connect(this, &StudioUI::handleCloseFileResponse); m_closeFileConfirm.response.connect(this, &StudioUI::handleCloseFileResponse);
m_removeRecentProject.dlg.response.connect(this, &StudioUI::handleRemoveRecentProjectResponse);
loadModules(); loadModules();
// open project and files // open project and files
auto const [config, err] = studio::readConfig<StudioConfig>(keelCtx(m_tctx)); auto const [config, err] = studio::readConfig<StudioConfig>(keelCtx(m_tctx));
m_showProjectExplorer = config.showProjectExplorer; m_showProjectExplorer = config.showProjectExplorer;
if (!err) { if (!err) {
auto const openProjErr = openProjectPath(config.projectPath); if (auto const pc = config.project()) {
if (!openProjErr) { oxLogError(openProjectPath(pc->projectPath));
for (auto const&f: config.openFiles) {
auto const openFileErr = openFileActiveTab(f, config.activeTabItemName == f);
if (openFileErr) {
oxErrorf("\nCould not open editor for file:\n\t{}\nReason:\n\t{}\n", f, toStr(openFileErr));
continue;
}
m_activeEditor = m_editors.back().value->get();
}
} }
} else { } else {
if constexpr(!ox::defines::Debug) { if constexpr(!ox::defines::Debug) {
@ -194,6 +246,18 @@ void StudioUI::drawMenu() noexcept {
if (ImGui::MenuItem("Open Project...", STUDIO_CTRL "+O")) { if (ImGui::MenuItem("Open Project...", STUDIO_CTRL "+O")) {
m_taskRunner.add(*ox::make<FileDialogManager>(this, &StudioUI::openProjectPath)); m_taskRunner.add(*ox::make<FileDialogManager>(this, &StudioUI::openProjectPath));
} }
if (ImGui::BeginMenu("Recent Projects", m_recentProjects.size() > 1)) {
for (size_t i = 1; i < m_recentProjects.size(); ++i) {
auto const &p = m_recentProjects[i];
if (ImGui::MenuItem(p.c_str())) {
if (openProjectPath(p)) {
m_removeRecentProject.idx = i;
m_removeRecentProject.dlg.open();
}
}
}
ImGui::EndMenu();
}
if (ImGui::MenuItem( if (ImGui::MenuItem(
"Save", "Save",
STUDIO_CTRL "+S", STUDIO_CTRL "+S",
@ -279,7 +343,9 @@ void StudioUI::drawTabs() noexcept {
if (m_activeEditor != e.get()) [[unlikely]] { if (m_activeEditor != e.get()) [[unlikely]] {
m_activeEditor = e.get(); m_activeEditor = e.get();
studio::editConfig<StudioConfig>(keelCtx(m_tctx), [&](StudioConfig &config) { studio::editConfig<StudioConfig>(keelCtx(m_tctx), [&](StudioConfig &config) {
config.activeTabItemName = m_activeEditor->itemPath(); if (auto const pc = config.project()) {
pc->activeTabItemName = m_activeEditor->itemPath();
}
}); });
turbine::setRefreshWithin(m_tctx, 0); turbine::setRefreshWithin(m_tctx, 0);
} else [[likely]] { } else [[likely]] {
@ -469,9 +535,11 @@ ox::Error StudioUI::handleMoveFile(ox::StringViewCR oldPath, ox::StringViewCR ne
if (f == oldPath) { if (f == oldPath) {
f = newPath; f = newPath;
editConfig<StudioConfig>(keelCtx(m_sctx), [&](StudioConfig &cfg) { editConfig<StudioConfig>(keelCtx(m_sctx), [&](StudioConfig &cfg) {
auto p = find(cfg.openFiles.begin(), cfg.openFiles.end(), oldPath); if (auto const pc = cfg.project()) {
auto p = find(pc->openFiles.begin(), pc->openFiles.end(), oldPath);
*p = newPath; *p = newPath;
cfg.activeTabItemName = newPath; pc->activeTabItemName = newPath;
}
}); });
break; break;
} }
@ -546,8 +614,36 @@ ox::Error StudioUI::openProjectPath(ox::StringParam path) noexcept {
m_openFiles.clear(); m_openFiles.clear();
m_editors.clear(); m_editors.clear();
studio::editConfig<StudioConfig>(keelCtx(m_tctx), [&](StudioConfig &config) { studio::editConfig<StudioConfig>(keelCtx(m_tctx), [&](StudioConfig &config) {
config.projectPath = ox::String(m_project->projectPath()); auto const pcIt = std::find_if(
config.openFiles.clear(); config.projects.begin(), config.projects.end(),
[this](StudioConfig::ProjectConfig const &pc) {
return pc.projectPath == m_project->projectPath();
});
if (pcIt != config.projects.end()) {
auto p = std::move(*pcIt);
std::ignore = config.projects.erase(pcIt);
auto &pc = *config.projects.emplace(0, std::move(p));
for (auto const &f: pc.openFiles) {
auto const openFileErr = openFileActiveTab(f, pc.activeTabItemName == f);
if (openFileErr) {
oxErrorf("\nCould not open editor for file:\n\t{}\nReason:\n\t{}\n", f, toStr(openFileErr));
continue;
}
m_activeEditor = m_editors.back().value->get();
}
} else {
config.projects.emplace(0, StudioConfig::ProjectConfig{
.projectPath = ox::String{m_project->projectPath()},
.activeTabItemName = {},
.openFiles = {},
});
}
config.projects.resize(ox::min<size_t>(10, config.projects.size()));
m_recentProjects.clear();
m_recentProjects.reserve(config.projects.size());
for (auto const &p : config.projects) {
m_recentProjects.emplace_back(p.projectPath);
}
}); });
return m_projectExplorer.refreshProjectTreeModel(); return m_projectExplorer.refreshProjectTreeModel();
} }
@ -593,8 +689,10 @@ ox::Error StudioUI::openFileActiveTab(ox::StringViewCR path, bool const makeActi
} }
// save to config // save to config
studio::editConfig<StudioConfig>(keelCtx(m_tctx), [&path](StudioConfig &config) { studio::editConfig<StudioConfig>(keelCtx(m_tctx), [&path](StudioConfig &config) {
if (!config.openFiles.contains(path)) { if (auto const pc = config.project()) {
config.openFiles.emplace_back(path); if (!pc->openFiles.contains(path)) {
pc->openFiles.emplace_back(path);
}
} }
}); });
return {}; return {};
@ -618,6 +716,22 @@ ox::Error StudioUI::handleCloseFileResponse(ig::PopupResponse const response) no
return {}; return {};
} }
ox::Error StudioUI::handleRemoveRecentProjectResponse(ig::PopupResponse const response) noexcept {
if (response == ig::PopupResponse::OK) {
auto const p = std::move(m_recentProjects[m_removeRecentProject.idx]);
studio::editConfig<StudioConfig>(keelCtx(m_tctx), [&p](StudioConfig &config) {
std::ignore = config.projects.erase(
std::remove_if(
config.projects.begin(), config.projects.end(),
[&p](StudioConfig::ProjectConfig const &pc) {
return pc.projectPath == p;
}));
});
return m_recentProjects.erase(m_removeRecentProject.idx).error;
}
return {};
}
ox::Error StudioUI::closeCurrentFile() noexcept { ox::Error StudioUI::closeCurrentFile() noexcept {
for (auto &e : m_editors) { for (auto &e : m_editors) {
if (m_activeEditor == e.get()) { if (m_activeEditor == e.get()) {
@ -636,7 +750,9 @@ ox::Error StudioUI::closeFile(ox::StringViewCR path) noexcept {
std::ignore = m_openFiles.erase(std::remove(m_openFiles.begin(), m_openFiles.end(), path)); std::ignore = m_openFiles.erase(std::remove(m_openFiles.begin(), m_openFiles.end(), path));
// save to config // save to config
studio::editConfig<StudioConfig>(keelCtx(m_tctx), [&](StudioConfig &config) { studio::editConfig<StudioConfig>(keelCtx(m_tctx), [&](StudioConfig &config) {
std::ignore = config.openFiles.erase(std::remove(config.openFiles.begin(), config.openFiles.end(), path)); if (auto const pc = config.project()) {
std::ignore = pc->openFiles.erase(std::remove(pc->openFiles.begin(), pc->openFiles.end(), path));
}
}); });
return {}; return {};
} }

View File

@ -44,6 +44,7 @@ class StudioUI: public ox::SignalHandler {
bool m_closeActiveTab{}; bool m_closeActiveTab{};
ox::Vector<ox::Pair<ox::String>> m_queuedMoves; ox::Vector<ox::Pair<ox::String>> m_queuedMoves;
ox::Vector<ox::Pair<ox::String>> m_queuedDirMoves; ox::Vector<ox::Pair<ox::String>> m_queuedDirMoves;
ox::Vector<ox::String> m_recentProjects;
NewMenu m_newMenu{keelCtx(m_tctx)}; NewMenu m_newMenu{keelCtx(m_tctx)};
AboutPopup m_aboutPopup{m_tctx}; AboutPopup m_aboutPopup{m_tctx};
DeleteConfirmation m_deleteConfirmation; DeleteConfirmation m_deleteConfirmation;
@ -57,7 +58,11 @@ class StudioUI: public ox::SignalHandler {
MakeCopyPopup m_copyFilePopup; MakeCopyPopup m_copyFilePopup;
RenameFile m_renameFile; RenameFile m_renameFile;
NewProject m_newProject{m_projectDataDir}; NewProject m_newProject{m_projectDataDir};
ox::Array<Widget*, 10> const m_widgets { struct {
ig::QuestionPopup dlg{"Remove From Recents?", "Unable to load project. Remove from recent projects?"};
size_t idx{};
} m_removeRecentProject;
ox::Array<Widget*, 11> const m_widgets {
&m_closeFileConfirm, &m_closeFileConfirm,
&m_closeAppConfirm, &m_closeAppConfirm,
&m_copyFilePopup, &m_copyFilePopup,
@ -68,6 +73,7 @@ class StudioUI: public ox::SignalHandler {
&m_newDirDialog, &m_newDirDialog,
&m_renameFile, &m_renameFile,
&m_messagePopup, &m_messagePopup,
&m_removeRecentProject.dlg,
}; };
bool m_showProjectExplorer = true; bool m_showProjectExplorer = true;
struct NavAction { struct NavAction {
@ -144,6 +150,8 @@ class StudioUI: public ox::SignalHandler {
ox::Error handleCloseFileResponse(ig::PopupResponse response) noexcept; ox::Error handleCloseFileResponse(ig::PopupResponse response) noexcept;
ox::Error handleRemoveRecentProjectResponse(ig::PopupResponse response) noexcept;
ox::Error closeCurrentFile() noexcept; ox::Error closeCurrentFile() noexcept;
ox::Error closeFile(ox::StringViewCR path) noexcept; ox::Error closeFile(ox::StringViewCR path) noexcept;

View File

@ -4,21 +4,21 @@
#pragma once #pragma once
#include <ox/claw/claw.hpp>
#include <ox/fs/fs.hpp> #include <ox/fs/fs.hpp>
#include <ox/model/typenamecatcher.hpp> #include <ox/model/typenamecatcher.hpp>
#include <ox/oc/oc.hpp>
#include <ox/std/buffer.hpp> #include <ox/std/buffer.hpp>
#include <ox/std/defines.hpp> #include <ox/std/defines.hpp>
#include <ox/std/fmt.hpp> #include <ox/std/fmt.hpp>
#include <ox/std/trace.hpp>
#include <ox/std/string.hpp> #include <ox/std/string.hpp>
#include <ox/std/trace.hpp>
#include <keel/context.hpp> #include <keel/context.hpp>
namespace studio { namespace studio {
namespace detail { namespace detail {
inline ox::String slashesToPct(ox::StringView str) noexcept { inline ox::String slashesToPct(ox::StringViewCR str) noexcept {
auto out = ox::String{str}; auto out = ox::String{str};
for (auto&c: out) { for (auto&c: out) {
if (c == '/' || c == '\\') { if (c == '/' || c == '\\') {
@ -30,78 +30,102 @@ inline ox::String slashesToPct(ox::StringView str) noexcept {
} }
[[nodiscard]] [[nodiscard]]
ox::String configPath(keel::Context const&ctx) noexcept; ox::String configPath(keel::Context const&kctx) noexcept;
template<typename T> template<typename T>
ox::Result<T> readConfig(keel::Context &ctx, ox::StringViewCR name) noexcept { ox::Result<T> readConfig(keel::Context &kctx, ox::StringViewCR name) noexcept {
oxAssert(name != "", "Config type has no TypeName"); oxAssert(name != "", "Config type has no TypeName");
auto const path = ox::sfmt("/{}.json", detail::slashesToPct(name)); auto const path = ox::sfmt("/{}.json", detail::slashesToPct(name));
ox::PassThroughFS fs(configPath(ctx)); ox::PassThroughFS fs(configPath(kctx));
auto const [buff, err] = fs.read(path); auto const [buff, err] = fs.read(path);
if (err) { if (err) {
//oxErrf("Could not read config file: {} - {}\n", path, toStr(err)); //oxErrf("Could not read config file: {} - {}\n", path, toStr(err));
return err; return err;
} }
return ox::readOC<T>(buff); OX_REQUIRE(id, ox::readClawTypeId(buff));
ox::Result<T> out;
if (id != ox::ModelTypeId_v<T>) {
out = keel::convert<T>(kctx, buff);
} else {
out = ox::readClaw<T>(buff);
}
OX_RETURN_ERROR(out);
OX_RETURN_ERROR(keel::ensureValid(out.value));
return out;
} }
template<typename T> template<typename T>
ox::Result<T> readConfig(keel::Context &ctx) noexcept { ox::Result<T> readConfig(keel::Context &kctx) noexcept {
constexpr auto TypeName = ox::requireModelTypeName<T>(); constexpr auto TypeName = ox::requireModelTypeName<T>();
return readConfig<T>(ctx, TypeName); return readConfig<T>(kctx, TypeName);
} }
template<typename T> template<typename T>
ox::Error writeConfig(keel::Context &ctx, ox::StringViewCR name, T const&data) noexcept { ox::Error writeConfig(keel::Context &kctx, ox::StringViewCR name, T const&data) noexcept {
oxAssert(name != "", "Config type has no TypeName"); oxAssert(name != "", "Config type has no TypeName");
auto const path = ox::sfmt("/{}.json", detail::slashesToPct(name)); auto const path = ox::sfmt("/{}.json", detail::slashesToPct(name));
ox::PassThroughFS fs(configPath(ctx)); ox::PassThroughFS fs(configPath(kctx));
if (auto const err = fs.mkdir("/", true)) { if (auto const err = fs.mkdir("/", true)) {
//oxErrf("Could not create config directory: {} - {}\n", path, toStr(err)); //oxErrf("Could not create config directory: {} - {}\n", path, toStr(err));
return err; return err;
} }
OX_REQUIRE_M(buff, ox::writeOC(data)); OX_REQUIRE_M(buff, ox::writeClaw(data, ox::ClawFormat::Organic));
*buff.back().value = '\n'; *buff.back().value = '\n';
if (auto const err = fs.write(path, buff.data(), buff.size())) { if (auto const err = fs.write(path, buff.data(), buff.size())) {
//oxErrf("Could not read config file: {} - {}\n", path, toStr(err)); //oxErrf("Could not read config file: {} - {}\n", path, toStr(err));
return ox::Error(2, "Could not read config file"); return ox::Error{2, "Could not read config file"};
} }
return {}; return {};
} }
template<typename T> template<typename T>
ox::Error writeConfig(keel::Context &ctx, T const&data) noexcept { ox::Error writeConfig(keel::Context &kctx, T const&data) noexcept {
constexpr auto TypeName = ox::requireModelTypeName<T>(); constexpr auto TypeName = ox::requireModelTypeName<T>();
return writeConfig(ctx, TypeName, data); return writeConfig(kctx, TypeName, data);
} }
template<typename T, typename Func> template<typename T, typename Func>
void openConfig(keel::Context &ctx, ox::StringViewCR name, Func f) noexcept { void openConfig(keel::Context &kctx, ox::StringViewCR name, Func f) noexcept {
oxAssert(name != "", "Config type has no TypeName"); oxAssert(name != "", "Config type has no TypeName");
auto const [c, err] = readConfig<T>(ctx, name); auto const [c, err] = readConfig<T>(kctx, name);
oxLogError(err); oxLogError(err);
f(c); f(c);
} }
template<typename T, typename Func> template<typename T, typename Func>
void openConfig(keel::Context &ctx, Func f) noexcept { void openConfig(keel::Context &kctx, Func f) noexcept {
constexpr auto TypeName = ox::requireModelTypeName<T>(); constexpr auto TypeName = ox::requireModelTypeName<T>();
openConfig<T>(ctx, TypeName, f); openConfig<T>(kctx, TypeName, f);
} }
template<typename T, typename Func> template<typename T, typename Func>
void editConfig(keel::Context &ctx, ox::StringViewCR name, Func f) noexcept { void editConfig(keel::Context &kctx, ox::StringViewCR name, Func f) noexcept {
oxAssert(name != "", "Config type has no TypeName"); oxAssert(name != "", "Config type has no TypeName");
auto [c, err] = readConfig<T>(ctx, name); auto [c, err] = readConfig<T>(kctx, name);
oxLogError(err); oxLogError(err);
f(c); f(c);
oxLogError(writeConfig(ctx, name, c)); oxLogError(writeConfig(kctx, name, c));
} }
template<typename T, typename Func> template<typename T, typename Func>
void editConfig(keel::Context &ctx, Func f) noexcept { void editConfig(keel::Context &kctx, Func f) noexcept {
constexpr auto TypeName = ox::requireModelTypeName<T>(); constexpr auto TypeName = ox::requireModelTypeName<T>();
editConfig<T>(ctx, TypeName, f); editConfig<T>(kctx, TypeName, f);
}
/**
* Older config files didn't use ClawHeaders, so they can't
* use the normal conversion system.
* Functions like this shouldn't be necessary moving forward.
*/
template<typename T>
ox::Error headerizeConfigFile(keel::Context &kctx, ox::StringViewCR name = ox::ModelTypeName_v<T>) noexcept {
auto const path = ox::sfmt("/{}.json", name);
ox::PassThroughFS fs(configPath(kctx));
OX_REQUIRE_M(buff, fs.read(path));
OX_REQUIRE_M(cv1, ox::readOC<T>(buff));
OX_RETURN_ERROR(ox::writeClaw(cv1, ox::ClawFormat::Organic).moveTo(buff));
return fs.write(path, buff);
} }
} }