Compare commits

..

2 Commits

Author SHA1 Message Date
46a51e1209 save work
All checks were successful
Build / build (push) Successful in 1m21s
2025-06-08 17:02:00 -05:00
55ef2cea51 [studio] Add Back/Forward navigation 2025-06-08 17:02:00 -05:00
37 changed files with 151 additions and 351 deletions

View File

@@ -52,25 +52,3 @@ configure-gba:
.PHONY: configure-gba-debug .PHONY: configure-gba-debug
configure-gba-debug: configure-gba-debug:
${BC_CMD_SETUP_BUILD} --toolchain=deps/gbabuildcore/cmake/modules/GBA.cmake --target=gba --current_build=0 --build_type=debug --build_root=${BC_VAR_BUILD_PATH} ${BC_CMD_SETUP_BUILD} --toolchain=deps/gbabuildcore/cmake/modules/GBA.cmake --target=gba --current_build=0 --build_type=debug --build_root=${BC_VAR_BUILD_PATH}
.PHONY: loc
loc:
${BC_PY3} util/scripts/loc.py \
--search-dirs \
src \
deps/ox/src \
deps/buildcore \
deps/gbabuildcore \
deps/glutils \
deps/teagba \
--include-exts \
.cpp \
.hpp \
.py \
.s \
.cmake \
--exclude-paths \
deps/teagba/src/gba_crt0.s \
src/olympic/studio/applib/src/font.cpp \
src/olympic/studio/applib/src/font.hpp \
src/nostalgia/studio/icondata.cpp

View File

@@ -91,7 +91,6 @@ ox::Error LoggerConn::sendInit(const InitTraceMsg &msg) noexcept {
} }
void LoggerConn::msgSend() noexcept { void LoggerConn::msgSend() noexcept {
try {
while (true) { while (true) {
std::unique_lock lk(m_waitMut); std::unique_lock lk(m_waitMut);
m_waitCond.wait(lk); m_waitCond.wait(lk);
@@ -110,10 +109,6 @@ void LoggerConn::msgSend() noexcept {
std::ignore = send(tmp.data(), read); std::ignore = send(tmp.data(), read);
} }
} }
} catch (std::exception const &e) {
oxErrf("Exception in logger thread: {}\n", e.what());
oxAssert(false, "logger thread exception");
}
} }
} }

View File

@@ -104,16 +104,13 @@ constexpr ox::Result<int> strToInt(StringViewCR str) noexcept {
OX_ALLOW_UNSAFE_BUFFERS_BEGIN OX_ALLOW_UNSAFE_BUFFERS_BEGIN
int total = 0; int total = 0;
int multiplier = 1; int multiplier = 1;
if (str.len() == 0) [[unlikely]] {
return Error{1, "Empty string passed to strToInt"};
}
for (auto i = static_cast<int64_t>(str.len()) - 1; i != -1; --i) { for (auto i = static_cast<int64_t>(str.len()) - 1; i != -1; --i) {
auto s = static_cast<std::size_t>(i); auto s = static_cast<std::size_t>(i);
if (str[s] >= '0' && str[s] <= '9') { if (str[s] >= '0' && str[s] <= '9') {
total += (str[s] - '0') * multiplier; total += (str[s] - '0') * multiplier;
multiplier *= 10; multiplier *= 10;
} else { } else {
return ox::Error{1}; return ox::Error(1);
} }
} }
return total; return total;

View File

@@ -1,20 +1,14 @@
# d2025.06.0 # d2025.06.0
* Add ability to remember recent projects in config * Add ability to remember recent projects in config
* Add navigation support (back and forward)
* Fix file deletion to close file even if not active
* Fix file copy to work when creating a copy with the name of a previously
deleted file
* Fix crash that could occur after switching projects
* Make file picker popup accept on double click of a file
* TileSheetEditor: Fix copy/cut/paste enablement when there is no selection
* TileSheetEditor: Fix manual redo of draw actions, fix drawing to pixel 0, 0
as first action
* TileSheetEditor: Fix draw command to work on same pixel after switching
subsheets
* PaletteEditor: Add RGB key shortcuts for focusing color channels * PaletteEditor: Add RGB key shortcuts for focusing color channels
* PaletteEditor: Add color preview to color editor * PaletteEditor: Add color preview to color editor
# d2025.05.2
* TileSheetEditor: Fix manual redo of draw actions, fix drawing to pixel 0, 0 as first action (cce5f52f96511694afd98f0b9b6b1f19c06ecd20)
* TileSheetEditor: Fix draw command to work on same pixel after switching subsheets (514cb978351ee4b0a5335c22a506a6d9f608f0a7)
# d2025.05.1 # d2025.05.1
* TileSheetEditor: Fix overrun errors when switching subsheets, clear selection * TileSheetEditor: Fix overrun errors when switching subsheets, clear selection

View File

@@ -8,7 +8,7 @@
namespace nostalgia::gfx { namespace nostalgia::gfx {
RemovePageCommand::RemovePageCommand(Palette &pal, size_t const idx) noexcept: RemovePageCommand::RemovePageCommand(Palette &pal, size_t idx) noexcept:
m_pal(pal), m_pal(pal),
m_idx(idx) {} m_idx(idx) {}

View File

@@ -9,13 +9,13 @@ namespace nostalgia::gfx {
UpdateColorCommand::UpdateColorCommand( UpdateColorCommand::UpdateColorCommand(
Palette &pal, Palette &pal,
size_t const page, size_t page,
size_t const idx, size_t idx,
Color16 const newColor): Color16 newColor):
m_pal{pal}, m_pal(pal),
m_page{page}, m_page(page),
m_idx{idx}, m_idx(idx),
m_altColor{newColor} { m_altColor(newColor) {
if (color(m_pal, m_page, m_idx) == newColor) { if (color(m_pal, m_page, m_idx) == newColor) {
throw studio::NoChangesException(); throw studio::NoChangesException();
} }

View File

@@ -13,7 +13,7 @@ AddSubSheetCommand::AddSubSheetCommand(
auto &parent = getSubSheet(m_img, m_parentIdx); auto &parent = getSubSheet(m_img, m_parentIdx);
if (!parent.subsheets.empty()) { if (!parent.subsheets.empty()) {
auto idx = m_parentIdx; auto idx = m_parentIdx;
idx.emplace_back(static_cast<uint32_t>(parent.subsheets.size())); idx.emplace_back(parent.subsheets.size());
m_addedSheets.push_back(idx); m_addedSheets.push_back(idx);
} else { } else {
auto idx = m_parentIdx; auto idx = m_parentIdx;

View File

@@ -13,19 +13,20 @@ DeleteTilesCommand::DeleteTilesCommand(
TileSheet::SubSheetIdx idx, TileSheet::SubSheetIdx idx,
std::size_t const tileIdx, std::size_t const tileIdx,
std::size_t const tileCnt) noexcept: std::size_t const tileCnt) noexcept:
m_img{img}, m_img(img),
m_idx{std::move(idx)}, m_idx(std::move(idx)) {
m_deletePos{tileIdx * PixelsPerTile}, constexpr unsigned bytesPerTile = PixelsPerTile;
m_deleteSz{tileCnt * PixelsPerTile}, m_deletePos = tileIdx * bytesPerTile;
m_deletedPixels{[this] { m_deleteSz = tileCnt * bytesPerTile;
ox::Vector<uint8_t> deletedPixels(m_deleteSz); m_deletedPixels.resize(m_deleteSz);
// copy pixels to be erased // copy pixels to be erased
auto const &s = getSubSheet(m_img, m_idx); {
auto const dst = deletedPixels.begin(); auto &s = getSubSheet(m_img, m_idx);
auto const src = s.pixels.begin() + m_deletePos; auto dst = m_deletedPixels.begin();
auto src = s.pixels.begin() + m_deletePos;
ox::copy_n(src, m_deleteSz, dst); ox::copy_n(src, m_deleteSz, dst);
return deletedPixels; }
}()} {} }
ox::Error DeleteTilesCommand::redo() noexcept { ox::Error DeleteTilesCommand::redo() noexcept {
auto &s = getSubSheet(m_img, m_idx); auto &s = getSubSheet(m_img, m_idx);

View File

@@ -11,10 +11,10 @@ namespace nostalgia::gfx {
class DeleteTilesCommand: public TileSheetCommand { class DeleteTilesCommand: public TileSheetCommand {
private: private:
TileSheet &m_img; TileSheet &m_img;
TileSheet::SubSheetIdx const m_idx; TileSheet::SubSheetIdx m_idx;
std::size_t const m_deletePos = 0; std::size_t m_deletePos = 0;
std::size_t const m_deleteSz = 0; std::size_t m_deleteSz = 0;
ox::Vector<uint8_t> const m_deletedPixels; ox::Vector<uint8_t> m_deletedPixels = {};
public: public:
DeleteTilesCommand( DeleteTilesCommand(

View File

@@ -62,11 +62,11 @@ constexpr void iterateLine(ox::Point const &a, ox::Point const &b, auto const &f
DrawCommand::DrawCommand( DrawCommand::DrawCommand(
TileSheet &img, TileSheet &img,
TileSheet::SubSheetIdx subSheetIdx, TileSheet::SubSheetIdx subSheetIdx,
std::size_t const idx, std::size_t idx,
int const palIdx) noexcept: int const palIdx) noexcept:
m_img{img}, m_img(img),
m_subSheetIdx{std::move(subSheetIdx)}, m_subSheetIdx(std::move(subSheetIdx)),
m_palIdx{palIdx} { m_palIdx(palIdx) {
auto &subsheet = getSubSheet(m_img, m_subSheetIdx); auto &subsheet = getSubSheet(m_img, m_subSheetIdx);
m_changes.emplace_back(static_cast<uint32_t>(idx), getPixel(subsheet, idx)); m_changes.emplace_back(static_cast<uint32_t>(idx), getPixel(subsheet, idx));
} }

View File

@@ -12,19 +12,19 @@ InsertTilesCommand::InsertTilesCommand(
std::size_t const tileIdx, std::size_t const tileIdx,
std::size_t const tileCnt) noexcept: std::size_t const tileCnt) noexcept:
m_img{img}, m_img{img},
m_idx{std::move(idx)}, m_idx{std::move(idx)} {
m_insertPos{tileIdx * PixelsPerTile}, m_insertPos = tileIdx * PixelsPerTile;
m_insertCnt{tileCnt * PixelsPerTile}, m_insertCnt = tileCnt * PixelsPerTile;
m_insertedPixels{[this] { m_deletedPixels.resize(m_insertCnt);
ox::Vector<uint8_t> insertedPixels(m_insertCnt);
// copy pixels to be erased // copy pixels to be erased
{
auto &s = getSubSheet(m_img, m_idx); auto &s = getSubSheet(m_img, m_idx);
auto &p = s.pixels; auto &p = s.pixels;
auto const dst = insertedPixels.begin(); auto const dst = m_deletedPixels.begin();
auto const src = p.begin() + p.size() - m_insertCnt; auto const src = p.begin() + p.size() - m_insertCnt;
ox::copy_n(src, m_insertCnt, dst); ox::copy_n(src, m_insertCnt, dst);
return insertedPixels; }
}()} {} }
OX_ALLOW_UNSAFE_BUFFERS_BEGIN OX_ALLOW_UNSAFE_BUFFERS_BEGIN
@@ -52,7 +52,7 @@ ox::Error InsertTilesCommand::undo() noexcept {
auto const src = &p[srcIdx]; auto const src = &p[srcIdx];
ox::memmove(dst1, src, sz); ox::memmove(dst1, src, sz);
} }
ox::memcpy(dst2, m_insertedPixels.data(), m_insertedPixels.size()); ox::memcpy(dst2, m_deletedPixels.data(), m_deletedPixels.size());
return {}; return {};
} }

View File

@@ -11,10 +11,10 @@ namespace nostalgia::gfx {
class InsertTilesCommand: public TileSheetCommand { class InsertTilesCommand: public TileSheetCommand {
private: private:
TileSheet &m_img; TileSheet &m_img;
TileSheet::SubSheetIdx const m_idx; TileSheet::SubSheetIdx m_idx;
std::size_t const m_insertPos{}; std::size_t m_insertPos = 0;
std::size_t const m_insertCnt{}; std::size_t m_insertCnt = 0;
ox::Vector<uint8_t> const m_insertedPixels; ox::Vector<uint8_t> m_deletedPixels = {};
public: public:
InsertTilesCommand( InsertTilesCommand(

View File

@@ -15,6 +15,7 @@ UpdateSubSheetCommand::UpdateSubSheetCommand(
m_img{img}, m_img{img},
m_idx{std::move(idx)}, m_idx{std::move(idx)},
m_sheet{getSubSheet(m_img, m_idx)} { m_sheet{getSubSheet(m_img, m_idx)} {
m_sheet = getSubSheet(m_img, m_idx);
m_sheet.name = std::move(name); m_sheet.name = std::move(name);
OX_THROW_ERROR(resizeSubsheet(m_sheet, {cols, rows})); OX_THROW_ERROR(resizeSubsheet(m_sheet, {cols, rows}));
} }

View File

@@ -11,7 +11,7 @@ namespace nostalgia::gfx {
class UpdateSubSheetCommand: public TileSheetCommand { class UpdateSubSheetCommand: public TileSheetCommand {
private: private:
TileSheet &m_img; TileSheet &m_img;
TileSheet::SubSheetIdx const m_idx; TileSheet::SubSheetIdx m_idx;
TileSheet::SubSheet m_sheet; TileSheet::SubSheet m_sheet;
public: public:

View File

@@ -192,9 +192,6 @@ void TileSheetEditorImGui::keyStateChanged(turbine::Key const key, bool const do
} }
void TileSheetEditorImGui::draw(studio::Context&) noexcept { void TileSheetEditorImGui::draw(studio::Context&) noexcept {
setCopyEnabled(m_model.hasSelection());
setCutEnabled(m_model.hasSelection());
setPasteEnabled(m_model.hasSelection());
if (ig::mainWinHasFocus() && m_tool == TileSheetTool::Select) { if (ig::mainWinHasFocus() && m_tool == TileSheetTool::Select) {
if (ImGui::IsKeyDown(ImGuiKey_ModCtrl) && !m_palPathFocused) { if (ImGui::IsKeyDown(ImGuiKey_ModCtrl) && !m_palPathFocused) {
if (ImGui::IsKeyPressed(ImGuiKey_A)) { if (ImGui::IsKeyPressed(ImGuiKey_A)) {
@@ -279,7 +276,7 @@ void TileSheetEditorImGui::draw(studio::Context&) noexcept {
auto insertOnIdx = m_model.activeSubSheetIdx(); auto insertOnIdx = m_model.activeSubSheetIdx();
auto const &parent = m_model.activeSubSheet(); auto const &parent = m_model.activeSubSheet();
m_model.addSubsheet(insertOnIdx); m_model.addSubsheet(insertOnIdx);
insertOnIdx.emplace_back(static_cast<uint32_t>(parent.subsheets.size() - 1)); insertOnIdx.emplace_back(parent.subsheets.size() - 1);
setActiveSubsheet(insertOnIdx); setActiveSubsheet(insertOnIdx);
} }
ImGui::SameLine(); ImGui::SameLine();

View File

@@ -315,10 +315,6 @@ void TileSheetEditorModel::clearSelection() noexcept {
m_selection.reset(); m_selection.reset();
} }
bool TileSheetEditorModel::hasSelection() const noexcept {
return m_selection.has_value();
}
bool TileSheetEditorModel::updated() const noexcept { bool TileSheetEditorModel::updated() const noexcept {
return m_updated; return m_updated;
} }

View File

@@ -118,9 +118,6 @@ class TileSheetEditorModel final: public ox::SignalHandler {
void clearSelection() noexcept; void clearSelection() noexcept;
[[nodiscard]]
bool hasSelection() const noexcept;
[[nodiscard]] [[nodiscard]]
bool updated() const noexcept; bool updated() const noexcept;

View File

@@ -15,7 +15,7 @@ target_link_libraries(
target_compile_definitions( target_compile_definitions(
NostalgiaStudio PUBLIC NostalgiaStudio PUBLIC
OLYMPIC_APP_VERSION="d2025.06.0" OLYMPIC_APP_VERSION="dev build"
) )
install( install(

View File

@@ -18,7 +18,7 @@
<string>APPL</string> <string>APPL</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>d2025.06.0</string> <string>dev build</string>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>12.0.0</string> <string>12.0.0</string>

View File

@@ -28,15 +28,10 @@ class StudioUIDrawer: public turbine::gl::Drawer {
}; };
static void keyEventHandler(turbine::Context &ctx, turbine::Key key, bool down) noexcept { static void keyEventHandler(turbine::Context &ctx, turbine::Key key, bool down) noexcept {
auto const sctx = turbine::applicationData<studio::Context>(ctx); auto sctx = turbine::applicationData<studio::Context>(ctx);
sctx->ui.handleKeyEvent(key, down); sctx->ui.handleKeyEvent(key, down);
} }
static void mouseButtonEventHandler(turbine::Context &ctx, int const btn, bool const down) noexcept {
auto const sctx = turbine::applicationData<studio::Context>(ctx);
sctx->ui.handleMouseButtonEvent(btn, down);
}
[[nodiscard]] [[nodiscard]]
ox::Vector<ox::SpanView<uint8_t>> WindowIcons() noexcept; ox::Vector<ox::SpanView<uint8_t>> WindowIcons() noexcept;
@@ -48,7 +43,6 @@ static ox::Error runApp(
oxLogError(turbine::setWindowIcon(*ctx, WindowIcons())); oxLogError(turbine::setWindowIcon(*ctx, WindowIcons()));
turbine::setWindowTitle(*ctx, keelCtx(*ctx).appName); turbine::setWindowTitle(*ctx, keelCtx(*ctx).appName);
turbine::setKeyEventHandler(*ctx, keyEventHandler); turbine::setKeyEventHandler(*ctx, keyEventHandler);
turbine::setMouseButtonEventHandler(*ctx, mouseButtonEventHandler);
turbine::requireRefreshWithin(*ctx, 0); turbine::requireRefreshWithin(*ctx, 0);
StudioUI ui(*ctx, projectDataDir); StudioUI ui(*ctx, projectDataDir);
StudioUIDrawer drawer(ui); StudioUIDrawer drawer(ui);

View File

@@ -15,11 +15,7 @@ namespace studio {
AboutPopup::AboutPopup(turbine::Context &ctx) noexcept: AboutPopup::AboutPopup(turbine::Context &ctx) noexcept:
Popup("About"), Popup("About"),
#ifdef DEBUG
m_text{sfmt("{} [DEBUG] - {}", keelCtx(ctx).appName, olympic::appVersion)} {
#else
m_text{sfmt("{} - {}", keelCtx(ctx).appName, olympic::appVersion)} { m_text{sfmt("{} - {}", keelCtx(ctx).appName, olympic::appVersion)} {
#endif
} }
void AboutPopup::draw(Context &sctx) noexcept { void AboutPopup::draw(Context &sctx) noexcept {

View File

@@ -80,9 +80,9 @@ void NewMenu::addItemMaker(ox::UPtr<ItemMaker> &&im) noexcept {
}); });
} }
void NewMenu::installItemTemplate(ox::UPtr<ItemTemplate> &&tmplt) noexcept { void NewMenu::installItemTemplate(ox::UPtr<ItemTemplate> &tmplt) noexcept {
for (auto const&im : m_types) { for (auto const&im : m_types) {
if (im->installTemplate(std::move(tmplt))) { if (im->installTemplate(tmplt)) {
break; break;
} }
} }

View File

@@ -72,7 +72,7 @@ class NewMenu final: public Popup {
void addItemMaker(ox::UPtr<ItemMaker> &&im) noexcept; void addItemMaker(ox::UPtr<ItemMaker> &&im) noexcept;
void installItemTemplate(ox::UPtr<ItemTemplate> &&tmplt) noexcept; void installItemTemplate(ox::UPtr<ItemTemplate> &tmplt) noexcept;
private: private:
void drawNewItemType(Context const&sctx) noexcept; void drawNewItemType(Context const&sctx) noexcept;

View File

@@ -114,8 +114,9 @@ static ox::Error convertStudioConfigV1ToStudioConfigV2(
using StudioConfig = StudioConfigV2; using StudioConfig = StudioConfigV2;
StudioUI::StudioUI(turbine::Context &tctx, ox::StringParam projectDataDir) noexcept: StudioUI::StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexcept:
m_sctx{*this, tctx}, m_sctx{*this, ctx},
m_tctx{ctx},
m_projectDataDir{std::move(projectDataDir)} { m_projectDataDir{std::move(projectDataDir)} {
{ {
ImFontConfig fontCfg; ImFontConfig fontCfg;
@@ -126,7 +127,7 @@ StudioUI::StudioUI(turbine::Context &tctx, ox::StringParam projectDataDir) noexc
// but AddFontFromMemoryTTF requires a mutable buffer. // but AddFontFromMemoryTTF requires a mutable buffer.
// However, setting fontCfg.FontDataOwnedByAtlas ensures // However, setting fontCfg.FontDataOwnedByAtlas ensures
// that it will still be treated as const. // that it will still be treated as const.
// ImGui documentation recognizes that this is a bad design, // ImGui documentation recognize that this is a bad design,
// 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);
} }
@@ -177,16 +178,6 @@ void StudioUI::handleKeyEvent(turbine::Key const key, bool const down) noexcept
} }
} }
void StudioUI::handleMouseButtonEvent(int const btn, bool const down) noexcept {
if (down) {
if (btn == 3) { // back button
navigateBack(m_sctx);
} else if (btn == 4) { // forward button
navigateForward(m_sctx);
}
}
}
void StudioUI::handleNavigationChange(ox::StringParam path, ox::StringParam navArgs) noexcept { void StudioUI::handleNavigationChange(ox::StringParam path, ox::StringParam navArgs) noexcept {
m_navAction.emplace(std::move(path), std::move(navArgs)); m_navAction.emplace(std::move(path), std::move(navArgs));
} }
@@ -312,13 +303,11 @@ void StudioUI::drawMenu() noexcept {
ImGui::EndMenu(); ImGui::EndMenu();
} }
if (ImGui::BeginMenu("Navigate")) { if (ImGui::BeginMenu("Navigate")) {
constexpr auto backShortcut = ox::defines::OS == ox::OS::Darwin ? "Cmd+[" : "Alt+Left Arrow"; if (ImGui::MenuItem("Back", STUDIO_CTRL "+{", false, m_sctx.navIdx > 1)) {
constexpr auto fwdShortcut = ox::defines::OS == ox::OS::Darwin ? "Cmd+]" : "Alt+Right Arrow";
if (ImGui::MenuItem("Back", backShortcut, false, m_sctx.navIdx > 1)) {
navigateBack(m_sctx); navigateBack(m_sctx);
} }
if (ImGui::MenuItem( if (ImGui::MenuItem(
"Forward", fwdShortcut, false, "Forward", STUDIO_CTRL "+}", false,
m_sctx.navIdx < m_sctx.navStack.size())) { m_sctx.navIdx < m_sctx.navStack.size())) {
navigateForward(m_sctx); navigateForward(m_sctx);
} }
@@ -377,6 +366,7 @@ void StudioUI::drawTabs() noexcept {
} }
if (m_closeActiveTab) [[unlikely]] { if (m_closeActiveTab) [[unlikely]] {
ImGui::SetTabItemClosed(e->itemDisplayName().c_str()); ImGui::SetTabItemClosed(e->itemDisplayName().c_str());
} else if (open) [[likely]] { } else if (open) [[likely]] {
e->draw(m_sctx); e->draw(m_sctx);
} }
@@ -419,6 +409,10 @@ void StudioUI::drawTabs() noexcept {
if (!openFile(m_navAction->path)) { if (!openFile(m_navAction->path)) {
m_activeEditor->navigateTo(m_navAction->args); m_activeEditor->navigateTo(m_navAction->args);
m_navAction.reset(); m_navAction.reset();
} else {
//auto const i = m_sctx.navIdx - 1;
//oxDebugf("deleting {}", m_sctx.navStack[i].filePath);
//std::ignore = m_sctx.navStack.erase(i);
} }
} }
} }
@@ -438,7 +432,7 @@ void StudioUI::loadModule(Module const &mod) noexcept {
} }
auto tmplts = mod.itemTemplates(m_sctx); auto tmplts = mod.itemTemplates(m_sctx);
for (auto &t : tmplts) { for (auto &t : tmplts) {
m_newMenu.installItemTemplate(std::move(t)); m_newMenu.installItemTemplate(t);
} }
} }
@@ -589,36 +583,24 @@ ox::Error StudioUI::handleMoveFile(ox::StringViewCR oldPath, ox::StringViewCR ne
ox::Error StudioUI::handleDeleteDir(ox::StringViewCR path) noexcept { ox::Error StudioUI::handleDeleteDir(ox::StringViewCR path) noexcept {
auto const p = sfmt("{}/", path); auto const p = sfmt("{}/", path);
std::ignore = m_editors.erase( for (auto &e : m_editors) {
std::remove_if(
m_editors.begin(), m_editors.end(),
[&](ox::UPtr<BaseEditor> const &e) {
if (beginsWith(e->itemPath(), p)) { if (beginsWith(e->itemPath(), p)) {
oxLogError(closeFile(path)); oxLogError(closeFile(path));
if (e.get() != m_activeEditor) {
return true;
}
m_closeActiveTab = true; m_closeActiveTab = true;
break;
}
} }
return false;
}));
return m_projectExplorer.refreshProjectTreeModel(); return m_projectExplorer.refreshProjectTreeModel();
} }
ox::Error StudioUI::handleDeleteFile(ox::StringViewCR path) noexcept { ox::Error StudioUI::handleDeleteFile(ox::StringViewCR path) noexcept {
std::ignore = m_editors.erase( for (auto &e : m_editors) {
std::remove_if(
m_editors.begin(), m_editors.end(),
[&](ox::UPtr<BaseEditor> const &e) {
if (path == e->itemPath()) { if (path == e->itemPath()) {
oxLogError(closeFile(path)); oxLogError(closeFile(path));
if (e.get() != m_activeEditor) {
return true;
}
m_closeActiveTab = true; m_closeActiveTab = true;
break;
}
} }
return false;
}));
return m_projectExplorer.refreshProjectTreeModel(); return m_projectExplorer.refreshProjectTreeModel();
} }
@@ -649,9 +631,6 @@ ox::Error StudioUI::openProjectPath(ox::StringParam path) noexcept {
ox::make_unique_catch<Project>(keelCtx(m_tctx), std::move(path), m_projectDataDir) ox::make_unique_catch<Project>(keelCtx(m_tctx), std::move(path), m_projectDataDir)
.moveTo(m_project)); .moveTo(m_project));
m_sctx.project = m_project.get(); m_sctx.project = m_project.get();
m_activeEditor = nullptr;
m_activeEditorOnLastDraw = nullptr;
m_activeEditorUpdatePending = nullptr;
turbine::setWindowTitle( turbine::setWindowTitle(
m_tctx, ox::sfmt("{} - {}", keelCtx(m_tctx).appName, m_project->projectPath())); m_tctx, ox::sfmt("{} - {}", keelCtx(m_tctx).appName, m_project->projectPath()));
m_deleteConfirmation.deleteFile.connect(m_sctx.project, &Project::deleteItem); m_deleteConfirmation.deleteFile.connect(m_sctx.project, &Project::deleteItem);
@@ -722,10 +701,10 @@ ox::Error StudioUI::openFileActiveTab(ox::StringViewCR path, bool const makeActi
} }
OX_REQUIRE(ext, fileExt(path)); OX_REQUIRE(ext, fileExt(path));
// create Editor // create Editor
ox::UPtr<BaseEditor> editor; BaseEditor *editor = nullptr;
auto const err = m_editorMakers.contains(ext) ? auto const err = m_editorMakers.contains(ext) ?
m_editorMakers[ext](path).to<ox::UPtr<BaseEditor>>().moveTo(editor) : m_editorMakers[ext](path).moveTo(editor) :
ox::make_unique_catch<ClawEditor>(m_sctx, path).moveTo(editor); ox::makeCatch<ClawEditor>(m_sctx, path).moveTo(editor);
if (err) { if (err) {
if constexpr(!ox::defines::Debug) { if constexpr(!ox::defines::Debug) {
oxErrf("Could not open Editor: {}\n", toStr(err)); oxErrf("Could not open Editor: {}\n", toStr(err));
@@ -735,11 +714,11 @@ ox::Error StudioUI::openFileActiveTab(ox::StringViewCR path, bool const makeActi
return err; return err;
} }
editor->closed.connect(this, &StudioUI::closeFile); editor->closed.connect(this, &StudioUI::closeFile);
auto const &e = m_editors.emplace_back(std::move(editor)); m_editors.emplace_back(editor);
m_openFiles.emplace_back(path); m_openFiles.emplace_back(path);
if (makeActiveTab) { if (makeActiveTab) {
m_activeEditor = m_editors.back().value->get(); m_activeEditor = m_editors.back().value->get();
m_activeEditorUpdatePending = e.get(); m_activeEditorUpdatePending = editor;
} }
// save to config // save to config
studio::editConfig<StudioConfig>(keelCtx(m_tctx), [&path](StudioConfig &config) { studio::editConfig<StudioConfig>(keelCtx(m_tctx), [&path](StudioConfig &config) {

View File

@@ -25,12 +25,12 @@
namespace studio { namespace studio {
class StudioUI final: public ox::SignalHandler { class StudioUI: public ox::SignalHandler {
friend class StudioUIDrawer; friend class StudioUIDrawer;
private: private:
Context m_sctx; Context m_sctx;
turbine::Context &m_tctx{m_sctx.tctx}; turbine::Context &m_tctx;
ox::String m_projectDataDir; ox::String m_projectDataDir;
ox::UPtr<Project> m_project; ox::UPtr<Project> m_project;
TaskRunner m_taskRunner; TaskRunner m_taskRunner;
@@ -83,12 +83,10 @@ class StudioUI final: public ox::SignalHandler {
ox::Optional<NavAction> m_navAction; ox::Optional<NavAction> m_navAction;
public: public:
explicit StudioUI(turbine::Context &tctx, ox::StringParam projectDataDir) noexcept; explicit StudioUI(turbine::Context &ctx, ox::StringParam projectDataDir) noexcept;
void handleKeyEvent(turbine::Key, bool down) noexcept; void handleKeyEvent(turbine::Key, bool down) noexcept;
void handleMouseButtonEvent(int btn, bool down) noexcept;
void handleNavigationChange(ox::StringParam path, ox::StringParam navArgs) noexcept; void handleNavigationChange(ox::StringParam path, ox::StringParam navArgs) noexcept;
[[nodiscard]] [[nodiscard]]

View File

@@ -15,14 +15,10 @@ namespace studio {
class StudioUI; class StudioUI;
struct Context { struct Context {
public:
friend void navigateTo(Context &ctx, ox::StringParam filePath, ox::StringParam navArgs) noexcept; friend void navigateTo(Context &ctx, ox::StringParam filePath, ox::StringParam navArgs) noexcept;
friend void navigateBack(Context &ctx) noexcept; friend void navigateBack(Context &ctx) noexcept;
friend void navigateForward(Context &ctx) noexcept; friend void navigateForward(Context &ctx) noexcept;
friend StudioUI; friend StudioUI;
StudioUI &ui;
Project *project = nullptr;
turbine::Context &tctx;
protected: protected:
struct NavPath { struct NavPath {
ox::String filePath; ox::String filePath;
@@ -31,8 +27,12 @@ struct Context {
size_t navIdx{}; size_t navIdx{};
ox::Vector<NavPath> navStack; ox::Vector<NavPath> navStack;
std::function<void(ox::StringParam filePath, ox::StringParam navArgs)> navCallback; std::function<void(ox::StringParam filePath, ox::StringParam navArgs)> navCallback;
public:
StudioUI &ui;
Project *project = nullptr;
turbine::Context &tctx;
Context(StudioUI &pUi, turbine::Context &pTctx) noexcept: Context(StudioUI &pUi, turbine::Context &pTctx) noexcept:
ui{pUi}, tctx{pTctx} {} ui(pUi), tctx(pTctx) {}
}; };
[[nodiscard]] [[nodiscard]]
@@ -40,11 +40,6 @@ inline keel::Context &keelCtx(Context &ctx) noexcept {
return keelCtx(ctx.tctx); return keelCtx(ctx.tctx);
} }
[[nodiscard]]
inline keel::Context const &keelCtx(Context const &ctx) noexcept {
return keelCtx(ctx.tctx);
}
void navigateTo(Context &ctx, ox::StringParam filePath, ox::StringParam navArgs = "") noexcept; void navigateTo(Context &ctx, ox::StringParam filePath, ox::StringParam navArgs = "") noexcept;
void navigateBack(Context &ctx) noexcept; void navigateBack(Context &ctx) noexcept;

View File

@@ -13,11 +13,7 @@ class FilePickerPopup {
private: private:
ox::String m_name; ox::String m_name;
struct Explorer: public FileExplorer { FileExplorer m_explorer;
mutable bool opened{};
explicit Explorer(keel::Context &kctx);
void fileOpened(ox::StringViewCR path) const noexcept override;
} m_explorer;
ox::Vector<ox::String> const m_fileExts; ox::Vector<ox::String> const m_fileExts;
bool m_open{}; bool m_open{};
@@ -37,10 +33,6 @@ class FilePickerPopup {
ox::Optional<ox::String> draw(Context &ctx) noexcept; ox::Optional<ox::String> draw(Context &ctx) noexcept;
private:
[[nodiscard]]
ox::Optional<ox::String> handlePick() noexcept;
}; };
} }

View File

@@ -109,7 +109,7 @@ class ItemMaker {
return m_typeDisplayName; return m_typeDisplayName;
} }
bool installTemplate(ox::UPtr<ItemTemplate> &&tmpl) { bool installTemplate(ox::UPtr<ItemTemplate> &tmpl) {
if (typeName() == tmpl->typeName() && if (typeName() == tmpl->typeName() &&
typeVersion() <= tmpl->typeVersion()) { typeVersion() <= tmpl->typeVersion()) {
m_templates.emplace_back(std::move(tmpl)); m_templates.emplace_back(std::move(tmpl));
@@ -120,6 +120,10 @@ class ItemMaker {
return false; return false;
} }
bool installTemplate(ox::UPtr<ItemTemplate> &&tmpl) {
return installTemplate(tmpl);
}
constexpr ox::Vector<ox::UPtr<ItemTemplate>> const&itemTemplates() const noexcept { constexpr ox::Vector<ox::UPtr<ItemTemplate>> const&itemTemplates() const noexcept {
return m_templates; return m_templates;
} }

View File

@@ -9,7 +9,7 @@ namespace studio {
void navigateTo(Context &ctx, ox::StringParam filePath, ox::StringParam navArgs) noexcept { void navigateTo(Context &ctx, ox::StringParam filePath, ox::StringParam navArgs) noexcept {
ox::String path = std::move(filePath); ox::String path = std::move(filePath);
if (beginsWith(path, "uuid://")) { if (beginsWith(path, "uuid://")) {
auto const [p, err] = keel::uuidUrlToPath(keelCtx(ctx), path); auto [p, err] = keel::uuidUrlToPath(keelCtx(ctx), path);
if (err) { if (err) {
return; return;
} }
@@ -17,12 +17,7 @@ void navigateTo(Context &ctx, ox::StringParam filePath, ox::StringParam navArgs)
} }
ctx.navStack.resize(ctx.navIdx + 1); ctx.navStack.resize(ctx.navIdx + 1);
ctx.navStack.emplace_back(ox::String{path}, ox::String{navArgs.view()}); ctx.navStack.emplace_back(ox::String{path}, ox::String{navArgs.view()});
try {
ctx.navCallback(std::move(path), std::move(navArgs)); ctx.navCallback(std::move(path), std::move(navArgs));
} catch (std::exception const &e) {
oxErrf("Nav error: {}", e.what());
oxAssert(false, "Nav error");
}
} }
void navigateBack(Context &ctx) noexcept { void navigateBack(Context &ctx) noexcept {
@@ -30,26 +25,19 @@ void navigateBack(Context &ctx) noexcept {
return; return;
} }
--ctx.navIdx; --ctx.navIdx;
while (ctx.navIdx < ctx.navStack.size() && ctx.navIdx) { if (ctx.navIdx < ctx.navStack.size() && ctx.navIdx) {
auto const i = ctx.navIdx - 1; auto const &n = ctx.navStack[ctx.navIdx - 1];
auto const &n = ctx.navStack[i];
if (!ctx.project->exists(n.filePath)) {
std::ignore = ctx.navStack.erase(i);
--ctx.navIdx;
continue;
}
try { try {
ctx.navCallback(n.filePath, n.navArgs); ctx.navCallback(n.filePath, n.navArgs);
} catch (std::exception const &e) { } catch (std::exception const &e) {
oxAssert(ctx.navCallback != nullptr, "navCallback is null"); oxAssert(ctx.navCallback != nullptr, "navCallback is null");
oxErrf("navigateForward failed: {}", e.what()); oxErrf("navigateForward failed: {}", e.what());
} }
break;
} }
} }
void navigateForward(Context &ctx) noexcept { void navigateForward(Context &ctx) noexcept {
while (ctx.navIdx < ctx.navStack.size()) { if (ctx.navIdx < ctx.navStack.size()) {
auto const &n = ctx.navStack[ctx.navIdx]; auto const &n = ctx.navStack[ctx.navIdx];
try { try {
ctx.navCallback(n.filePath, n.navArgs); ctx.navCallback(n.filePath, n.navArgs);
@@ -57,12 +45,7 @@ void navigateForward(Context &ctx) noexcept {
oxAssert(ctx.navCallback != nullptr, "navCallback is null"); oxAssert(ctx.navCallback != nullptr, "navCallback is null");
oxErrf("navigateForward failed: {}", e.what()); oxErrf("navigateForward failed: {}", e.what());
} }
if (!ctx.project->exists(n.filePath)) {
std::ignore = ctx.navStack.erase(ctx.navIdx);
continue;
}
++ctx.navIdx; ++ctx.navIdx;
break;
} }
} }

View File

@@ -72,10 +72,8 @@ bool BaseEditor::exportable() const noexcept {
} }
void BaseEditor::setCutEnabled(bool v) { void BaseEditor::setCutEnabled(bool v) {
if (m_cutEnabled != v) {
m_cutEnabled = v; m_cutEnabled = v;
cutEnabledChanged.emit(v); cutEnabledChanged.emit(v);
}
} }
bool BaseEditor::cutEnabled() const noexcept { bool BaseEditor::cutEnabled() const noexcept {
@@ -83,10 +81,8 @@ bool BaseEditor::cutEnabled() const noexcept {
} }
void BaseEditor::setCopyEnabled(bool v) { void BaseEditor::setCopyEnabled(bool v) {
if (m_copyEnabled != v) {
m_copyEnabled = v; m_copyEnabled = v;
copyEnabledChanged.emit(v); copyEnabledChanged.emit(v);
}
} }
bool BaseEditor::copyEnabled() const noexcept { bool BaseEditor::copyEnabled() const noexcept {
@@ -94,10 +90,8 @@ bool BaseEditor::copyEnabled() const noexcept {
} }
void BaseEditor::setPasteEnabled(bool v) { void BaseEditor::setPasteEnabled(bool v) {
if (m_pasteEnabled != v) {
m_pasteEnabled = v; m_pasteEnabled = v;
pasteEnabledChanged.emit(v); pasteEnabledChanged.emit(v);
}
} }
bool BaseEditor::pasteEnabled() const noexcept { bool BaseEditor::pasteEnabled() const noexcept {

View File

@@ -8,15 +8,6 @@
namespace studio { namespace studio {
FilePickerPopup::Explorer::Explorer(keel::Context &kctx):
FileExplorer{kctx} {
}
void FilePickerPopup::Explorer::fileOpened(ox::StringViewCR) const noexcept {
opened = true;
}
FilePickerPopup::FilePickerPopup( FilePickerPopup::FilePickerPopup(
ox::StringParam name, ox::StringParam name,
keel::Context &kctx, keel::Context &kctx,
@@ -50,7 +41,6 @@ void FilePickerPopup::refresh() noexcept {
void FilePickerPopup::open() noexcept { void FilePickerPopup::open() noexcept {
refresh(); refresh();
m_open = true; m_open = true;
m_explorer.opened = false;
} }
void FilePickerPopup::close() noexcept { void FilePickerPopup::close() noexcept {
@@ -70,22 +60,16 @@ ox::Optional<ox::String> FilePickerPopup::draw(Context &ctx) noexcept {
if (ig::BeginPopup(ctx.tctx, m_name, m_open, {380, 340})) { if (ig::BeginPopup(ctx.tctx, m_name, m_open, {380, 340})) {
auto const vp = ImGui::GetContentRegionAvail(); auto const vp = ImGui::GetContentRegionAvail();
m_explorer.draw(ctx, {vp.x, vp.y - 30}); m_explorer.draw(ctx, {vp.x, vp.y - 30});
if (ig::PopupControlsOkCancel(m_open) == ig::PopupResponse::OK || m_explorer.opened) { if (ig::PopupControlsOkCancel(m_open) == ig::PopupResponse::OK) {
out = handlePick(); auto p = m_explorer.selectedPath();
if (p) {
out.emplace(*p);
}
close();
} }
ImGui::EndPopup(); ImGui::EndPopup();
} }
return out; return out;
} }
ox::Optional<ox::String> FilePickerPopup::handlePick() noexcept {
ox::Optional<ox::String> out;
auto p = m_explorer.selectedPath();
if (p) {
out.emplace(*p);
}
close();
return out;
}
} }

View File

@@ -101,8 +101,8 @@ ox::Result<ox::FileStat> Project::stat(ox::StringViewCR path) const noexcept {
ox::Error Project::copyItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept { ox::Error Project::copyItem(ox::StringViewCR src, ox::StringViewCR dest) noexcept {
OX_REQUIRE_M(buff, loadBuff(src)); OX_REQUIRE_M(buff, loadBuff(src));
OX_REQUIRE(id, keel::regenerateUuidHeader(buff)); OX_REQUIRE(id, keel::regenerateUuidHeader(buff));
OX_RETURN_ERROR(writeBuff(dest, buff));
createUuidMapping(m_kctx, dest, id); createUuidMapping(m_kctx, dest, id);
OX_RETURN_ERROR(writeBuff(dest, ox::BufferView{buff} + keel::K1HdrSz));
return {}; return {};
} }

View File

@@ -79,14 +79,11 @@ enum Key {
}; };
using KeyEventHandler = void(*)(Context&, Key, bool); using KeyEventHandler = void(*)(Context&, Key, bool);
using MouseButtonEventHandler = void(*)(Context&, int btn, bool);
void setKeyEventHandler(Context &ctx, KeyEventHandler h) noexcept; void setKeyEventHandler(Context &ctx, KeyEventHandler h) noexcept;
void setMouseButtonEventHandler(Context &ctx, MouseButtonEventHandler h) noexcept;
[[nodiscard]] [[nodiscard]]
KeyEventHandler keyEventHandler(Context const &ctx) noexcept; KeyEventHandler keyEventHandler(Context &ctx) noexcept;
[[nodiscard]] [[nodiscard]]
bool buttonDown(Context const&ctx, Key) noexcept; bool buttonDown(Context const&ctx, Key) noexcept;

View File

@@ -133,9 +133,7 @@ void setKeyEventHandler(Context &ctx, KeyEventHandler h) noexcept {
ctx.keyEventHandler = h; ctx.keyEventHandler = h;
} }
void setMouseButtonEventHandler(Context&, MouseButtonEventHandler) noexcept {} KeyEventHandler keyEventHandler(Context &ctx) noexcept {
KeyEventHandler keyEventHandler(Context const &ctx) noexcept {
return ctx.keyEventHandler; return ctx.keyEventHandler;
} }

View File

@@ -16,7 +16,6 @@ class Context {
UpdateHandler updateHandler = [](Context&) -> int {return -1;}; UpdateHandler updateHandler = [](Context&) -> int {return -1;};
keel::Context keelCtx; keel::Context keelCtx;
KeyEventHandler keyEventHandler = nullptr; KeyEventHandler keyEventHandler = nullptr;
MouseButtonEventHandler mouseButtonEventHandler = nullptr;
ox::AnyPtr applicationData; ox::AnyPtr applicationData;
// GLFW impl data //////////////////////////////////////////////////////// // GLFW impl data ////////////////////////////////////////////////////////

View File

@@ -299,14 +299,9 @@ static void handleKeyPress(Context &ctx, int const key, bool const down) noexcep
static void handleGlfwCursorPosEvent(GLFWwindow*, double, double) noexcept { static void handleGlfwCursorPosEvent(GLFWwindow*, double, double) noexcept {
} }
static void handleGlfwMouseButtonEvent( static void handleGlfwMouseButtonEvent(GLFWwindow *window, int, int, int) noexcept {
GLFWwindow *window,
int const btn,
int const action,
int) noexcept {
auto &ctx = *static_cast<Context*>(glfwGetWindowUserPointer(window)); auto &ctx = *static_cast<Context*>(glfwGetWindowUserPointer(window));
setMandatoryRefreshPeriod(ctx, ticksMs(ctx) + config::MandatoryRefreshPeriod); setMandatoryRefreshPeriod(ctx, ticksMs(ctx) + config::MandatoryRefreshPeriod);
ctx.mouseButtonEventHandler(ctx, btn, action == 1);
} }
static void handleGlfwKeyEvent(GLFWwindow *window, int const key, int, int const action, int) noexcept { static void handleGlfwKeyEvent(GLFWwindow *window, int const key, int, int const action, int) noexcept {
@@ -344,8 +339,7 @@ ox::Result<ox::UPtr<Context>> init(
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE); glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE); glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
constexpr auto Scale = 5; constexpr auto Scale = 5;
ctx->window = glfwCreateWindow( ctx->window = glfwCreateWindow(240 * Scale, 160 * Scale, ctx->keelCtx.appName.c_str(), nullptr, nullptr);
240 * Scale, 160 * Scale, ctx->keelCtx.appName.c_str(), nullptr, nullptr);
//ctx.window = glfwCreateWindow(876, 743, ctx.keelCtx.appName.c_str(), nullptr, nullptr); //ctx.window = glfwCreateWindow(876, 743, ctx.keelCtx.appName.c_str(), nullptr, nullptr);
if (ctx->window == nullptr) { if (ctx->window == nullptr) {
return ox::Error(1, "Could not open GLFW window"); return ox::Error(1, "Could not open GLFW window");
@@ -453,19 +447,15 @@ void setShutdownHandler(Context &ctx, ShutdownHandler const handler) noexcept {
ctx.shutdownHandler = handler; ctx.shutdownHandler = handler;
} }
void setUpdateHandler(Context &ctx, UpdateHandler const h) noexcept { void setUpdateHandler(Context &ctx, UpdateHandler h) noexcept {
ctx.updateHandler = h; ctx.updateHandler = h;
} }
void setKeyEventHandler(Context &ctx, KeyEventHandler const h) noexcept { void setKeyEventHandler(Context &ctx, KeyEventHandler h) noexcept {
ctx.keyEventHandler = h; ctx.keyEventHandler = h;
} }
void setMouseButtonEventHandler(Context &ctx, MouseButtonEventHandler const h) noexcept { KeyEventHandler keyEventHandler(Context &ctx) noexcept {
ctx.mouseButtonEventHandler = h;
}
KeyEventHandler keyEventHandler(Context const &ctx) noexcept {
return ctx.keyEventHandler; return ctx.keyEventHandler;
} }

View File

@@ -1,59 +0,0 @@
#! /usr/bin/env python3
from pathlib import Path
import argparse
def parse_args():
parser = argparse.ArgumentParser(description="Count and sort lines of code in selected files.")
parser.add_argument(
"--search-dirs", nargs="+", required=True,
help="List of directories to search (recursively)."
)
parser.add_argument(
"--include-exts", nargs="+", required=True,
help="List of file extensions to include (e.g. .cpp .hpp .py)"
)
parser.add_argument(
"--exclude-paths", nargs="*", default=[],
help="Full file paths to exclude from counting."
)
return parser.parse_args()
def main():
args = parse_args()
include_exts = tuple(args.include_exts)
exclude_paths = {Path(p).resolve() for p in args.exclude_paths}
files = []
for base in args.search_dirs:
path = Path(base)
if path.is_dir():
for file in path.rglob("*"):
try:
resolved_file = file.resolve()
if (
file.is_file()
and file.name.endswith(include_exts)
and resolved_file not in exclude_paths
):
files.append(str(resolved_file))
except FileNotFoundError:
continue
line_counts = []
total_lines = 0
for f in files:
try:
with open(f, "r", encoding="utf-8", errors="ignore") as file:
lines = sum(1 for _ in file)
line_counts.append((lines, f))
total_lines += lines
except Exception as e:
print(f"Failed to read {f}: {e}")
line_counts.sort()
for count, filename in line_counts:
print(f"{count:>7} {filename}")
print(f"{total_lines:>7} total")
if __name__ == "__main__":
main()