11using Clock = std::chrono::system_clock;
15 auto ext = p.extension().string();
16 std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
17 return (ext ==
".md" || ext ==
".markdown");
21 cfg_(std::move(cfg)), toaster_(std::move(toaster))
32 std::lock_guard<std::mutex> lk(
mtx_);
41 if (!dir.is_directory())
43 auto tableName = dir.path().filename().string();
44 auto notesDir = dir.path() /
"Notes";
53 std::lock_guard<std::mutex> lk(
mtx_);
60 std::lock_guard<std::mutex> lk(
mtx_);
63 toastInfo(
"Table notes loaded: " + tableName);
67 std::optional<std::string> tableName)
70 if (dir.empty() || !std::filesystem::exists(dir, ec))
73 for (
auto& entry : std::filesystem::directory_iterator(dir, ec))
75 if (!entry.is_regular_file(ec))
83 auto n = std::make_shared<Note>(std::move(temp));
84 n->file_path = entry.path();
85 n->saved_locally =
true;
89 n->table_name = tableName;
97 std::optional<std::string> tableName)
99 std::lock_guard<std::mutex> lk(
mtx_);
101 auto n = std::make_shared<Note>();
103 n->title = std::move(title);
104 n->author = std::move(author);
105 n->table_name = std::move(tableName);
106 n->creation_ts = Clock::now();
107 n->last_update_ts = n->creation_ts;
108 n->saved_locally =
false;
112 n->open_editor =
true;
122 std::lock_guard<std::mutex> lk(
mtx_);
126 auto& n = *it->second;
129 if (n.file_path.empty())
133 std::filesystem::create_directories(n.file_path.parent_path(), ec);
135 n.last_update_ts = Clock::now();
141 n.saved_locally =
true;
150 std::lock_guard<std::mutex> lk(
mtx_);
154 auto& n = *it->second;
157 std::filesystem::create_directories(absolutePath.parent_path(), ec);
159 n.last_update_ts = Clock::now();
162 toastError(
"Failed to save as: " + absolutePath.string());
165 n.file_path = absolutePath;
166 n.saved_locally =
true;
169 toastGood(
"Saved as: " + absolutePath.filename().string());
175 std::lock_guard<std::mutex> lk(
mtx_);
180 if (deleteFromDisk && !it->second->file_path.empty())
183 std::filesystem::remove(it->second->file_path, ec);
192 std::lock_guard<std::mutex> lk(
mtx_);
194 return (it ==
notesById_.end() ?
nullptr : it->second);
199 std::lock_guard<std::mutex> lk(
mtx_);
201 return (it ==
notesById_.end() ?
nullptr : it->second);
206 std::lock_guard<std::mutex> lk(
mtx_);
207 std::vector<std::shared_ptr<Note>> v;
210 v.push_back(kv.second);
216 std::lock_guard<std::mutex> lk(
mtx_);
217 std::vector<std::shared_ptr<Note>> v;
221 if (!kv.second->inbox)
222 v.push_back(kv.second);
229 std::lock_guard<std::mutex> lk(
mtx_);
230 std::vector<std::shared_ptr<Note>> v;
234 if (kv.second->inbox)
235 v.push_back(kv.second);
242 std::lock_guard<std::mutex> lk(
mtx_);
246 it->second->title = std::move(title);
247 it->second->dirty =
true;
248 it->second->last_update_ts = Clock::now();
253 std::lock_guard<std::mutex> lk(
mtx_);
257 it->second->markdown_text = std::move(md);
258 it->second->dirty =
true;
259 it->second->last_update_ts = Clock::now();
264 std::lock_guard<std::mutex> lk(
mtx_);
268 it->second->author = std::move(author);
269 it->second->dirty =
true;
270 it->second->last_update_ts = Clock::now();
276 std::string markdown,
277 std::optional<std::string> fromPeerName)
279 std::lock_guard<std::mutex> lk(
mtx_);
286 auto n = std::make_shared<Note>();
288 n->title = std::move(title);
289 n->author = std::move(author);
290 n->markdown_text = std::move(markdown);
291 n->creation_ts = Clock::now();
292 n->last_update_ts = n->creation_ts;
293 n->saved_locally =
false;
297 n->shared_from = std::move(fromPeerName);
298 n->open_editor =
false;
303 auto& n = *it->second;
304 n.title = std::move(title);
305 n.author = std::move(author);
306 n.markdown_text = std::move(markdown);
307 n.last_update_ts = Clock::now();
310 n.shared_from = std::move(fromPeerName);
317 std::optional<std::string> tableName)
319 std::lock_guard<std::mutex> lk(
mtx_);
323 auto& n = *it->second;
326 n.table_name = tableName;
329 if (n.file_path.empty())
333 std::filesystem::create_directories(n.file_path.parent_path(), ec);
335 n.last_update_ts = Clock::now();
341 n.saved_locally =
true;
349 auto slug =
slugify(n.title.empty() ?
"note" : n.title);
350 auto fname = slug +
"__" + n.uuid +
".md";
359 std::random_device rd;
360 std::mt19937_64 gen(rd());
361 uint64_t a = gen(), b = gen();
362 std::ostringstream oss;
363 oss << std::hex << std::nouppercase << std::setfill(
'0')
364 << std::setw(16) << a << std::setw(16) << b;
371 out.reserve(s.size());
372 for (
unsigned char c : s)
375 out.push_back((
char)std::tolower(c));
376 else if (c ==
' ' || c ==
'-' || c ==
'_')
379 while (!out.empty() && out.back() ==
'-')
388 return std::chrono::duration_cast<std::chrono::milliseconds>(tp.time_since_epoch()).count();
392 return std::chrono::system_clock::time_point(std::chrono::milliseconds(ms));
398 std::ifstream in(path, std::ios::binary);
401 std::string text((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
405 auto startsWith = [&](
const char* s)
406 {
return text.compare(pos, std::strlen(s), s) == 0; };
408 std::unordered_map<std::string, std::string> meta;
411 if (startsWith(
"---"))
414 while (pos < text.size() && (text[pos] ==
'\r' || text[pos] ==
'\n'))
416 while (pos < text.size())
418 if (text.compare(pos, 3,
"---") == 0)
421 while (pos < text.size() && (text[pos] ==
'\r' || text[pos] ==
'\n'))
425 size_t lineEnd = text.find_first_of(
"\r\n", pos);
426 std::string line = (lineEnd == std::string::npos) ? text.substr(pos)
427 : text.substr(pos, lineEnd - pos);
428 if (lineEnd == std::string::npos)
433 while (pos < text.size() && (text[pos] ==
'\r' || text[pos] ==
'\n'))
436 auto colon = line.find(
':');
437 if (colon != std::string::npos)
439 auto k = line.substr(0, colon);
440 auto v = line.substr(colon + 1);
441 auto ltrim = [](std::string& s)
442 { s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](
int ch)
443 { return !std::isspace(ch); })); };
444 auto rtrim = [](std::string& s)
445 { s.erase(std::find_if(s.rbegin(), s.rend(), [](
int ch)
446 {
return !std::isspace(ch); })
449 auto trim = [&](std::string& s)
450 { ltrim(s); rtrim(s); };
456 body = (pos < text.size()) ? text.substr(pos) : std::string{};
465 n.title = meta.count(
"title") ? meta[
"title"] : path.stem().string();
466 n.author = meta.count(
"author") ? meta[
"author"] :
"unknown";
467 if (meta.count(
"creation_ts"))
475 n.creation_ts = Clock::now();
479 n.creation_ts = Clock::now();
480 if (meta.count(
"last_update_ts"))
484 n.last_update_ts =
fromEpochMillis(std::stoll(meta[
"last_update_ts"]));
488 n.last_update_ts = n.creation_ts;
492 n.last_update_ts = n.creation_ts;
494 if (meta.count(
"table") && !meta[
"table"].empty())
495 n.table_name = meta[
"table"];
496 if (meta.count(
"shared"))
497 n.shared = (meta[
"shared"] ==
"1");
498 if (meta.count(
"shared_from") && !meta[
"shared_from"].empty())
499 n.shared_from = meta[
"shared_from"];
501 n.markdown_text = std::move(body);
502 outNote = std::move(n);
508 std::ofstream out(path, std::ios::binary);
516 out <<
"uuid: " << note.uuid <<
"\n";
517 out <<
"title: " << note.title <<
"\n";
518 out <<
"author: " << note.author <<
"\n";
519 out <<
"creation_ts: " << c_ms <<
"\n";
520 out <<
"last_update_ts: " << u_ms <<
"\n";
521 out <<
"table: " << (note.table_name ? *note.table_name :
"") <<
"\n";
522 out <<
"shared: " << (note.shared ?
"1" :
"0") <<
"\n";
523 out <<
"shared_from: " << (note.shared_from ? *note.shared_from :
"") <<
"\n";
525 out << note.markdown_text;
554 return (it !=
notesByUuid_.end()) ? it->second :
nullptr;
564 if (!n->title.empty())
577 if (!it->second->title.empty())
579 auto key =
toLower_(it->second->title);
612 std::string matchUuid;
616 const std::string& uuid = kv.first;
622 compact.push_back((
char)std::tolower((
unsigned char)c));
625 if (compact.rfind(needle, 0) == 0)
627 if (matchUuid.empty())
636 if (!ambig && !matchUuid.empty())
650 const int dashPos[4] = {8, 13, 18, 23};
651 for (
int i = 0; i < 36; ++i)
653 if (i == dashPos[0] || i == dashPos[1] || i == dashPos[2] || i == dashPos[3])
658 else if (!std::isxdigit((
unsigned char)s[i]))
674 if (!std::isxdigit((
unsigned char)c))
683 out.resize(s.size());
684 std::transform(s.begin(), s.end(), out.begin(),
686 { return (char)std::tolower(c); });
static bool isMarkdownExt(const std::filesystem::path &p)
std::chrono::steady_clock Clock
static bool parseMarkdownFile(const std::filesystem::path &path, Note &outNote)
bool saveInboxToLocal(const std::string &uuid, std::optional< std::string > tableName=std::nullopt)
static bool looksLikeShortHex_(const std::string &s)
static std::string toLower_(const std::string &s)
void toastGood(const std::string &msg) const
void setTitle(const std::string &uuid, std::string title)
std::string createNote(std::string title, std::string author, std::optional< std::string > tableName=std::nullopt)
static std::string slugify(const std::string &title)
std::vector< std::shared_ptr< Note > > listInbox()
std::unordered_map< std::string, std::string > titleToUuid_
NotesManager(NotesManagerConfig cfg, std::shared_ptr< ImGuiToaster > toaster)
std::string resolveRef(const std::string &ref) const
std::unordered_map< std::string, std::shared_ptr< Note > > notesByUuid_
std::shared_ptr< Note > getByUuid(const std::string &uuid) const
void removeFromIndex(const std::string &uuid)
void toastWarn(const std::string &msg) const
bool deleteNote(const std::string &uuid, bool deleteFromDisk)
void indexNote(const std::shared_ptr< Note > &n)
std::vector< std::shared_ptr< Note > > listMyNotes()
bool saveNote(const std::string &uuid)
void setContent(const std::string &uuid, std::string md)
bool saveNoteAs(const std::string &uuid, const std::filesystem::path &absolutePath)
void setAuthor(const std::string &uuid, std::string author)
void toastError(const std::string &msg) const
static std::string generateUUID()
std::string upsertSharedIncoming(std::string uuid, std::string title, std::string author, std::string markdown, std::optional< std::string > fromPeerName)
void scanFolderForMd_(const std::filesystem::path &dir, std::optional< std::string > tableName)
static bool looksLikeUuid_(const std::string &s)
std::vector< std::shared_ptr< Note > > listAll()
void toastInfo(const std::string &msg) const
static std::chrono::system_clock::time_point fromEpochMillis(int64_t ms)
std::shared_ptr< ImGuiToaster > toaster_
std::shared_ptr< Note > getNote(const std::string &uuid)
static int64_t toEpochMillis(std::chrono::system_clock::time_point tp)
void loadFromTable(const std::string &tableName)
std::unordered_map< std::string, std::shared_ptr< Note > > notesById_
std::filesystem::path defaultSavePath(const Note ¬e) const
static bool writeMarkdownFile(const std::filesystem::path &path, const Note ¬e)
std::filesystem::path gameTablesRootDir
std::filesystem::path globalNotesDir