#define BASIC_IMPL #include "../pdf_browser/basic.h" #include "filesystem.h" #include "tests.h" #include "raylib.h" #include #include #include Arena Perm; /* TODO: - New threading model idea: I could just spin up a bunch of threads and then don't kill them. Just use them for searching! Each one would have it's own xarena and so on. - Improve scrolling - Highlight selected line - Improve the looks of the application */ struct TimeString { uint16_t hour; uint16_t minute; uint16_t second; String string; }; Array ParseSrtFile(Arena *arena, String filename) { String content = ReadFile(*arena, filename); Array lines = Split(*arena, content, "\n"); IterRemove(lines) { IterRemovePrepare(lines); it = Trim(it); if (it.len == 0) remove_item = true; } long section_number = 1; Array time_strings = {*arena}; for (int i = 0; i < lines.len;) { String it0 = lines[i++]; long num = strtol(it0.data, NULL, 10); Assert(section_number == num); section_number += 1; TimeString item = {}; String it1 = lines[i++]; item.hour = (uint16_t)strtol(it1.data, NULL, 10); item.minute = (uint16_t)strtol(it1.data + 3, NULL, 10); item.second = (uint16_t)strtol(it1.data + 6, NULL, 10); String next_section_number = Format(*arena, "%d", section_number); while (i < lines.len && lines[i] != next_section_number) { String it = lines[i]; item.string = lines[i]; time_strings.add(item); i += 1; } } IterRemove(time_strings) { IterRemovePrepare(time_strings); if (i > 0 && AreEqual(time_strings[i - 1].string, time_strings[i].string, true)) { remove_item = true; } } return time_strings; } struct TimeFile { Array time_strings; String file; }; struct ParseThreadIO { Array input_files; // output Arena *arena; Array time_files; }; void ParseThreadEntry(ParseThreadIO *io) { io->arena = AllocArena(); io->time_files.allocator = *io->arena; For(io->input_files) { Array time_strings = ParseSrtFile(io->arena, it); io->time_files.add({time_strings, it}); } } struct XToTimeString { String string; // String inside transcript arena uint16_t hour; uint16_t minute; uint16_t second; String filepath; }; Arena XArena; void AddFolder(String folder, Array *filenames, Array *x_to_time_string) { Scratch scratch; Array srt_files = {scratch}; for (FileIter iter = IterateFiles(scratch, folder); IsValid(iter); Advance(&iter)) { String file = Copy(Perm, iter.absolute_path); filenames->add(file); if (EndsWith(iter.filename, ".srt")) { srt_files.add(file); } } int64_t thread_count = 16; Array threads = {scratch}; int64_t files_per_thread = srt_files.len / thread_count; int64_t remainder = srt_files.len % thread_count; int64_t fi = 0; Array io = {scratch}; io.reserve(thread_count); for (int ti = 0; ti < thread_count; ti += 1) { Array files = {scratch}; for (int i = 0; fi < srt_files.len && i < files_per_thread + remainder; fi += 1, i += 1) { files.add(srt_files[fi]); } if (remainder) remainder = 0; ParseThreadIO *i = io.alloc(); i->input_files = files; threads.add(new std::thread(ParseThreadEntry, i)); } For(threads) { it->join(); delete it; } ForItem(it_io, io) { ForItem(it_time_file, it_io.time_files) { For(it_time_file.time_strings) { String s = Copy(XArena, it.string); s.data[s.len] = ' '; x_to_time_string->add({s, it.hour, it.minute, it.second, it_time_file.file}); } } Release(it_io.arena); } } // // Searching thread // Arena MatchesArena; Array Matches = {MatchesArena}; XToTimeString *ItemFound; Array Prompt; // system allocated std::mutex SearchThreadArrayMutex; std::binary_semaphore SearchThreadSemaphore{0}; bool SearchThreadStopSearching = false; bool SearchThreadRunning = true; void SearchThreadEntry() { InitArena(&MatchesArena); for (;;) { SearchThreadSemaphore.acquire(); if (!SearchThreadRunning) break; SearchThreadStopSearching = false; if (Prompt.len) { SearchThreadArrayMutex.lock(); { Matches.clear(); } SearchThreadArrayMutex.unlock(); String buffer = {(char *)XArena.data, (int64_t)XArena.len}; String find = {Prompt.data, Prompt.len}; int64_t index = 0; while (Seek(buffer, find, &index, SeekFlag_IgnoreCase)) { String found = {buffer.data + index, find.len}; SearchThreadArrayMutex.lock(); { Matches.add(found); } SearchThreadArrayMutex.unlock(); if (SearchThreadStopSearching) break; buffer = buffer.skip(index + find.len); } } } } void SearchThreadClose(std::thread &thread) { SearchThreadRunning = false; SearchThreadSemaphore.release(); thread.join(); } int main() { InitOS(); InitScratch(); InitArena(&Perm); InitArena(&XArena); Arena *frame_arena = AllocArena(); XArena.align = 0; String start_string = "read=D:/zizek"; For(start_string) Prompt.add(it); std::thread search_thread(SearchThreadEntry); int64_t chosen_text = 0; int64_t match_search_offset = 0; Array filenames = {}; Array x_to_time_string = {}; InitWindow(1920, 1080, "Transcript Browser"); SetWindowState(FLAG_WINDOW_RESIZABLE); SetTargetFPS(60); Font font = LoadFontEx("C:/Windows/Fonts/consola.ttf", 20, 0, 250); while (!WindowShouldClose()) { Clear(frame_arena); for (int key = GetCharPressed(); key; key = GetCharPressed()) { UTF8Result utf8 = UTF32ToUTF8(key); if (utf8.error) { Prompt.add('?'); continue; } for (int i = 0; i < utf8.len; i += 1) { Prompt.add(utf8.out_str[i]); match_search_offset = 0; SearchThreadStopSearching = true; SearchThreadSemaphore.release(); } } if (IsKeyPressed(KEY_BACKSPACE) || IsKeyPressedRepeat(KEY_BACKSPACE)) { if (ItemFound) { ItemFound = NULL; } else if (Prompt.len > 0) { Prompt.pop(); match_search_offset = 0; SearchThreadStopSearching = true; SearchThreadSemaphore.release(); } } int64_t offset_size = 1; if (IsKeyDown(KEY_LEFT_CONTROL)) { offset_size = 10; } if (IsKeyPressed(KEY_DOWN) || IsKeyPressedRepeat(KEY_DOWN)) { chosen_text += offset_size; if (chosen_text > 10) match_search_offset += offset_size; } if (IsKeyPressed(KEY_UP) || IsKeyPressedRepeat(KEY_UP)) { chosen_text -= offset_size; match_search_offset -= offset_size; } chosen_text = Clamp(chosen_text, (int64_t)0, Max(Matches.len - 1, (int64_t)0)); match_search_offset = Clamp(match_search_offset, (int64_t)0, Max(Matches.len - 1 - 10, (int64_t)0)); if (IsKeyPressed(KEY_ENTER)) { String prompt = {Prompt.data, Prompt.len}; if (StartsWith(prompt, "read=")) { Prompt.add('\0'); AddFolder(prompt.skip(5), &filenames, &x_to_time_string); Prompt.clear(); } else if (ItemFound) { String base = ChopLastPeriod(ItemFound->filepath); // .srt base = ChopLastPeriod(base); // .en For(filenames) { if (StartsWith(it, base)) { if (EndsWith(it, ".mkv") || EndsWith(it, ".webm") || EndsWith(it, ".mp4")) { int seconds = ItemFound->hour * 60 * 60 + ItemFound->minute * 60 + ItemFound->second; String copy = Copy(*frame_arena, it); for (int i = 0; i < copy.len; i += 1) if (copy.data[i] == '/') copy.data[i] = '\\'; String args = Format(*frame_arena, "C:\\Program Files\\VideoLAN\\VLC\\vlc.exe --start-time %d \"%.*s\"", seconds, FmtString(copy)); printf("%.*s\n", FmtString(args)); RunEx(args); } } } } else if (Matches.len) { String string = Matches[chosen_text]; For(x_to_time_string) { uintptr_t begin = (uintptr_t)(it.string.data); uintptr_t end = (uintptr_t)(it.string.data + it.string.len); uintptr_t needle = (uintptr_t)string.data; if (needle >= begin && needle < end) { ItemFound = ⁢ break; } } } } BeginDrawing(); ClearBackground(RAYWHITE); float font_size = 20; float y = 0; int xwidth = (int)MeasureTextEx(font, "_", font_size, 1).x; if (ItemFound) { uintptr_t begin_region = (uintptr_t)XArena.data; uintptr_t end_region = (uintptr_t)XArena.data + XArena.len; uintptr_t begin = (uintptr_t)(ItemFound->string.data - 1000); uintptr_t end = (uintptr_t)(ItemFound->string.data + 1000); begin = Clamp(begin, begin_region, end_region); end = Clamp(end, begin_region, end_region); String string = {(char *)begin, (int64_t)(end - begin)}; String filename = SkipToLastSlash(ItemFound->filepath); DrawTextEx(font, filename.data, {0, y}, font_size, 1, BLACK); y += font_size; int per_line = (GetRenderWidth() / xwidth) - 20; for (String it = string; it.len;) { String line = it.get_prefix(per_line); if (ItemFound->string.data >= line.data && ItemFound->string.data < line.data + line.len) { DrawRectangleLines(0, (int)(y + font_size), GetRenderWidth(), 2, SKYBLUE); } String line_terminated = Copy(*frame_arena, line); DrawTextEx(font, line_terminated.data, {0, y}, font_size, 1, DARKGRAY); y += font_size; it = it.skip(per_line); } } else { Prompt.add('\0'); DrawTextEx(font, "> ", {0, y}, font_size, 1, BLACK); DrawTextEx(font, Prompt.data, {(float)xwidth * 3, y}, font_size, 1, BLACK); Prompt.pop(); y += font_size; int64_t chars_per_line = GetRenderWidth() / xwidth - Prompt.len; SearchThreadArrayMutex.lock(); for (int64_t i = match_search_offset; i < Matches.len; i += 1) { String it = Matches[i]; uintptr_t begin_region = (uintptr_t)XArena.data; uintptr_t end_region = (uintptr_t)XArena.data + XArena.len; uintptr_t begin = (uintptr_t)(it.data - chars_per_line / 2); uintptr_t end = (uintptr_t)(it.data + chars_per_line / 2); begin = Clamp(begin, begin_region, end_region); end = Clamp(end, begin_region, end_region); String string = Copy(*frame_arena, {(char *)begin, (int64_t)(end - begin)}); String string_first = Copy(*frame_arena, {(char *)begin, (int64_t)(it.data - begin)}); String string_middle = Copy(*frame_arena, it); int width = (int)MeasureTextEx(font, string_first.data, font_size, 1).x; if (chosen_text == i) DrawRectangleLines(0, (int)(y + font_size), GetRenderWidth(), 2, SKYBLUE); String num = Format(*frame_arena, "%d", i); DrawTextEx(font, num.data, {0, y}, font_size, 1, DARKGRAY); DrawTextEx(font, string.data, {(float)xwidth * 4, y}, font_size, 1, DARKGRAY); DrawTextEx(font, string_middle.data, {(float)xwidth * 4 + (float)width, y}, font_size, 1, SKYBLUE); y += font_size; if (y > GetRenderHeight()) break; } SearchThreadArrayMutex.unlock(); } if (IsKeyDown(KEY_F1)) DrawFPS(0, 0); EndDrawing(); } CloseWindow(); SearchThreadClose(search_thread); }