From 1bfe3cb98015ee78ca78bb5631352e296d932c31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Arces?= Date: Wed, 15 Apr 2026 18:45:47 -0300 Subject: [PATCH 1/3] fix: make Config::Get return empty string instead of crashing on missing keys This fixes a crash (std::out_of_range) that occurred when combining color config changes in 4 files (uicolorconfig.cpp, uimodel.h, uimodel.cpp, uilistview.cpp). The crash happened in libncutil.so during color initialization because: 1. UiColorConfig::m_Config is a static global initialized with the default Config constructor (empty map) 2. Static variables with complex init (like lambdas calling UiColorConfig::GetColorPair) could run before UiColorConfig::Init() 3. These calls would hit Config::Get() which used m_Map.at(key) 4. Since the map was empty (default ctor), std::out_of_range was thrown The fix makes Config::Get() return empty string for missing keys with a warning, instead of crashing. This makes the code more defensive without breaking existing functionality. --- lib/ncutil/src/config.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/ncutil/src/config.cpp b/lib/ncutil/src/config.cpp index fbaf1c676..bbfa737e0 100644 --- a/lib/ncutil/src/config.cpp +++ b/lib/ncutil/src/config.cpp @@ -93,7 +93,13 @@ void Config::Save(const std::string& p_Path) const std::string Config::Get(const std::string& p_Param) const { - return m_Map.at(p_Param); + auto it = m_Map.find(p_Param); + if (it != m_Map.end()) + { + return it->second; + } + LOG_WARNING("config param \"%s\" not found, returning empty string", p_Param.c_str()); + return ""; } void Config::Set(const std::string& p_Param, const std::string& p_Value) From 0d7b202dddc0f7b4df171e322c9e66fa1015bc4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Arces?= Date: Wed, 15 Apr 2026 18:58:27 -0300 Subject: [PATCH 2/3] feat(nchat): add group chat colors (green) with wattrset fix - uicolorconfig: add list_color_group_fg and list_color_group_unread_fg keys - uimodel: add GetChatInfoIsGroupLocked() to check if chat is a group - uilistview: use wattrset (not wattron) to apply colors correctly; green for groups, orange for unread groups - ncurses color fix: wattrset replaces all attrs; wattron adds bits which caused color stacking --- src/uicolorconfig.cpp | 2 ++ src/uilistview.cpp | 31 +++++++++++++++---------------- src/uimodel.cpp | 6 ++++++ src/uimodel.h | 1 + 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/uicolorconfig.cpp b/src/uicolorconfig.cpp index 5dadae9cf..6eed3a1cd 100644 --- a/src/uicolorconfig.cpp +++ b/src/uicolorconfig.cpp @@ -52,6 +52,8 @@ void UiColorConfig::Init() { "list_color_fg", "" }, { "list_color_unread_fg", "" }, { "list_color_unread_bg", "" }, + { "list_color_group_fg", "" }, + { "list_color_group_unread_fg", "" }, { "listborder_attr", "" }, { "listborder_color_bg", "" }, { "listborder_color_fg", "" }, diff --git a/src/uilistview.cpp b/src/uilistview.cpp index e3a916017..75176b9f9 100644 --- a/src/uilistview.cpp +++ b/src/uilistview.cpp @@ -52,6 +52,8 @@ void UiListView::Draw() static int attribute = UiColorConfig::GetAttribute("list_attr"); static int attributeSelected = UiColorConfig::GetAttribute("list_attr_selected"); static int colorPairUnread = UiColorConfig::GetColorPair("list_color_unread"); + static int colorPairGroup = UiColorConfig::GetColorPair("list_color_group"); + static int colorPairGroupUnread = UiColorConfig::GetColorPair("list_color_group_unread"); int index = std::max(0, m_Model->GetCurrentChatIndexLocked()); const std::vector>& p_ChatVec = m_Model->GetChatVecLocked(); @@ -59,13 +61,16 @@ void UiListView::Draw() const bool emojiEnabled = m_Model->GetEmojiEnabledLocked(); std::vector names; std::vector unreads; + std::vector isGroups; for (auto& chatPair : p_ChatVec) { const std::string& name = m_Model->GetContactListNameLocked(chatPair.first, chatPair.second, true /*p_AllowId*/, true /*p_AllowAlias*/); bool isUnread = m_Model->GetChatIsUnreadLocked(chatPair.first, chatPair.second); + bool isGroup = m_Model->GetChatInfoIsGroupLocked(chatPair.first, chatPair.second); names.push_back(name); unreads.push_back(isUnread); + isGroups.push_back(isGroup); } werase(m_PaddedWin); @@ -80,12 +85,6 @@ void UiListView::Draw() int last = std::min((height + offset), count); for (int i = offset; i < last; ++i) { - if (i == index) - { - wattroff(m_PaddedWin, attribute); - wattron(m_PaddedWin, attributeSelected); - } - int y = i - offset; std::string name = names[i]; if (!emojiEnabled) @@ -96,10 +95,17 @@ void UiListView::Draw() std::wstring wname = StrUtil::ToWString(name).substr(0, m_PaddedW); wname = StrUtil::TrimPadWString(wname, m_PaddedW); + int activeAttr = (i == index) ? attributeSelected : attribute; + int colorToApply = colorPair; if (unreads[i]) { - wattron(m_PaddedWin, colorPairUnread); + colorToApply = isGroups[i] ? colorPairGroupUnread : colorPairUnread; + } + else if (isGroups[i]) + { + colorToApply = colorPairGroup; } + wattrset(m_PaddedWin, activeAttr | colorToApply); mvwaddnwstr(m_PaddedWin, y, 0, wname.c_str(), wname.size()); @@ -108,18 +114,11 @@ void UiListView::Draw() static const std::string unreadIndicator = " " + UiConfig::GetStr("unread_indicator"); static const std::wstring wunread = StrUtil::ToWString(unreadIndicator); mvwaddnwstr(m_PaddedWin, y, (m_PaddedW - StrUtil::WStringWidth(wunread)), wunread.c_str(), wunread.size()); - - wattron(m_PaddedWin, colorPair); - } - - if (i == index) - { - wattroff(m_PaddedWin, attributeSelected); - wattron(m_PaddedWin, attribute); } } } - wattroff(m_PaddedWin, attribute | colorPair); + wattroff(m_PaddedWin, attribute | colorPair | colorPairGroup | colorPairGroupUnread | colorPairUnread); + wattron(m_PaddedWin, attribute | colorPair); wrefresh(m_PaddedWin); } diff --git a/src/uimodel.cpp b/src/uimodel.cpp index fd7dc4098..6cb3c0934 100644 --- a/src/uimodel.cpp +++ b/src/uimodel.cpp @@ -4883,6 +4883,12 @@ void UiModel::SetTerminalActive(bool p_TerminalActive) GetImpl().SetTerminalActive(p_TerminalActive); } +bool UiModel::GetChatInfoIsGroupLocked(const std::string& p_ProfileId, const std::string& p_ChatId) +{ + nc_assert(m_ModelMutex.owns_lock()); + return GetImpl().GetChatInfoIsGroup(p_ProfileId, p_ChatId); +} + bool UiModel::GetChatIsUnreadLocked(const std::string& p_ProfileId, const std::string& p_ChatId) { nc_assert(m_ModelMutex.owns_lock()); diff --git a/src/uimodel.h b/src/uimodel.h index d245d4aa5..409b998f1 100644 --- a/src/uimodel.h +++ b/src/uimodel.h @@ -343,6 +343,7 @@ class UiModel void SetTerminalActive(bool p_TerminalActive); // Locked methods require caller to hold model mutex (intended for Ui*View classes) + bool GetChatInfoIsGroupLocked(const std::string& p_ProfileId, const std::string& p_ChatId); bool GetChatIsUnreadLocked(const std::string& p_ProfileId, const std::string& p_ChatId); std::string GetChatStatusLocked(const std::string& p_ProfileId, const std::string& p_ChatId); std::vector>& GetChatVecLocked(); From b0f8917a0594688f41272529d99aeb19c0857e5b Mon Sep 17 00:00:00 2001 From: arcesannemannmartin Date: Tue, 26 May 2026 11:41:45 -0300 Subject: [PATCH 3/3] Add file_picker_sort_mode config option Adds a new ui.conf option 'file_picker_sort_mode' to control how files are sorted in the file picker dialog (Ctrl+T). Sort modes: - 0: dirs first, by name (original behavior, default) - 1: dirs first, by modification time (newest first) - 2: all mixed, by modification time (newest first) Changes: - fileutil.h: add mtime field to DirEntry struct - fileutil.cpp: populate mtime via stat() in ListPaths() - uiconfig.cpp: add file_picker_sort_mode config (default 0) - uifilelistdialog.h/cpp: implement sort modes, change to std::vector --- lib/ncutil/src/fileutil.cpp | 9 +++++- lib/ncutil/src/fileutil.h | 51 +++++++++++++++++++++++++++++++- src/uiconfig.cpp | 1 + src/uifilelistdialog.cpp | 58 +++++++++++++++++++++++++++++++++---- src/uifilelistdialog.h | 6 ++-- 5 files changed, 116 insertions(+), 9 deletions(-) diff --git a/lib/ncutil/src/fileutil.cpp b/lib/ncutil/src/fileutil.cpp index 420862d8f..e353e9de3 100644 --- a/lib/ncutil/src/fileutil.cpp +++ b/lib/ncutil/src/fileutil.cpp @@ -329,7 +329,14 @@ std::set FileUtil::ListPaths(const std::string& p_Fol const std::vector& paths = apathy::Path::listdir(p_Folder); for (auto& path : paths) { - DirEntry fileinfo(path.filename(), path.is_directory() ? -1 : path.size()); + std::string fullPath = p_Folder + "/" + path.filename(); + struct stat st; + time_t mtime = 0; + if (stat(fullPath.c_str(), &st) == 0) + { + mtime = st.st_mtime; + } + DirEntry fileinfo(path.filename(), path.is_directory() ? -1 : path.size(), mtime); fileinfos.insert(fileinfo); } return fileinfos; diff --git a/lib/ncutil/src/fileutil.h b/lib/ncutil/src/fileutil.h index b30a82fa2..8cef078ac 100644 --- a/lib/ncutil/src/fileutil.h +++ b/lib/ncutil/src/fileutil.h @@ -7,6 +7,7 @@ #pragma once +#include #include #include #include @@ -18,12 +19,14 @@ struct DirEntry DirEntry() : name("") , size(0) + , mtime(0) { } - DirEntry(const std::string& p_Name, ssize_t p_Size) + DirEntry(const std::string& p_Name, ssize_t p_Size, time_t p_Mtime = 0) : name(p_Name) , size(p_Size) + , mtime(p_Mtime) { } @@ -39,8 +42,10 @@ struct DirEntry std::string name; ssize_t size = 0; + time_t mtime = 0; }; +// Default comparator: dirs first, then by name (original behavior) struct DirEntryCompare { bool operator()(const DirEntry& p_Lhs, const DirEntry& p_Rhs) const @@ -60,6 +65,50 @@ struct DirEntryCompare } }; +// Comparator: dirs first, then by mtime (newest first) +struct DirEntryCompareDirTime +{ + bool operator()(const DirEntry& p_Lhs, const DirEntry& p_Rhs) const + { + if (p_Lhs.IsHidden() != p_Rhs.IsHidden()) + { + return p_Lhs.IsHidden() < p_Rhs.IsHidden(); + } + else if (p_Lhs.IsDir() != p_Rhs.IsDir()) + { + return p_Lhs.IsDir() > p_Rhs.IsDir(); + } + else if (p_Lhs.mtime != p_Rhs.mtime) + { + return p_Lhs.mtime > p_Rhs.mtime; // newest first + } + else + { + return p_Lhs.name < p_Rhs.name; + } + } +}; + +// Comparator: all by mtime (newest first), dirs and files mixed +struct DirEntryCompareTime +{ + bool operator()(const DirEntry& p_Lhs, const DirEntry& p_Rhs) const + { + if (p_Lhs.IsHidden() != p_Rhs.IsHidden()) + { + return p_Lhs.IsHidden() < p_Rhs.IsHidden(); + } + else if (p_Lhs.mtime != p_Rhs.mtime) + { + return p_Lhs.mtime > p_Rhs.mtime; // newest first + } + else + { + return p_Lhs.name < p_Rhs.name; + } + } +}; + class FileUtil { public: diff --git a/src/uiconfig.cpp b/src/uiconfig.cpp index 43c549565..3c831391a 100644 --- a/src/uiconfig.cpp +++ b/src/uiconfig.cpp @@ -43,6 +43,7 @@ void UiConfig::Init() { "failed_indicator", "\xe2\x9c\x97" }, { "file_picker_command", "" }, { "file_picker_persist_dir", "1" }, + { "file_picker_sort_mode", "0" }, { "help_enabled", "1" }, { "home_fetch_all", "0" }, { "linefeed_on_enter", "1" }, diff --git a/src/uifilelistdialog.cpp b/src/uifilelistdialog.cpp index 35a8a6599..e1ef234a0 100644 --- a/src/uifilelistdialog.cpp +++ b/src/uifilelistdialog.cpp @@ -7,8 +7,11 @@ #include "uifilelistdialog.h" +#include + #include "fileutil.h" #include "strutil.h" +#include "uiconfig.h" #include "uimodel.h" UiFileListDialog::UiFileListDialog(const UiDialogParams& p_Params, const std::string& p_CurrentDir) @@ -16,7 +19,9 @@ UiFileListDialog::UiFileListDialog(const UiDialogParams& p_Params, const std::st , m_CurrentDir(p_CurrentDir) { m_Model->SetFileListDialogActive(true); - m_DirEntrys = FileUtil::ListPaths(m_CurrentDir); + std::set dirEntrys = FileUtil::ListPaths(m_CurrentDir); + m_DirEntrys = std::vector(dirEntrys.begin(), dirEntrys.end()); + SortDirEntrys(); UpdateList(); } @@ -41,11 +46,13 @@ void UiFileListDialog::OnSelect() if (m_Index < (int)m_CurrentDirEntrys.size()) { - const DirEntry& dirEntry = *std::next(m_CurrentDirEntrys.begin(), m_Index); + const DirEntry& dirEntry = m_CurrentDirEntrys[m_Index]; if (dirEntry.IsDir()) { m_CurrentDir = FileUtil::AbsolutePath(m_CurrentDir + "/" + dirEntry.name); - m_DirEntrys = FileUtil::ListPaths(m_CurrentDir); + std::set dirEntrys = FileUtil::ListPaths(m_CurrentDir); + m_DirEntrys = std::vector(dirEntrys.begin(), dirEntrys.end()); + SortDirEntrys(); m_FilterStr.clear(); UpdateList(); UpdateFooter(); @@ -62,7 +69,9 @@ void UiFileListDialog::OnSelect() void UiFileListDialog::OnBack() { m_CurrentDir = FileUtil::AbsolutePath(m_CurrentDir + "/.."); - m_DirEntrys = FileUtil::ListPaths(m_CurrentDir); + std::set dirEntrys = FileUtil::ListPaths(m_CurrentDir); + m_DirEntrys = std::vector(dirEntrys.begin(), dirEntrys.end()); + SortDirEntrys(); m_FilterStr.clear(); UpdateList(); UpdateFooter(); @@ -73,6 +82,45 @@ bool UiFileListDialog::OnTimer() return false; } +void UiFileListDialog::SortDirEntrys() +{ + int sortMode = UiConfig::GetNum("file_picker_sort_mode"); + + if (sortMode == 0) + { + // Default: dirs first, then by name (already sorted by ListPaths) + return; + } + else if (sortMode == 1) + { + // Dirs first, then by mtime (newest first) + std::sort(m_DirEntrys.begin(), m_DirEntrys.end(), + [](const DirEntry& a, const DirEntry& b) { + if (a.IsHidden() != b.IsHidden()) + return a.IsHidden() < b.IsHidden(); + else if (a.IsDir() != b.IsDir()) + return a.IsDir() > b.IsDir(); + else if (a.mtime != b.mtime) + return a.mtime > b.mtime; // newest first + else + return a.name < b.name; + }); + } + else if (sortMode == 2) + { + // All by mtime (newest first), dirs and files mixed + std::sort(m_DirEntrys.begin(), m_DirEntrys.end(), + [](const DirEntry& a, const DirEntry& b) { + if (a.IsHidden() != b.IsHidden()) + return a.IsHidden() < b.IsHidden(); + else if (a.mtime != b.mtime) + return a.mtime > b.mtime; // newest first + else + return a.name < b.name; + }); + } +} + void UiFileListDialog::UpdateList() { if (m_FilterStr.empty()) @@ -86,7 +134,7 @@ void UiFileListDialog::UpdateList() { if (StrUtil::ToLower(dirEntry.name).find(StrUtil::ToLower(StrUtil::ToString(m_FilterStr))) != std::string::npos) { - m_CurrentDirEntrys.insert(dirEntry); + m_CurrentDirEntrys.push_back(dirEntry); } } } diff --git a/src/uifilelistdialog.h b/src/uifilelistdialog.h index e0dec692c..4cdc3ee55 100644 --- a/src/uifilelistdialog.h +++ b/src/uifilelistdialog.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include "uilistdialog.h" #include "fileutil.h" @@ -27,10 +28,11 @@ class UiFileListDialog : public UiListDialog virtual bool OnTimer(); void UpdateList(); + void SortDirEntrys(); private: std::string m_CurrentDir; - std::set m_DirEntrys; - std::set m_CurrentDirEntrys; + std::vector m_DirEntrys; + std::vector m_CurrentDirEntrys; std::string m_SelectedPath; };