#include "text_pager.h" #include namespace roro::files { TextPager::TextPager(ReadAt read, uint32_t size, int cols, int rows) : read_(std::move(read)), size_(size), cols_(std::max(1, cols)), rows_(std::max(1, rows)) {} const uint8_t* TextPager::bytes(uint32_t at, size_t& len) { len = 0; if (at >= size_) return nullptr; bool cached = at >= cacheAt_ && at < cacheAt_ + cache_.size(); // Wanted: a line's worth ahead, unless the cache already reaches the end of the file. size_t ahead = cached ? cacheAt_ + cache_.size() - at : 0; if (!cached || (ahead < kBlock / 4 && cacheAt_ + cache_.size() < size_)) { cacheAt_ = at - std::min(at, kBlock / 2); // room behind too: scrolling back is common cache_.resize(std::min(kBlock, size_ - cacheAt_)); cache_.resize(read_(cacheAt_, cache_.data(), cache_.size())); if (at >= cacheAt_ + cache_.size()) return nullptr; // the file got shorter, or the card failed } len = cacheAt_ + cache_.size() - at; return cache_.data() + (at - cacheAt_); } int TextPager::byteAt(uint32_t at) { size_t len; const uint8_t* p = bytes(at, len); return p ? *p : -1; } uint32_t TextPager::nextLine(uint32_t at, std::string* text) { size_t len; const uint8_t* p = bytes(at, len); if (text) text->clear(); if (!p) return size_; size_t end = len, next = len; // the line is [0, end); the one after starts at `next` int count = 0; size_t lastSpace = 0; for (size_t i = 0; i < len; i++) { uint8_t b = p[i]; if (b == '\n') { end = i; next = i + 1; break; } if ((b & 0xC0) == 0x80) continue; // inside a UTF-8 character if (count == cols_) { // one character too many: wrap if (b == ' ') end = i, next = i + 1; else if (lastSpace > 0) end = lastSpace, next = lastSpace + 1; else end = next = i; break; } count++; if (b == ' ') lastSpace = i; } if (text) { size_t n = end > 0 && p[end - 1] == '\r' ? end - 1 : end; text->reserve(n); for (size_t i = 0; i < n; i++) text->push_back(p[i] == '\t' ? ' ' : (p[i] < 0x20 || p[i] == 0x7F) ? '.' : static_cast(p[i])); } return at + static_cast(std::max(next, 1)); } uint32_t TextPager::lineBefore(uint32_t at) { if (at == 0) return 0; at = std::min(at, size_); // The paragraph the line before `at` belongs to starts after the newline before it. The byte // just before `at` may be that line's own newline. uint32_t from = at - 1; if (from > 0 && byteAt(from) == '\n') from--; uint32_t limit = at > kLookBack ? at - kLookBack : 0, start = limit; for (uint32_t i = from + 1; i-- > limit;) { if (byteAt(i) == '\n' && i < at - 1) { start = i + 1; break; } } // No newline that near: any character boundary will do as a place to wrap from. while (start > 0 && start < at && (byteAt(start) & 0xC0) == 0x80) start++; for (uint32_t a = start;;) { uint32_t next = nextLine(a, nullptr); if (next >= at) return a; a = next; } } bool TextPager::atEnd() { uint32_t a = top_; for (int i = 0; i < rows_; i++) { a = nextLine(a, nullptr); if (a >= size_) return true; } return false; } void TextPager::down(int n) { for (; n > 0 && !atEnd(); n--) top_ = nextLine(top_, nullptr); } void TextPager::up(int n) { for (; n > 0 && top_ > 0; n--) top_ = lineBefore(top_); } void TextPager::toEnd() { top_ = size_; up(rows_); } std::vector TextPager::lines() { std::vector out; uint32_t a = top_; for (int i = 0; i < rows_ && a < size_; i++) { std::string text; a = nextLine(a, &text); out.push_back(std::move(text)); } return out; } } // namespace roro::files