From 91777a9023a9c57a21edd2a53e820fde66ac502f Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Fri, 6 Feb 2026 03:18:47 +1100 Subject: [PATCH 01/29] release: 1.0.0 --- .github/workflows/release_candidate.yml | 48 +++++++++++++++++++++++++ platformio.ini | 8 ++++- 2 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release_candidate.yml diff --git a/.github/workflows/release_candidate.yml b/.github/workflows/release_candidate.yml new file mode 100644 index 00000000..8f704484 --- /dev/null +++ b/.github/workflows/release_candidate.yml @@ -0,0 +1,48 @@ +name: Compile Release Candidate + +on: + pull_request: + +jobs: + build-release-candidate: + if: startsWith(github.head_ref, 'release/') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: recursive + + - uses: actions/cache@v5 + with: + path: | + ~/.cache/pip + ~/.platformio/.cache + key: ${{ runner.os }}-pio + + - uses: actions/setup-python@v6 + with: + python-version: '3.14' + + - name: Install PlatformIO Core + run: pip install --upgrade platformio + + - name: Extract env + run: | + echo "SHORT_SHA=${GITHUB_SHA::6}" >> $GITHUB_ENV + echo "BRANCH_SUFFIX=${GITHUB_REF_NAME#release/}" >> $GITHUB_ENV + + - name: Build CrossPoint Release Candidate + env: + CROSSPOINT_RC_HASH: ${{ env.SHORT_SHA }} + run: pio run -e gh_release_rc + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: CrossPoint-RC-${{ env.BRANCH_SUFFIX }} + path: | + .pio/build/gh_release_rc/bootloader.bin + .pio/build/gh_release_rc/firmware.bin + .pio/build/gh_release_rc/firmware.elf + .pio/build/gh_release_rc/firmware.map + .pio/build/gh_release_rc/partitions.bin diff --git a/platformio.ini b/platformio.ini index e8574470..c4b992e8 100644 --- a/platformio.ini +++ b/platformio.ini @@ -2,7 +2,7 @@ default_envs = default [crosspoint] -version = 0.16.0 +version = 1.0.0 [base] platform = espressif32 @ 6.12.0 @@ -60,3 +60,9 @@ extends = base build_flags = ${base.build_flags} -DCROSSPOINT_VERSION=\"${crosspoint.version}\" + +[env:gh_release_rc] +extends = base +build_flags = + ${base.build_flags} + -DCROSSPOINT_VERSION=\"${crosspoint.version}-rc-${sysenv.CROSSPOINT_RC_HASH}\" From 211153fcd5d9e6d01cb2b61847d8e401e67c5e96 Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Fri, 6 Feb 2026 03:49:00 +1100 Subject: [PATCH 02/29] Use GITHUB_HEAD_REF --- .github/workflows/release_candidate.yml | 4 ++-- platformio.ini | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release_candidate.yml b/.github/workflows/release_candidate.yml index 8f704484..e9bbc636 100644 --- a/.github/workflows/release_candidate.yml +++ b/.github/workflows/release_candidate.yml @@ -28,8 +28,8 @@ jobs: - name: Extract env run: | - echo "SHORT_SHA=${GITHUB_SHA::6}" >> $GITHUB_ENV - echo "BRANCH_SUFFIX=${GITHUB_REF_NAME#release/}" >> $GITHUB_ENV + echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV + echo "BRANCH_SUFFIX=${GITHUB_HEAD_REF#release/}" >> $GITHUB_ENV - name: Build CrossPoint Release Candidate env: diff --git a/platformio.ini b/platformio.ini index c4b992e8..c441e526 100644 --- a/platformio.ini +++ b/platformio.ini @@ -65,4 +65,4 @@ build_flags = extends = base build_flags = ${base.build_flags} - -DCROSSPOINT_VERSION=\"${crosspoint.version}-rc-${sysenv.CROSSPOINT_RC_HASH}\" + -DCROSSPOINT_VERSION=\"${crosspoint.version}-rc+${sysenv.CROSSPOINT_RC_HASH}\" From 3223e85ea5f64e45429495bcd274080d03220887 Mon Sep 17 00:00:00 2001 From: Jake Kenneally Date: Fri, 6 Feb 2026 02:49:04 -0500 Subject: [PATCH 03/29] feat: Add Settings for toggling CSS on or off (#717) Closes #712 ## Summary **What is the goal of this PR?** - To add new settings for toggling on/off embedded CSS styles in the reader. This gives more control and customization to the user over how the ereader experience looks. **What changes are included?** - Added new "Embedded Style" option to the Reader settings - Added new "Book's Style" option for "Paragraph Alignment" - User's selected "Paragraph Alignment" will take precedence and override the embedded CSS `text-align` property, _unless_ the user has "Book's Style" set as their "Paragraph Alignment" ## Additional Context ![IMG_6336](https://github.com/user-attachments/assets/dff619ef-986d-465e-b352-73a76baae334) https://github.com/user-attachments/assets/9e404b13-c7e0-41c7-9406-4715f389166a Addresses feedback from the community about the new CSS feature: https://github.com/crosspoint-reader/crosspoint-reader/pull/700 --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**YES**_, Claude Code --- lib/Epub/Epub/Section.cpp | 24 +++++++++++-------- lib/Epub/Epub/Section.h | 7 +++--- lib/Epub/Epub/blocks/BlockStyle.h | 5 ++-- lib/Epub/Epub/css/CssStyle.h | 2 +- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 18 ++++++++++---- lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h | 7 ++++-- src/CrossPointSettings.cpp | 5 +++- src/CrossPointSettings.h | 3 +++ src/activities/reader/EpubReaderActivity.cpp | 4 ++-- src/activities/settings/SettingsActivity.cpp | 5 ++-- 10 files changed, 53 insertions(+), 27 deletions(-) diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 9cb70027..18e0faef 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -8,9 +8,9 @@ #include "parsers/ChapterHtmlSlimParser.h" namespace { -constexpr uint8_t SECTION_FILE_VERSION = 11; +constexpr uint8_t SECTION_FILE_VERSION = 12; constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) + - sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) + sizeof(uint32_t); } // namespace @@ -33,7 +33,8 @@ uint32_t Section::onPageComplete(std::unique_ptr page) { void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing, const uint8_t paragraphAlignment, const uint16_t viewportWidth, - const uint16_t viewportHeight, const bool hyphenationEnabled) { + const uint16_t viewportHeight, const bool hyphenationEnabled, + const bool embeddedStyle) { if (!file) { Serial.printf("[%lu] [SCT] File not open for writing header\n", millis()); return; @@ -41,7 +42,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) + sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) + sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) + - sizeof(uint32_t), + sizeof(embeddedStyle) + sizeof(uint32_t), "Header size mismatch"); serialization::writePod(file, SECTION_FILE_VERSION); serialization::writePod(file, fontId); @@ -51,13 +52,14 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi serialization::writePod(file, viewportWidth); serialization::writePod(file, viewportHeight); serialization::writePod(file, hyphenationEnabled); + serialization::writePod(file, embeddedStyle); serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0 when written) serialization::writePod(file, static_cast(0)); // Placeholder for LUT offset } bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, const uint8_t paragraphAlignment, const uint16_t viewportWidth, - const uint16_t viewportHeight, const bool hyphenationEnabled) { + const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle) { if (!SdMan.openFileForRead("SCT", filePath, file)) { return false; } @@ -79,6 +81,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con bool fileExtraParagraphSpacing; uint8_t fileParagraphAlignment; bool fileHyphenationEnabled; + bool fileEmbeddedStyle; serialization::readPod(file, fileFontId); serialization::readPod(file, fileLineCompression); serialization::readPod(file, fileExtraParagraphSpacing); @@ -86,11 +89,12 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con serialization::readPod(file, fileViewportWidth); serialization::readPod(file, fileViewportHeight); serialization::readPod(file, fileHyphenationEnabled); + serialization::readPod(file, fileEmbeddedStyle); if (fontId != fileFontId || lineCompression != fileLineCompression || extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment || viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight || - hyphenationEnabled != fileHyphenationEnabled) { + hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle) { file.close(); Serial.printf("[%lu] [SCT] Deserialization failed: Parameters do not match\n", millis()); clearCache(); @@ -122,7 +126,7 @@ bool Section::clearCache() const { bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing, const uint8_t paragraphAlignment, const uint16_t viewportWidth, - const uint16_t viewportHeight, const bool hyphenationEnabled, + const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle, const std::function& popupFn) { const auto localPath = epub->getSpineItem(spineIndex).href; const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html"; @@ -173,14 +177,14 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c return false; } writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, - viewportHeight, hyphenationEnabled); + viewportHeight, hyphenationEnabled, embeddedStyle); std::vector lut = {}; ChapterHtmlSlimParser visitor( tmpHtmlPath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, viewportHeight, hyphenationEnabled, - [this, &lut](std::unique_ptr page) { lut.emplace_back(this->onPageComplete(std::move(page))); }, popupFn, - epub->getCssParser()); + [this, &lut](std::unique_ptr page) { lut.emplace_back(this->onPageComplete(std::move(page))); }, + embeddedStyle, popupFn, embeddedStyle ? epub->getCssParser() : nullptr); Hyphenator::setPreferredLanguage(epub->getLanguage()); success = visitor.parseAndBuildPages(); diff --git a/lib/Epub/Epub/Section.h b/lib/Epub/Epub/Section.h index 5fdf210a..42a6d993 100644 --- a/lib/Epub/Epub/Section.h +++ b/lib/Epub/Epub/Section.h @@ -15,7 +15,8 @@ class Section { FsFile file; void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, - uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled); + uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, + bool embeddedStyle); uint32_t onPageComplete(std::unique_ptr page); public: @@ -29,10 +30,10 @@ class Section { filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {} ~Section() = default; bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, - uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled); + uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle); bool clearCache() const; bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, - uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, + uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle, const std::function& popupFn = nullptr); std::unique_ptr loadPageFromSectionFile(); }; diff --git a/lib/Epub/Epub/blocks/BlockStyle.h b/lib/Epub/Epub/blocks/BlockStyle.h index 5c26a21d..63b054c9 100644 --- a/lib/Epub/Epub/blocks/BlockStyle.h +++ b/lib/Epub/Epub/blocks/BlockStyle.h @@ -80,8 +80,9 @@ struct BlockStyle { blockStyle.textIndent = cssStyle.textIndent.toPixelsInt16(emSize); blockStyle.textIndentDefined = cssStyle.hasTextIndent(); blockStyle.textAlignDefined = cssStyle.hasTextAlign(); - if (blockStyle.textAlignDefined) { - blockStyle.alignment = cssStyle.textAlign; + // User setting overrides CSS, unless "Book's Style" alignment setting is selected + if (paragraphAlignment == CssTextAlign::None) { + blockStyle.alignment = blockStyle.textAlignDefined ? cssStyle.textAlign : CssTextAlign::Justify; } else { blockStyle.alignment = paragraphAlignment; } diff --git a/lib/Epub/Epub/css/CssStyle.h b/lib/Epub/Epub/css/CssStyle.h index 7b83da3f..adbc19e2 100644 --- a/lib/Epub/Epub/css/CssStyle.h +++ b/lib/Epub/Epub/css/CssStyle.h @@ -3,7 +3,7 @@ #include // Matches order of PARAGRAPH_ALIGNMENT in CrossPointSettings -enum class CssTextAlign : uint8_t { Justify = 0, Left = 1, Center = 2, Right = 3 }; +enum class CssTextAlign : uint8_t { Justify = 0, Left = 1, Center = 2, Right = 3, None = 4 }; enum class CssUnit : uint8_t { Pixels = 0, Em = 1, Rem = 2, Points = 3 }; // Represents a CSS length value with its unit, allowing deferred resolution to pixels diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index ab93d9cb..cb5625c1 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -211,11 +211,17 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* } const float emSize = static_cast(self->renderer.getLineHeight(self->fontId)) * self->lineCompression; - const auto userAlignment = static_cast(self->paragraphAlignment); + const auto userAlignmentBlockStyle = + BlockStyle::fromCssStyle(cssStyle, emSize, static_cast(self->paragraphAlignment)); if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) { self->currentCssStyle = cssStyle; - self->startNewTextBlock(BlockStyle::fromCssStyle(cssStyle, emSize, userAlignment)); + auto headerBlockStyle = BlockStyle::fromCssStyle(cssStyle, emSize, CssTextAlign::Center); + headerBlockStyle.textAlignDefined = true; + if (self->embeddedStyle && cssStyle.hasTextAlign()) { + headerBlockStyle.alignment = cssStyle.textAlign; + } + self->startNewTextBlock(headerBlockStyle); self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth); self->updateEffectiveInlineStyle(); } else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) { @@ -227,7 +233,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* self->startNewTextBlock(self->currentTextBlock->getBlockStyle()); } else { self->currentCssStyle = cssStyle; - self->startNewTextBlock(BlockStyle::fromCssStyle(cssStyle, emSize, userAlignment)); + self->startNewTextBlock(userAlignmentBlockStyle); self->updateEffectiveInlineStyle(); if (strcmp(name, "li") == 0) { @@ -430,7 +436,11 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n bool ChapterHtmlSlimParser::parseAndBuildPages() { auto paragraphAlignmentBlockStyle = BlockStyle(); paragraphAlignmentBlockStyle.textAlignDefined = true; - paragraphAlignmentBlockStyle.alignment = static_cast(this->paragraphAlignment); + // Resolve None sentinel to Justify for initial block (no CSS context yet) + const auto align = (this->paragraphAlignment == static_cast(CssTextAlign::None)) + ? CssTextAlign::Justify + : static_cast(this->paragraphAlignment); + paragraphAlignmentBlockStyle.alignment = align; startNewTextBlock(paragraphAlignmentBlockStyle); const XML_Parser parser = XML_ParserCreate(nullptr); diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index 92a9838a..261d8f4e 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -41,6 +41,7 @@ class ChapterHtmlSlimParser { uint16_t viewportHeight; bool hyphenationEnabled; const CssParser* cssParser; + bool embeddedStyle; // Style tracking (replaces depth-based approach) struct StyleStackEntry { @@ -70,7 +71,8 @@ class ChapterHtmlSlimParser { const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint16_t viewportHeight, const bool hyphenationEnabled, const std::function)>& completePageFn, - const std::function& popupFn = nullptr, const CssParser* cssParser = nullptr) + const bool embeddedStyle, const std::function& popupFn = nullptr, + const CssParser* cssParser = nullptr) : filepath(filepath), renderer(renderer), @@ -83,7 +85,8 @@ class ChapterHtmlSlimParser { hyphenationEnabled(hyphenationEnabled), completePageFn(completePageFn), popupFn(popupFn), - cssParser(cssParser) {} + cssParser(cssParser), + embeddedStyle(embeddedStyle) {} ~ChapterHtmlSlimParser() = default; bool parseAndBuildPages(); diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index da287046..28a357e9 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -22,7 +22,7 @@ void readAndValidate(FsFile& file, uint8_t& member, const uint8_t maxValue) { namespace { constexpr uint8_t SETTINGS_FILE_VERSION = 1; // Increment this when adding new persisted settings fields -constexpr uint8_t SETTINGS_COUNT = 29; +constexpr uint8_t SETTINGS_COUNT = 30; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; // Validate front button mapping to ensure each hardware button is unique. @@ -117,6 +117,7 @@ bool CrossPointSettings::saveToFile() const { serialization::writePod(outputFile, frontButtonLeft); serialization::writePod(outputFile, frontButtonRight); serialization::writePod(outputFile, fadingFix); + serialization::writePod(outputFile, embeddedStyle); // New fields added at end for backward compatibility outputFile.close(); @@ -220,6 +221,8 @@ bool CrossPointSettings::loadFromFile() { if (++settingsRead >= fileSettingsCount) break; serialization::readPod(inputFile, fadingFix); if (++settingsRead >= fileSettingsCount) break; + serialization::readPod(inputFile, embeddedStyle); + if (++settingsRead >= fileSettingsCount) break; // New fields added at end for backward compatibility } while (false); diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 86700cad..1348519f 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -86,6 +86,7 @@ class CrossPointSettings { LEFT_ALIGN = 1, CENTER_ALIGN = 2, RIGHT_ALIGN = 3, + BOOK_STYLE = 4, PARAGRAPH_ALIGNMENT_COUNT }; @@ -168,6 +169,8 @@ class CrossPointSettings { uint8_t uiTheme = LYRA; // Sunlight fading compensation uint8_t fadingFix = 0; + // Use book's embedded CSS styles for EPUB rendering (1 = enabled, 0 = disabled) + uint8_t embeddedStyle = 1; ~CrossPointSettings() = default; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 72600c57..5428a00c 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -575,14 +575,14 @@ void EpubReaderActivity::renderScreen() { if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, - viewportHeight, SETTINGS.hyphenationEnabled)) { + viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle)) { Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis()); const auto popupFn = [this]() { GUI.drawPopup(renderer, "Indexing..."); }; if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, - viewportHeight, SETTINGS.hyphenationEnabled, popupFn)) { + viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, popupFn)) { Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis()); section.reset(); return; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 893eb108..efe5b5ed 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -35,14 +35,15 @@ const SettingInfo displaySettings[displaySettingsCount] = { SettingInfo::Toggle("Sunlight Fading Fix", &CrossPointSettings::fadingFix), }; -constexpr int readerSettingsCount = 9; +constexpr int readerSettingsCount = 10; const SettingInfo readerSettings[readerSettingsCount] = { SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic"}), SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}), SettingInfo::Enum("Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}), SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}), SettingInfo::Enum("Paragraph Alignment", &CrossPointSettings::paragraphAlignment, - {"Justify", "Left", "Center", "Right"}), + {"Justify", "Left", "Center", "Right", "Book's Style"}), + SettingInfo::Toggle("Embedded Style", &CrossPointSettings::embeddedStyle), SettingInfo::Toggle("Hyphenation", &CrossPointSettings::hyphenationEnabled), SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation, {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}), From d8632eae0861c624e4f17f4bebe781f8ba777d51 Mon Sep 17 00:00:00 2001 From: CaptainFrito Date: Fri, 6 Feb 2026 14:58:32 +0700 Subject: [PATCH 04/29] fix: Lag before displaying covers on home screen (#721) ## Summary Reduce/fix the lag on the home screen before recent book covers are rendered ## Additional Context We were previously rendering the screen in two steps, delaying the recent book covers render to avoid a lag before the screen loads. In this PR, we are now doing that only if at least one book doesn't have the cover thumbnail generated yet. If all thumbs are already generated, we load and display them right away, with no lag. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**NO **_ --- lib/JpegToBmpConverter/JpegToBmpConverter.cpp | 2 +- lib/Xtc/Xtc.cpp | 2 +- src/RecentBooksStore.cpp | 13 +++ src/RecentBooksStore.h | 3 + src/activities/home/HomeActivity.cpp | 83 +++++++++---------- src/activities/home/HomeActivity.h | 5 +- src/components/themes/BaseTheme.cpp | 7 +- src/components/themes/lyra/LyraTheme.cpp | 49 ++++++----- 8 files changed, 95 insertions(+), 69 deletions(-) diff --git a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp index 837bfd27..0b1541b4 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp @@ -567,5 +567,5 @@ bool JpegToBmpConverter::jpegFileToBmpStreamWithSize(FsFile& jpegFile, Print& bm // Convert to 1-bit BMP (black and white only, no grays) for fast home screen rendering bool JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight) { - return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, true, false); + return jpegFileToBmpStreamInternal(jpegFile, bmpOut, targetMaxWidth, targetMaxHeight, true, true); } diff --git a/lib/Xtc/Xtc.cpp b/lib/Xtc/Xtc.cpp index 05f6651d..717bb670 100644 --- a/lib/Xtc/Xtc.cpp +++ b/lib/Xtc/Xtc.cpp @@ -340,7 +340,7 @@ bool Xtc::generateThumbBmp(int height) const { // Calculate scale factor float scaleX = static_cast(THUMB_TARGET_WIDTH) / pageInfo.width; float scaleY = static_cast(THUMB_TARGET_HEIGHT) / pageInfo.height; - float scale = (scaleX < scaleY) ? scaleX : scaleY; + float scale = (scaleX > scaleY) ? scaleX : scaleY; // for cropping // Only scale down, never up if (scale >= 1.0f) { diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index 276eb522..99586b79 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -38,6 +38,19 @@ void RecentBooksStore::addBook(const std::string& path, const std::string& title saveToFile(); } +void RecentBooksStore::updateBook(const std::string& path, const std::string& title, const std::string& author, + const std::string& coverBmpPath) { + auto it = + std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; }); + if (it != recentBooks.end()) { + RecentBook& book = *it; + book.title = title; + book.author = author; + book.coverBmpPath = coverBmpPath; + saveToFile(); + } +} + bool RecentBooksStore::saveToFile() const { // Make sure the directory exists SdMan.mkdir("/.crosspoint"); diff --git a/src/RecentBooksStore.h b/src/RecentBooksStore.h index 6f6a164a..8dbf0813 100644 --- a/src/RecentBooksStore.h +++ b/src/RecentBooksStore.h @@ -27,6 +27,9 @@ class RecentBooksStore { void addBook(const std::string& path, const std::string& title, const std::string& author, const std::string& coverBmpPath); + void updateBook(const std::string& path, const std::string& title, const std::string& author, + const std::string& coverBmpPath); + // Get the list of recent books (most recent first) const std::vector& getBooks() const { return recentBooks; } diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index ef5cc98f..15a59bfc 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -35,16 +35,11 @@ int HomeActivity::getMenuItemCount() const { return count; } -void HomeActivity::loadRecentBooks(int maxBooks, int coverHeight) { - recentsLoading = true; - bool showingLoading = false; - Rect popupRect; - +void HomeActivity::loadRecentBooks(int maxBooks) { recentBooks.clear(); const auto& books = RECENT_BOOKS.getBooks(); recentBooks.reserve(std::min(static_cast(books.size()), maxBooks)); - int progress = 0; for (const RecentBook& book : books) { // Limit to maximum number of recent books if (recentBooks.size() >= maxBooks) { @@ -56,19 +51,22 @@ void HomeActivity::loadRecentBooks(int maxBooks, int coverHeight) { continue; } + recentBooks.push_back(book); + } +} + +void HomeActivity::loadRecentCovers(int coverHeight) { + recentsLoading = true; + bool showingLoading = false; + Rect popupRect; + + int progress = 0; + for (RecentBook& book : recentBooks) { if (!book.coverBmpPath.empty()) { std::string coverPath = UITheme::getCoverThumbPath(book.coverBmpPath, coverHeight); if (!SdMan.exists(coverPath.c_str())) { - std::string lastBookFileName = ""; - const size_t lastSlash = book.path.find_last_of('/'); - if (lastSlash != std::string::npos) { - lastBookFileName = book.path.substr(lastSlash + 1); - } - - Serial.printf("Loading recent book: %s\n", book.path.c_str()); - // If epub, try to load the metadata for title/author and cover - if (StringUtils::checkFileExtension(lastBookFileName, ".epub")) { + if (StringUtils::checkFileExtension(book.path, ".epub")) { Epub epub(book.path, "/.crosspoint"); // Skip loading css since we only need metadata here epub.load(false, true); @@ -78,10 +76,16 @@ void HomeActivity::loadRecentBooks(int maxBooks, int coverHeight) { showingLoading = true; popupRect = GUI.drawPopup(renderer, "Loading..."); } - GUI.fillPopupProgress(renderer, popupRect, progress * 30); - epub.generateThumbBmp(coverHeight); - } else if (StringUtils::checkFileExtension(lastBookFileName, ".xtch") || - StringUtils::checkFileExtension(lastBookFileName, ".xtc")) { + GUI.fillPopupProgress(renderer, popupRect, 10 + progress * (90 / recentBooks.size())); + bool success = epub.generateThumbBmp(coverHeight); + if (!success) { + RECENT_BOOKS.updateBook(book.path, book.title, book.author, ""); + book.coverBmpPath = ""; + } + coverRendered = false; + updateRequired = true; + } else if (StringUtils::checkFileExtension(book.path, ".xtch") || + StringUtils::checkFileExtension(book.path, ".xtc")) { // Handle XTC file Xtc xtc(book.path, "/.crosspoint"); if (xtc.load()) { @@ -90,21 +94,23 @@ void HomeActivity::loadRecentBooks(int maxBooks, int coverHeight) { showingLoading = true; popupRect = GUI.drawPopup(renderer, "Loading..."); } - GUI.fillPopupProgress(renderer, popupRect, progress * 30); - xtc.generateThumbBmp(coverHeight); + GUI.fillPopupProgress(renderer, popupRect, 10 + progress * (90 / recentBooks.size())); + bool success = xtc.generateThumbBmp(coverHeight); + if (!success) { + RECENT_BOOKS.updateBook(book.path, book.title, book.author, ""); + book.coverBmpPath = ""; + } + coverRendered = false; + updateRequired = true; } } } } - - recentBooks.push_back(book); progress++; } - Serial.printf("Recent books loaded: %d\n", recentBooks.size()); recentsLoaded = true; recentsLoading = false; - updateRequired = true; } void HomeActivity::onEnter() { @@ -112,14 +118,14 @@ void HomeActivity::onEnter() { renderingMutex = xSemaphoreCreateMutex(); - // Check if we have a book to continue reading - hasContinueReading = !APP_STATE.openEpubPath.empty() && SdMan.exists(APP_STATE.openEpubPath.c_str()); - // Check if OPDS browser URL is configured hasOpdsUrl = strlen(SETTINGS.opdsServerUrl) > 0; selectorIndex = 0; + auto metrics = UITheme::getInstance().getMetrics(); + loadRecentBooks(metrics.homeRecentBooksCount); + // Trigger first update updateRequired = true; @@ -246,24 +252,14 @@ void HomeActivity::render() { const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); + renderer.clearScreen(); bool bufferRestored = coverBufferStored && restoreCoverBuffer(); - if (!firstRenderDone || (recentsLoaded && !recentsDisplayed)) { - renderer.clearScreen(); - } GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.homeTopPadding}, nullptr); - if (hasContinueReading) { - if (recentsLoaded) { - recentsDisplayed = true; - GUI.drawRecentBookCover(renderer, Rect{0, metrics.homeTopPadding, pageWidth, metrics.homeCoverTileHeight}, - recentBooks, selectorIndex, coverRendered, coverBufferStored, bufferRestored, - std::bind(&HomeActivity::storeCoverBuffer, this)); - } else if (!recentsLoading && firstRenderDone) { - recentsLoading = true; - loadRecentBooks(metrics.homeRecentBooksCount, metrics.homeCoverHeight); - } - } + GUI.drawRecentBookCover(renderer, Rect{0, metrics.homeTopPadding, pageWidth, metrics.homeCoverTileHeight}, + recentBooks, selectorIndex, coverRendered, coverBufferStored, bufferRestored, + std::bind(&HomeActivity::storeCoverBuffer, this)); // Build menu items dynamically std::vector menuItems = {"Browse Files", "Recents", "File Transfer", "Settings"}; @@ -288,5 +284,8 @@ void HomeActivity::render() { if (!firstRenderDone) { firstRenderDone = true; updateRequired = true; + } else if (!recentsLoaded && !recentsLoading) { + recentsLoading = true; + loadRecentCovers(metrics.homeCoverHeight); } } diff --git a/src/activities/home/HomeActivity.h b/src/activities/home/HomeActivity.h index f27a8f93..1f714217 100644 --- a/src/activities/home/HomeActivity.h +++ b/src/activities/home/HomeActivity.h @@ -17,10 +17,8 @@ class HomeActivity final : public Activity { SemaphoreHandle_t renderingMutex = nullptr; int selectorIndex = 0; bool updateRequired = false; - bool hasContinueReading = false; bool recentsLoading = false; bool recentsLoaded = false; - bool recentsDisplayed = false; bool firstRenderDone = false; bool hasOpdsUrl = false; bool coverRendered = false; // Track if cover has been rendered once @@ -41,7 +39,8 @@ class HomeActivity final : public Activity { bool storeCoverBuffer(); // Store frame buffer for cover image bool restoreCoverBuffer(); // Restore frame buffer from stored cover void freeCoverBuffer(); // Free the stored cover buffer - void loadRecentBooks(int maxBooks, int coverHeight); + void loadRecentBooks(int maxBooks); + void loadRecentCovers(int coverHeight); public: explicit HomeActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 3b23e3fe..2e8ddbc8 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -301,6 +301,7 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: { // Draw cover image as background if available (inside the box) // Only load from SD on first render, then use stored buffer + if (hasContinueReading && !recentBooks[0].coverBmpPath.empty() && !coverRendered) { const std::string coverBmpPath = UITheme::getCoverThumbPath(recentBooks[0].coverBmpPath, BaseMetrics::values.homeCoverHeight); @@ -310,6 +311,7 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: if (SdMan.openFileForRead("HOME", coverBmpPath, file)) { Bitmap bitmap(file); if (bitmap.parseHeaders() == BmpReaderError::Ok) { + Serial.printf("Rendering bmp\n"); // Calculate position to center image within the book card int coverX, coverY; @@ -343,13 +345,16 @@ void BaseTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: // First render: if selected, draw selection indicators now if (bookSelected) { + Serial.printf("Drawing selection\n"); renderer.drawRect(bookX + 1, bookY + 1, bookWidth - 2, bookHeight - 2); renderer.drawRect(bookX + 2, bookY + 2, bookWidth - 4, bookHeight - 4); } } file.close(); } - } else if (!bufferRestored && !coverRendered) { + } + + if (!bufferRestored && !coverRendered) { // No cover image: draw border or fill, plus bookmark as visual flair if (bookSelected) { renderer.fillRect(bookX, bookY, bookWidth, bookHeight); diff --git a/src/components/themes/lyra/LyraTheme.cpp b/src/components/themes/lyra/LyraTheme.cpp index 71d6a15d..c07e7a5f 100644 --- a/src/components/themes/lyra/LyraTheme.cpp +++ b/src/components/themes/lyra/LyraTheme.cpp @@ -274,30 +274,37 @@ void LyraTheme::drawRecentBookCover(GfxRenderer& renderer, Rect rect, const std: for (int i = 0; i < std::min(static_cast(recentBooks.size()), LyraMetrics::values.homeRecentBooksCount); i++) { std::string coverPath = recentBooks[i].coverBmpPath; + bool hasCover = true; + int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i; if (coverPath.empty()) { - continue; + hasCover = false; + } else { + const std::string coverBmpPath = UITheme::getCoverThumbPath(coverPath, LyraMetrics::values.homeCoverHeight); + + // First time: load cover from SD and render + FsFile file; + if (SdMan.openFileForRead("HOME", coverBmpPath, file)) { + Bitmap bitmap(file); + if (bitmap.parseHeaders() == BmpReaderError::Ok) { + float coverHeight = static_cast(bitmap.getHeight()); + float coverWidth = static_cast(bitmap.getWidth()); + float ratio = coverWidth / coverHeight; + const float tileRatio = static_cast(tileWidth - 2 * hPaddingInSelection) / + static_cast(LyraMetrics::values.homeCoverHeight); + float cropX = 1.0f - (tileRatio / ratio); + + renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection, + tileWidth - 2 * hPaddingInSelection, LyraMetrics::values.homeCoverHeight, cropX); + } else { + hasCover = false; + } + file.close(); + } } - const std::string coverBmpPath = UITheme::getCoverThumbPath(coverPath, LyraMetrics::values.homeCoverHeight); - - int tileX = LyraMetrics::values.contentSidePadding + tileWidth * i; - - // First time: load cover from SD and render - FsFile file; - if (SdMan.openFileForRead("HOME", coverBmpPath, file)) { - Bitmap bitmap(file); - if (bitmap.parseHeaders() == BmpReaderError::Ok) { - float coverHeight = static_cast(bitmap.getHeight()); - float coverWidth = static_cast(bitmap.getWidth()); - float ratio = coverWidth / coverHeight; - const float tileRatio = static_cast(tileWidth - 2 * hPaddingInSelection) / - static_cast(LyraMetrics::values.homeCoverHeight); - float cropX = 1.0f - (tileRatio / ratio); - - renderer.drawBitmap(bitmap, tileX + hPaddingInSelection, tileY + hPaddingInSelection, - tileWidth - 2 * hPaddingInSelection, LyraMetrics::values.homeCoverHeight, cropX); - } - file.close(); + if (!hasCover) { + renderer.drawRect(tileX + hPaddingInSelection, tileY + hPaddingInSelection, + tileWidth - 2 * hPaddingInSelection, LyraMetrics::values.homeCoverHeight); } } From 47f3137dee5b6865d7cddb40ae1c906490fa208b Mon Sep 17 00:00:00 2001 From: Jake Kenneally Date: Fri, 6 Feb 2026 03:10:37 -0500 Subject: [PATCH 05/29] fix: Remove separations after style changes (#720) Closes #182. Closes #710. Closes #711. ## Summary **What is the goal of this PR?** - A longer-term, more robust fix for the issue with spurious spaces appearing after style changes. Replaces solution from #694. **What changes are included?** - Add continuation flags to determine if to add a space after a word or if the word connects to the previous word. Replaces simple solution that only considered ending punctuation. - Fixed an issue with greedy line-breaking algorithm where punctuation could appear on the next line, separated from the word, if there was a style change between the word and punctuation --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**YES**_, Claude Code --- lib/Epub/Epub/ParsedText.cpp | 132 +++++++++--------- lib/Epub/Epub/ParsedText.h | 13 +- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 32 ++++- lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h | 1 + 4 files changed, 108 insertions(+), 70 deletions(-) diff --git a/lib/Epub/Epub/ParsedText.cpp b/lib/Epub/Epub/ParsedText.cpp index 3af85a1b..82ddaecd 100644 --- a/lib/Epub/Epub/ParsedText.cpp +++ b/lib/Epub/Epub/ParsedText.cpp @@ -19,38 +19,6 @@ namespace { constexpr char SOFT_HYPHEN_UTF8[] = "\xC2\xAD"; constexpr size_t SOFT_HYPHEN_BYTES = 2; -// Known attaching punctuation (including UTF-8 sequences) -const std::vector punctuation = { - ".", - ",", - "!", - "?", - ";", - ":", - "\"", - "'", - "\xE2\x80\x99", // ’ (U+2019 right single quote) - "\xE2\x80\x9D" // ” (U+201D right double quote) -}; - -bool isAttachingPunctuationWord(const std::string& word) { - if (word.empty()) return false; - - size_t pos = 0; - while (pos < word.size()) { - bool matched = false; - for (const auto& p : punctuation) { - if (word.compare(pos, p.size(), p) == 0) { - pos += p.size(); - matched = true; - break; - } - } - if (!matched) return false; - } - return true; -} - bool containsSoftHyphen(const std::string& word) { return word.find(SOFT_HYPHEN_UTF8) != std::string::npos; } // Removes every soft hyphen in-place so rendered glyphs match measured widths. @@ -81,15 +49,17 @@ uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const s } // namespace -void ParsedText::addWord(std::string word, const EpdFontFamily::Style style, const bool underline) { +void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, const bool underline, + const bool attachToPrevious) { if (word.empty()) return; words.push_back(std::move(word)); - EpdFontFamily::Style combinedStyle = style; + EpdFontFamily::Style combinedStyle = fontStyle; if (underline) { combinedStyle = static_cast(combinedStyle | EpdFontFamily::UNDERLINE); } wordStyles.push_back(combinedStyle); + wordContinues.push_back(attachToPrevious); } // Consumes data to minimize memory usage @@ -106,17 +76,21 @@ void ParsedText::layoutAndExtractLines(const GfxRenderer& renderer, const int fo const int pageWidth = viewportWidth; const int spaceWidth = renderer.getSpaceWidth(fontId); auto wordWidths = calculateWordWidths(renderer, fontId); + + // Build indexed continues vector from the parallel list for O(1) access during layout + std::vector continuesVec(wordContinues.begin(), wordContinues.end()); + std::vector lineBreakIndices; if (hyphenationEnabled) { // Use greedy layout that can split words mid-loop when a hyphenated prefix fits. - lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths); + lineBreakIndices = computeHyphenatedLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, continuesVec); } else { - lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths); + lineBreakIndices = computeLineBreaks(renderer, fontId, pageWidth, spaceWidth, wordWidths, continuesVec); } const size_t lineCount = includeLastLine ? lineBreakIndices.size() : lineBreakIndices.size() - 1; for (size_t i = 0; i < lineCount; ++i) { - extractLine(i, pageWidth, spaceWidth, wordWidths, lineBreakIndices, processLine); + extractLine(i, pageWidth, spaceWidth, wordWidths, continuesVec, lineBreakIndices, processLine); } } @@ -140,7 +114,8 @@ std::vector ParsedText::calculateWordWidths(const GfxRenderer& rendere } std::vector ParsedText::computeLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth, - const int spaceWidth, std::vector& wordWidths) { + const int spaceWidth, std::vector& wordWidths, + std::vector& continuesVec) { if (words.empty()) { return {}; } @@ -157,7 +132,8 @@ std::vector ParsedText::computeLineBreaks(const GfxRenderer& renderer, c // First word needs to fit in reduced width if there's an indent const int effectiveWidth = i == 0 ? pageWidth - firstLineIndent : pageWidth; while (wordWidths[i] > effectiveWidth) { - if (!hyphenateWordAtIndex(i, effectiveWidth, renderer, fontId, wordWidths, /*allowFallbackBreaks=*/true)) { + if (!hyphenateWordAtIndex(i, effectiveWidth, renderer, fontId, wordWidths, /*allowFallbackBreaks=*/true, + &continuesVec)) { break; } } @@ -175,20 +151,26 @@ std::vector ParsedText::computeLineBreaks(const GfxRenderer& renderer, c ans[totalWordCount - 1] = totalWordCount - 1; for (int i = totalWordCount - 2; i >= 0; --i) { - int currlen = -spaceWidth; + int currlen = 0; dp[i] = MAX_COST; // First line has reduced width due to text-indent const int effectivePageWidth = i == 0 ? pageWidth - firstLineIndent : pageWidth; for (size_t j = i; j < totalWordCount; ++j) { - // Current line length: previous width + space + current word width - currlen += wordWidths[j] + spaceWidth; + // Add space before word j, unless it's the first word on the line or a continuation + const int gap = j > static_cast(i) && !continuesVec[j] ? spaceWidth : 0; + currlen += wordWidths[j] + gap; if (currlen > effectivePageWidth) { break; } + // Cannot break after word j if the next word attaches to it (continuation group) + if (j + 1 < totalWordCount && continuesVec[j + 1]) { + continue; + } + int cost; if (j == totalWordCount - 1) { cost = 0; // Last line @@ -260,7 +242,8 @@ void ParsedText::applyParagraphIndent() { // Builds break indices while opportunistically splitting the word that would overflow the current line. std::vector ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& renderer, const int fontId, const int pageWidth, const int spaceWidth, - std::vector& wordWidths) { + std::vector& wordWidths, + std::vector& continuesVec) { // Calculate first line indent (only for left/justified text without extra paragraph spacing) const int firstLineIndent = blockStyle.textIndent > 0 && !extraParagraphSpacing && @@ -282,7 +265,7 @@ std::vector ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r // Consume as many words as possible for current line, splitting when prefixes fit while (currentIndex < wordWidths.size()) { const bool isFirstWord = currentIndex == lineStart; - const int spacing = isFirstWord ? 0 : spaceWidth; + const int spacing = isFirstWord || continuesVec[currentIndex] ? 0 : spaceWidth; const int candidateWidth = spacing + wordWidths[currentIndex]; // Word fits on current line @@ -296,8 +279,8 @@ std::vector ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r const int availableWidth = effectivePageWidth - lineWidth - spacing; const bool allowFallbackBreaks = isFirstWord; // Only for first word on line - if (availableWidth > 0 && - hyphenateWordAtIndex(currentIndex, availableWidth, renderer, fontId, wordWidths, allowFallbackBreaks)) { + if (availableWidth > 0 && hyphenateWordAtIndex(currentIndex, availableWidth, renderer, fontId, wordWidths, + allowFallbackBreaks, &continuesVec)) { // Prefix now fits; append it to this line and move to next line lineWidth += spacing + wordWidths[currentIndex]; ++currentIndex; @@ -312,6 +295,12 @@ std::vector ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r break; } + // Don't break before a continuation word (e.g., orphaned "?" after "question"). + // Backtrack to the start of the continuation group so the whole group moves to the next line. + while (currentIndex > lineStart + 1 && currentIndex < wordWidths.size() && continuesVec[currentIndex]) { + --currentIndex; + } + lineBreakIndices.push_back(currentIndex); isFirstLine = false; } @@ -323,7 +312,7 @@ std::vector ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r // available width. bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availableWidth, const GfxRenderer& renderer, const int fontId, std::vector& wordWidths, - const bool allowFallbackBreaks) { + const bool allowFallbackBreaks, std::vector* continuesVec) { // Guard against invalid indices or zero available width before attempting to split. if (availableWidth <= 0 || wordIndex >= words.size()) { return false; @@ -378,12 +367,28 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl wordIt->push_back('-'); } - // Insert the remainder word (with matching style) directly after the prefix. + // Insert the remainder word (with matching style and continuation flag) directly after the prefix. auto insertWordIt = std::next(wordIt); auto insertStyleIt = std::next(styleIt); words.insert(insertWordIt, remainder); wordStyles.insert(insertStyleIt, style); + // The remainder inherits whatever continuation status the original word had with the word after it. + // Find the continues entry for the original word and insert the remainder's entry after it. + auto continuesIt = wordContinues.begin(); + std::advance(continuesIt, wordIndex); + const bool originalContinuedToNext = *continuesIt; + // The original word (now prefix) does NOT continue to remainder (hyphen separates them) + *continuesIt = false; + const auto insertContinuesIt = std::next(continuesIt); + wordContinues.insert(insertContinuesIt, originalContinuedToNext); + + // Keep the indexed vector in sync if provided + if (continuesVec) { + (*continuesVec)[wordIndex] = false; + continuesVec->insert(continuesVec->begin() + wordIndex + 1, originalContinuedToNext); + } + // Update cached widths to reflect the new prefix/remainder pairing. wordWidths[wordIndex] = static_cast(chosenWidth); const uint16_t remainderWidth = measureWordWidth(renderer, fontId, remainder, style); @@ -392,7 +397,8 @@ bool ParsedText::hyphenateWordAtIndex(const size_t wordIndex, const int availabl } void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const int spaceWidth, - const std::vector& wordWidths, const std::vector& lineBreakIndices, + const std::vector& wordWidths, const std::vector& continuesVec, + const std::vector& lineBreakIndices, const std::function)>& processLine) { const size_t lineBreak = lineBreakIndices[breakIndex]; const size_t lastBreakAt = breakIndex > 0 ? lineBreakIndices[breakIndex - 1] : 0; @@ -407,19 +413,16 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const : 0; // Calculate total word width for this line and count actual word gaps - // (punctuation that attaches to previous word doesn't count as a gap) - // Note: words list starts at the beginning because previous lines were spliced out + // (continuation words attach to previous word with no gap) int lineWordWidthSum = 0; size_t actualGapCount = 0; - auto countWordIt = words.begin(); for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) { lineWordWidthSum += wordWidths[lastBreakAt + wordIdx]; - // Count gaps: each word after the first creates a gap, unless it's attaching punctuation - if (wordIdx > 0 && !isAttachingPunctuationWord(*countWordIt)) { + // Count gaps: each word after the first creates a gap, unless it's a continuation + if (wordIdx > 0 && !continuesVec[lastBreakAt + wordIdx]) { actualGapCount++; } - ++countWordIt; } // Calculate spacing (account for indent reducing effective page width on first line) @@ -443,30 +446,27 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const } // Pre-calculate X positions for words - // Punctuation that attaches to the previous word doesn't get space before it - // Note: words list starts at the beginning because previous lines were spliced out + // Continuation words attach to the previous word with no space before them std::list lineXPos; - auto wordIt = words.begin(); for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) { const uint16_t currentWordWidth = wordWidths[lastBreakAt + wordIdx]; lineXPos.push_back(xpos); - // Add spacing after this word, unless the next word is attaching punctuation - auto nextWordIt = wordIt; - ++nextWordIt; - const bool nextIsAttachingPunctuation = wordIdx + 1 < lineWordCount && isAttachingPunctuationWord(*nextWordIt); + // Add spacing after this word, unless the next word is a continuation + const bool nextIsContinuation = wordIdx + 1 < lineWordCount && continuesVec[lastBreakAt + wordIdx + 1]; - xpos += currentWordWidth + (nextIsAttachingPunctuation ? 0 : spacing); - ++wordIt; + xpos += currentWordWidth + (nextIsContinuation ? 0 : spacing); } // Iterators always start at the beginning as we are moving content with splice below auto wordEndIt = words.begin(); auto wordStyleEndIt = wordStyles.begin(); + auto wordContinuesEndIt = wordContinues.begin(); std::advance(wordEndIt, lineWordCount); std::advance(wordStyleEndIt, lineWordCount); + std::advance(wordContinuesEndIt, lineWordCount); // *** CRITICAL STEP: CONSUME DATA USING SPLICE *** std::list lineWords; @@ -474,6 +474,10 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const std::list lineWordStyles; lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyles.begin(), wordStyleEndIt); + // Consume continues flags (not passed to TextBlock, but must be consumed to stay in sync) + std::list lineContinues; + lineContinues.splice(lineContinues.begin(), wordContinues, wordContinues.begin(), wordContinuesEndIt); + for (auto& word : lineWords) { if (containsSoftHyphen(word)) { stripSoftHyphensInPlace(word); diff --git a/lib/Epub/Epub/ParsedText.h b/lib/Epub/Epub/ParsedText.h index a13d13b5..8dd040cb 100644 --- a/lib/Epub/Epub/ParsedText.h +++ b/lib/Epub/Epub/ParsedText.h @@ -16,19 +16,22 @@ class GfxRenderer; class ParsedText { std::list words; std::list wordStyles; + std::list wordContinues; // true = word attaches to previous (no space before it) BlockStyle blockStyle; bool extraParagraphSpacing; bool hyphenationEnabled; void applyParagraphIndent(); std::vector computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, int spaceWidth, - std::vector& wordWidths); + std::vector& wordWidths, std::vector& continuesVec); std::vector computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth, - int spaceWidth, std::vector& wordWidths); + int spaceWidth, std::vector& wordWidths, + std::vector& continuesVec); bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId, - std::vector& wordWidths, bool allowFallbackBreaks); + std::vector& wordWidths, bool allowFallbackBreaks, + std::vector* continuesVec = nullptr); void extractLine(size_t breakIndex, int pageWidth, int spaceWidth, const std::vector& wordWidths, - const std::vector& lineBreakIndices, + const std::vector& continuesVec, const std::vector& lineBreakIndices, const std::function)>& processLine); std::vector calculateWordWidths(const GfxRenderer& renderer, int fontId); @@ -38,7 +41,7 @@ class ParsedText { : blockStyle(blockStyle), extraParagraphSpacing(extraParagraphSpacing), hyphenationEnabled(hyphenationEnabled) {} ~ParsedText() = default; - void addWord(std::string word, EpdFontFamily::Style fontStyle, bool underline = false); + void addWord(std::string word, EpdFontFamily::Style fontStyle, bool underline = false, bool attachToPrevious = false); void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; } BlockStyle& getBlockStyle() { return blockStyle; } size_t size() const { return words.size(); } diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index cb5625c1..043cfca7 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -90,12 +90,14 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() { // flush the buffer partWordBuffer[partWordBufferIndex] = '\0'; - currentTextBlock->addWord(partWordBuffer, fontStyle); + currentTextBlock->addWord(partWordBuffer, fontStyle, false, nextWordContinues); partWordBufferIndex = 0; + nextWordContinues = false; } // start a new text block if needed void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) { + nextWordContinues = false; // New block = new paragraph, no continuation if (currentTextBlock) { // already have a text block running and it is empty - just reuse it if (currentTextBlock->isEmpty()) { @@ -241,6 +243,11 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* } } } else if (matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS)) { + // Flush buffer before style change so preceding text gets current style + if (self->partWordBufferIndex > 0) { + self->flushPartWordBuffer(); + self->nextWordContinues = true; + } self->underlineUntilDepth = std::min(self->underlineUntilDepth, self->depth); // Push inline style entry for underline tag StyleStackEntry entry; @@ -258,6 +265,11 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* self->inlineStyleStack.push_back(entry); self->updateEffectiveInlineStyle(); } else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) { + // Flush buffer before style change so preceding text gets current style + if (self->partWordBufferIndex > 0) { + self->flushPartWordBuffer(); + self->nextWordContinues = true; + } self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth); // Push inline style entry for bold tag StyleStackEntry entry; @@ -275,6 +287,11 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* self->inlineStyleStack.push_back(entry); self->updateEffectiveInlineStyle(); } else if (matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS)) { + // Flush buffer before style change so preceding text gets current style + if (self->partWordBufferIndex > 0) { + self->flushPartWordBuffer(); + self->nextWordContinues = true; + } self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth); // Push inline style entry for italic tag StyleStackEntry entry; @@ -294,6 +311,11 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* } else if (strcmp(name, "span") == 0 || !isHeaderOrBlock(name)) { // Handle span and other inline elements for CSS styling if (cssStyle.hasFontWeight() || cssStyle.hasFontStyle() || cssStyle.hasTextDecoration()) { + // Flush buffer before style change so preceding text gets current style + if (self->partWordBufferIndex > 0) { + self->flushPartWordBuffer(); + self->nextWordContinues = true; + } StyleStackEntry entry; entry.depth = self->depth; // Track depth for matching pop if (cssStyle.hasFontWeight()) { @@ -331,6 +353,8 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char if (self->partWordBufferIndex > 0) { self->flushPartWordBuffer(); } + // Whitespace is a real word boundary — reset continuation state + self->nextWordContinues = false; // Skip the whitespace char continue; } @@ -387,6 +411,8 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n // Flush buffer with current style BEFORE any style changes if (self->partWordBufferIndex > 0) { // Flush if style will change OR if we're closing a block/structural element + const bool isInlineTag = !headerOrBlockTag && strcmp(name, "table") != 0 && + !matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS) && self->depth != 1; const bool shouldFlush = styleWillChange || headerOrBlockTag || matches(name, BOLD_TAGS, NUM_BOLD_TAGS) || matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) || matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS) || strcmp(name, "table") == 0 || @@ -394,6 +420,10 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n if (shouldFlush) { self->flushPartWordBuffer(); + // If closing an inline element, the next word fragment continues the same visual word + if (isInlineTag) { + self->nextWordContinues = true; + } } } diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index 261d8f4e..909913b1 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -30,6 +30,7 @@ class ChapterHtmlSlimParser { // leave one char at end for null pointer char partWordBuffer[MAX_WORD_SIZE + 1] = {}; int partWordBufferIndex = 0; + bool nextWordContinues = false; // true when next flushed word attaches to previous (inline element boundary) std::unique_ptr currentTextBlock = nullptr; std::unique_ptr currentPage = nullptr; int16_t currentPageNextY = 0; From 75b0ed778123ae1835e552490c85bb4fc4dae281 Mon Sep 17 00:00:00 2001 From: James Whyte Date: Sat, 7 Feb 2026 15:15:41 +0000 Subject: [PATCH 06/29] fix: increase lyra sideButtonHintsWidth to 30 (#727) ## Summary Increase the width of Lyra's side button hints. It has been set to 30, the same width as the classic theme. Before: image After: image ## Additional Context Resolves https://github.com/crosspoint-reader/crosspoint-reader/pull/700#issuecomment-3856983832 --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? No --- src/components/themes/lyra/LyraTheme.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/themes/lyra/LyraTheme.h b/src/components/themes/lyra/LyraTheme.h index f00758cf..93ec0579 100644 --- a/src/components/themes/lyra/LyraTheme.h +++ b/src/components/themes/lyra/LyraTheme.h @@ -26,7 +26,7 @@ constexpr ThemeMetrics values = {.batteryWidth = 16, .homeCoverTileHeight = 287, .homeRecentBooksCount = 3, .buttonHintsHeight = 40, - .sideButtonHintsWidth = 19, + .sideButtonHintsWidth = 30, .versionTextRightX = 20, .versionTextY = 55, .bookProgressBarHeight = 4}; From b45eaf7dedac84f1b91be2de07d5d1f4ff1c9e52 Mon Sep 17 00:00:00 2001 From: Xuan-Son Nguyen Date: Sun, 8 Feb 2026 12:59:13 +0100 Subject: [PATCH 07/29] feat: optimize fillRectDither (#737) ## Summary This PR optimizes the `fillRectDither` function, making it as fast as a normal `fillRect` Testing code: ```cpp { auto start_t = millis(); renderer.fillRectDither(0, 0, renderer.getScreenWidth(), renderer.getScreenHeight(), Color::LightGray); auto elapsed = millis() - start_t; Serial.printf("[%lu] [ ] Test fillRectDither drawn in %lu ms\n", millis(), elapsed); } { auto start_t = millis(); renderer.fillRect(0, 0, renderer.getScreenWidth(), renderer.getScreenHeight(), true); auto elapsed = millis() - start_t; Serial.printf("[%lu] [ ] Test fillRect drawn in %lu ms\n", millis(), elapsed); } ``` Before: ``` [1125] [ ] Test fillRectDither drawn in 327 ms [1347] [ ] Test fillRect drawn in 222 ms ``` After: ``` [1065] [ ] Test fillRectDither drawn in 238 ms [1287] [ ] Test fillRect drawn in 222 ms ``` ## Visual validation Before: Screenshot 2026-02-07 at 01 04 19 After: Screenshot 2026-02-07 at 01 36 30 ## Details The original version is quite slow because it does quite a lot of computations. A single pixel needs around 20 instructions just to know if it's black or white: Screenshot 2026-02-07 at 00 15 54 With the new, templated and more light-weight approach, each pixel takes only 3-4 instructions, the modulo operator is translated into bitwise ops: Screenshot 2026-02-07 at 01 47 51 --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? **NO** --- lib/GfxRenderer/GfxRenderer.cpp | 97 ++++++++++++++++++++------------- lib/GfxRenderer/GfxRenderer.h | 6 +- 2 files changed, 63 insertions(+), 40 deletions(-) diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 98cc0ee9..8abb16ea 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -237,54 +237,56 @@ void GfxRenderer::fillRect(const int x, const int y, const int width, const int } } -static constexpr uint8_t bayer4x4[4][4] = { - {0, 8, 2, 10}, - {12, 4, 14, 6}, - {3, 11, 1, 9}, - {15, 7, 13, 5}, -}; -static constexpr int matrixSize = 4; -static constexpr int matrixLevels = matrixSize * matrixSize; - -void GfxRenderer::drawPixelDither(const int x, const int y, Color color) const { - if (color == Color::Clear) { - } else if (color == Color::Black) { - drawPixel(x, y, true); - } else if (color == Color::White) { - drawPixel(x, y, false); - } else { - // Use dithering - const int greyLevel = static_cast(color) - 1; // 0-15 - const int normalizedGrey = (greyLevel * 255) / (matrixLevels - 1); - const int clampedGrey = std::max(0, std::min(normalizedGrey, 255)); - const int threshold = (clampedGrey * (matrixLevels + 1)) / 256; - - const int matrixX = x & (matrixSize - 1); - const int matrixY = y & (matrixSize - 1); - const uint8_t patternValue = bayer4x4[matrixY][matrixX]; - const bool black = patternValue < threshold; - drawPixel(x, y, black); - } +// NOTE: Those are in critical path, and need to be templated to avoid runtime checks for every pixel. +// Any branching must be done outside the loops to avoid performance degradation. +template <> +void GfxRenderer::drawPixelDither(const int x, const int y) const { + // Do nothing +} + +template <> +void GfxRenderer::drawPixelDither(const int x, const int y) const { + drawPixel(x, y, true); +} + +template <> +void GfxRenderer::drawPixelDither(const int x, const int y) const { + drawPixel(x, y, false); +} + +template <> +void GfxRenderer::drawPixelDither(const int x, const int y) const { + drawPixel(x, y, x % 2 == 0 && y % 2 == 0); +} + +template <> +void GfxRenderer::drawPixelDither(const int x, const int y) const { + drawPixel(x, y, (x + y) % 2 == 0); // TODO: maybe find a better pattern? } -// Use Bayer matrix 4x4 dithering to fill the rectangle with a grey level void GfxRenderer::fillRectDither(const int x, const int y, const int width, const int height, Color color) const { if (color == Color::Clear) { } else if (color == Color::Black) { fillRect(x, y, width, height, true); } else if (color == Color::White) { fillRect(x, y, width, height, false); - } else { + } else if (color == Color::LightGray) { for (int fillY = y; fillY < y + height; fillY++) { for (int fillX = x; fillX < x + width; fillX++) { - drawPixelDither(fillX, fillY, color); + drawPixelDither(fillX, fillY); + } + } + } else if (color == Color::DarkGray) { + for (int fillY = y; fillY < y + height; fillY++) { + for (int fillX = x; fillX < x + width; fillX++) { + drawPixelDither(fillX, fillY); } } } } -void GfxRenderer::fillArc(const int maxRadius, const int cx, const int cy, const int xDir, const int yDir, - Color color) const { +template +void GfxRenderer::fillArc(const int maxRadius, const int cx, const int cy, const int xDir, const int yDir) const { const int radiusSq = maxRadius * maxRadius; for (int dy = 0; dy <= maxRadius; ++dy) { for (int dx = 0; dx <= maxRadius; ++dx) { @@ -292,7 +294,7 @@ void GfxRenderer::fillArc(const int maxRadius, const int cx, const int cy, const const int px = cx + xDir * dx; const int py = cy + yDir * dy; if (distSq <= radiusSq) { - drawPixelDither(px, py, color); + drawPixelDither(px, py); } } } @@ -327,26 +329,45 @@ void GfxRenderer::fillRoundedRect(const int x, const int y, const int width, con fillRectDither(x + width - maxRadius - 1, y + maxRadius + 1, maxRadius + 1, verticalHeight, color); } + auto fillArcTemplated = [this](int maxRadius, int cx, int cy, int xDir, int yDir, Color color) { + switch (color) { + case Color::Clear: + break; + case Color::Black: + fillArc(maxRadius, cx, cy, xDir, yDir); + break; + case Color::White: + fillArc(maxRadius, cx, cy, xDir, yDir); + break; + case Color::LightGray: + fillArc(maxRadius, cx, cy, xDir, yDir); + break; + case Color::DarkGray: + fillArc(maxRadius, cx, cy, xDir, yDir); + break; + } + }; + if (roundTopLeft) { - fillArc(maxRadius, x + maxRadius, y + maxRadius, -1, -1, color); + fillArcTemplated(maxRadius, x + maxRadius, y + maxRadius, -1, -1, color); } else { fillRectDither(x, y, maxRadius + 1, maxRadius + 1, color); } if (roundTopRight) { - fillArc(maxRadius, x + width - maxRadius - 1, y + maxRadius, 1, -1, color); + fillArcTemplated(maxRadius, x + width - maxRadius - 1, y + maxRadius, 1, -1, color); } else { fillRectDither(x + width - maxRadius - 1, y, maxRadius + 1, maxRadius + 1, color); } if (roundBottomRight) { - fillArc(maxRadius, x + width - maxRadius - 1, y + height - maxRadius - 1, 1, 1, color); + fillArcTemplated(maxRadius, x + width - maxRadius - 1, y + height - maxRadius - 1, 1, 1, color); } else { fillRectDither(x + width - maxRadius - 1, y + height - maxRadius - 1, maxRadius + 1, maxRadius + 1, color); } if (roundBottomLeft) { - fillArc(maxRadius, x + maxRadius, y + height - maxRadius - 1, -1, 1, color); + fillArcTemplated(maxRadius, x + maxRadius, y + height - maxRadius - 1, -1, 1, color); } else { fillRectDither(x, y + height - maxRadius - 1, maxRadius + 1, maxRadius + 1, color); } diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index b84e7993..014349dc 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -39,8 +39,10 @@ class GfxRenderer { EpdFontFamily::Style style) const; void freeBwBufferChunks(); void rotateCoordinates(int x, int y, int* rotatedX, int* rotatedY) const; - void drawPixelDither(int x, int y, Color color) const; - void fillArc(int maxRadius, int cx, int cy, int xDir, int yDir, Color color) const; + template + void drawPixelDither(int x, int y) const; + template + void fillArc(int maxRadius, int cx, int cy, int xDir, int yDir) const; public: explicit GfxRenderer(HalDisplay& halDisplay) From bb983d0ef444d0168d0cb9b95abdbecc60982d46 Mon Sep 17 00:00:00 2001 From: CaptainFrito Date: Mon, 9 Feb 2026 00:58:46 +0700 Subject: [PATCH 08/29] fix: Scrolling page items calculation (#716) ## Summary Fix for the page skip issue detected https://github.com/crosspoint-reader/crosspoint-reader/pull/700#issuecomment-3856374323 by user @whyte-j Skipping down on the last page now skips to the last item, and up on the first page to the first item, rather than wrapping around the list in a weird way. ## Additional Context The calculation was outdated after several changes were added afterwards --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**NO**_ --- src/activities/home/MyLibraryActivity.cpp | 10 ++++++---- src/activities/home/RecentBooksActivity.cpp | 8 +++++--- src/components/UITheme.cpp | 4 ++-- src/components/themes/BaseTheme.cpp | 2 +- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index 35fbd44f..7523e542 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -3,6 +3,8 @@ #include #include +#include + #include "MappedInputManager.h" #include "components/UITheme.h" #include "fontIds.h" @@ -114,7 +116,7 @@ void MyLibraryActivity::loop() { mappedInput.wasReleased(MappedInputManager::Button::Down); const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; - const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, true); + const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false); if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (files.empty()) { @@ -157,14 +159,14 @@ void MyLibraryActivity::loop() { int listSize = static_cast(files.size()); if (upReleased) { if (skipPage) { - selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + listSize) % listSize; + selectorIndex = std::max(static_cast((selectorIndex / pageItems - 1) * pageItems), 0); } else { selectorIndex = (selectorIndex + listSize - 1) % listSize; } updateRequired = true; } else if (downReleased) { if (skipPage) { - selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % listSize; + selectorIndex = std::min(static_cast((selectorIndex / pageItems + 1) * pageItems), listSize - 1); } else { selectorIndex = (selectorIndex + 1) % listSize; } @@ -195,7 +197,7 @@ void MyLibraryActivity::render() const { GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, folderName); const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; - const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; + const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing; if (files.empty()) { renderer.drawText(UI_10_FONT_ID, metrics.contentSidePadding, contentTop + 20, "No books found"); } else { diff --git a/src/activities/home/RecentBooksActivity.cpp b/src/activities/home/RecentBooksActivity.cpp index 9d22ae16..1be0e1e0 100644 --- a/src/activities/home/RecentBooksActivity.cpp +++ b/src/activities/home/RecentBooksActivity.cpp @@ -3,6 +3,8 @@ #include #include +#include + #include "MappedInputManager.h" #include "RecentBooksStore.h" #include "components/UITheme.h" @@ -92,14 +94,14 @@ void RecentBooksActivity::loop() { int listSize = static_cast(recentBooks.size()); if (upReleased) { if (skipPage) { - selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + listSize) % listSize; + selectorIndex = std::max(static_cast((selectorIndex / pageItems - 1) * pageItems), 0); } else { selectorIndex = (selectorIndex + listSize - 1) % listSize; } updateRequired = true; } else if (downReleased) { if (skipPage) { - selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % listSize; + selectorIndex = std::min(static_cast((selectorIndex / pageItems + 1) * pageItems), listSize - 1); } else { selectorIndex = (selectorIndex + 1) % listSize; } @@ -129,7 +131,7 @@ void RecentBooksActivity::render() const { GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, "Recent Books"); const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; - const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing * 2; + const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing; // Recent tab if (recentBooks.empty()) { diff --git a/src/components/UITheme.cpp b/src/components/UITheme.cpp index 45dfefde..9dbe429e 100644 --- a/src/components/UITheme.cpp +++ b/src/components/UITheme.cpp @@ -40,10 +40,10 @@ int UITheme::getNumberOfItemsPerPage(const GfxRenderer& renderer, bool hasHeader const ThemeMetrics& metrics = UITheme::getInstance().getMetrics(); int reservedHeight = metrics.topPadding; if (hasHeader) { - reservedHeight += metrics.headerHeight; + reservedHeight += metrics.headerHeight + metrics.verticalSpacing; } if (hasTabBar) { - reservedHeight += metrics.tabBarHeight + metrics.verticalSpacing; + reservedHeight += metrics.tabBarHeight; } if (hasButtonHints) { reservedHeight += metrics.verticalSpacing + metrics.buttonHintsHeight; diff --git a/src/components/themes/BaseTheme.cpp b/src/components/themes/BaseTheme.cpp index 2e8ddbc8..9253953e 100644 --- a/src/components/themes/BaseTheme.cpp +++ b/src/components/themes/BaseTheme.cpp @@ -174,7 +174,7 @@ void BaseTheme::drawList(const GfxRenderer& renderer, Rect rect, int itemCount, const int centerX = rect.x + rect.width - indicatorWidth / 2 - margin; const int indicatorTop = rect.y; // Offset to avoid overlapping side button hints - const int indicatorBottom = rect.y + rect.height - 30; + const int indicatorBottom = rect.y + rect.height - arrowSize; // Draw up arrow at top (^) - narrow point at top, wide base at bottom for (int i = 0; i < arrowSize; ++i) { From 4f0a3aa4dda56b2a2c96754493d95fcacca7d7ed Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Sun, 8 Feb 2026 21:01:30 +0300 Subject: [PATCH 09/29] feat: wakeup target detection (#731) ## Summary * If going to sleep was from the Reader view, wake up to the same book. Otherwise, wakeup to the Home view --- src/CrossPointState.cpp | 9 ++++++++- src/CrossPointState.h | 1 + src/activities/Activity.h | 1 + src/activities/reader/ReaderActivity.h | 1 + src/main.cpp | 9 ++++++--- 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/CrossPointState.cpp b/src/CrossPointState.cpp index d578945c..d97344f2 100644 --- a/src/CrossPointState.cpp +++ b/src/CrossPointState.cpp @@ -5,7 +5,7 @@ #include namespace { -constexpr uint8_t STATE_FILE_VERSION = 3; +constexpr uint8_t STATE_FILE_VERSION = 4; constexpr char STATE_FILE[] = "/.crosspoint/state.bin"; } // namespace @@ -21,6 +21,7 @@ bool CrossPointState::saveToFile() const { serialization::writeString(outputFile, openEpubPath); serialization::writePod(outputFile, lastSleepImage); serialization::writePod(outputFile, readerActivityLoadCount); + serialization::writePod(outputFile, lastSleepFromReader); outputFile.close(); return true; } @@ -50,6 +51,12 @@ bool CrossPointState::loadFromFile() { serialization::readPod(inputFile, readerActivityLoadCount); } + if (version >= 4) { + serialization::readPod(inputFile, lastSleepFromReader); + } else { + lastSleepFromReader = false; + } + inputFile.close(); return true; } diff --git a/src/CrossPointState.h b/src/CrossPointState.h index e8c65c10..68fa1d68 100644 --- a/src/CrossPointState.h +++ b/src/CrossPointState.h @@ -10,6 +10,7 @@ class CrossPointState { std::string openEpubPath; uint8_t lastSleepImage; uint8_t readerActivityLoadCount = 0; + bool lastSleepFromReader = false; ~CrossPointState() = default; // Get singleton instance diff --git a/src/activities/Activity.h b/src/activities/Activity.h index 4a60607b..632d396d 100644 --- a/src/activities/Activity.h +++ b/src/activities/Activity.h @@ -23,4 +23,5 @@ class Activity { virtual void loop() {} virtual bool skipLoopDelay() { return false; } virtual bool preventAutoSleep() { return false; } + virtual bool isReaderActivity() const { return false; } }; diff --git a/src/activities/reader/ReaderActivity.h b/src/activities/reader/ReaderActivity.h index 6ecd6f34..5a2c1012 100644 --- a/src/activities/reader/ReaderActivity.h +++ b/src/activities/reader/ReaderActivity.h @@ -34,4 +34,5 @@ class ReaderActivity final : public ActivityWithSubactivity { onGoBack(onGoBack), onGoToLibrary(onGoToLibrary) {} void onEnter() override; + bool isReaderActivity() const override { return true; } }; diff --git a/src/main.cpp b/src/main.cpp index e1c74038..f18c47e0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -194,6 +194,8 @@ void waitForPowerRelease() { // Enter deep sleep mode void enterDeepSleep() { + APP_STATE.lastSleepFromReader = currentActivity && currentActivity->isReaderActivity(); + APP_STATE.saveToFile(); exitActivity(); enterNewActivity(new SleepActivity(renderer, mappedInputManager)); @@ -331,9 +333,10 @@ void setup() { APP_STATE.loadFromFile(); RECENT_BOOKS.loadFromFile(); - // Boot to home screen directly when back button is held or when reader activity crashes 3 times - if (APP_STATE.openEpubPath.empty() || mappedInputManager.isPressed(MappedInputManager::Button::Back) || - APP_STATE.readerActivityLoadCount > 0) { + // Boot to home screen if no book is open, last sleep was not from reader, back button is held, or reader activity + // crashed (indicated by readerActivityLoadCount > 0) + if (APP_STATE.openEpubPath.empty() || !APP_STATE.lastSleepFromReader || + mappedInputManager.isPressed(MappedInputManager::Button::Back) || APP_STATE.readerActivityLoadCount > 0) { onGoHome(); } else { // Clear app state to avoid getting into a boot loop if the epub doesn't load From 6909f127b4395764572f4af34ffc18c1293c19c3 Mon Sep 17 00:00:00 2001 From: Xuan-Son Nguyen Date: Sun, 8 Feb 2026 19:05:42 +0100 Subject: [PATCH 10/29] perf: optimize drawPixel() (#748) ## Summary Ref https://github.com/crosspoint-reader/crosspoint-reader/pull/737 This PR further reduce ~25ms from rendering time, testing inside the Setting screen: ``` master: [68440] [GFX] Time = 73 ms from clearScreen to displayBuffer PR: [97806] [GFX] Time = 47 ms from clearScreen to displayBuffer ``` And in extreme case (fill the entire screen with black or gray color): ``` master: [1125] [ ] Test fillRectDither drawn in 327 ms [1347] [ ] Test fillRect drawn in 222 ms PR: [1334] [ ] Test fillRectDither drawn in 225 ms [1455] [ ] Test fillRect drawn in 121 ms ``` Note that https://github.com/crosspoint-reader/crosspoint-reader/pull/737 is NOT applied on top of this PR. But with 2 of them combined, it should reduce from 47ms --> 42ms ## Details This PR based on the fact that function calls are costly if the function is small enough. For example, this simple call: ``` int rotatedX = 0; int rotatedY = 0; rotateCoordinates(x, y, &rotatedX, &rotatedY); ``` Generated assembly code: image This adds ~10 instructions just to prepare the registers prior to the function call, plus some more instructions for the function's epilogue/prologue. Inlining it removing all of these: image Of course, this optimization is not magic. It's only beneficial under 3 conditions: - The function is small, not in size, but in terms of effective instructions. For example, the `rotateCoordinates` is simply a jump table, where each branch is just 3-4 inst - The function has multiple input arguments, which requires some move to put it onto the correct place - The function is called very frequently (i.e. critical path) --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? **NO** --- lib/GfxRenderer/GfxRenderer.cpp | 100 +++++++++++++++----------------- lib/GfxRenderer/GfxRenderer.h | 6 +- src/main.cpp | 1 + 3 files changed, 52 insertions(+), 55 deletions(-) diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 8abb16ea..14024bc4 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -2,61 +2,68 @@ #include +void GfxRenderer::begin() { + frameBuffer = display.getFrameBuffer(); + if (!frameBuffer) { + Serial.printf("[%lu] [GFX] !! No framebuffer\n", millis()); + assert(false); + } +} + void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); } -void GfxRenderer::rotateCoordinates(const int x, const int y, int* rotatedX, int* rotatedY) const { +// Translate logical (x,y) coordinates to physical panel coordinates based on current orientation +// This should always be inlined for better performance +static inline void rotateCoordinates(const GfxRenderer::Orientation orientation, const int x, const int y, int* phyX, + int* phyY) { switch (orientation) { - case Portrait: { + case GfxRenderer::Portrait: { // Logical portrait (480x800) → panel (800x480) // Rotation: 90 degrees clockwise - *rotatedX = y; - *rotatedY = HalDisplay::DISPLAY_HEIGHT - 1 - x; + *phyX = y; + *phyY = HalDisplay::DISPLAY_HEIGHT - 1 - x; break; } - case LandscapeClockwise: { + case GfxRenderer::LandscapeClockwise: { // Logical landscape (800x480) rotated 180 degrees (swap top/bottom and left/right) - *rotatedX = HalDisplay::DISPLAY_WIDTH - 1 - x; - *rotatedY = HalDisplay::DISPLAY_HEIGHT - 1 - y; + *phyX = HalDisplay::DISPLAY_WIDTH - 1 - x; + *phyY = HalDisplay::DISPLAY_HEIGHT - 1 - y; break; } - case PortraitInverted: { + case GfxRenderer::PortraitInverted: { // Logical portrait (480x800) → panel (800x480) // Rotation: 90 degrees counter-clockwise - *rotatedX = HalDisplay::DISPLAY_WIDTH - 1 - y; - *rotatedY = x; + *phyX = HalDisplay::DISPLAY_WIDTH - 1 - y; + *phyY = x; break; } - case LandscapeCounterClockwise: { + case GfxRenderer::LandscapeCounterClockwise: { // Logical landscape (800x480) aligned with panel orientation - *rotatedX = x; - *rotatedY = y; + *phyX = x; + *phyY = y; break; } } } +// IMPORTANT: This function is in critical rendering path and is called for every pixel. Please keep it as simple and +// efficient as possible. void GfxRenderer::drawPixel(const int x, const int y, const bool state) const { - uint8_t* frameBuffer = display.getFrameBuffer(); + int phyX = 0; + int phyY = 0; - // Early return if no framebuffer is set - if (!frameBuffer) { - Serial.printf("[%lu] [GFX] !! No framebuffer\n", millis()); - return; - } - - int rotatedX = 0; - int rotatedY = 0; - rotateCoordinates(x, y, &rotatedX, &rotatedY); + // Note: this call should be inlined for better performance + rotateCoordinates(orientation, x, y, &phyX, &phyY); // Bounds checking against physical panel dimensions - if (rotatedX < 0 || rotatedX >= HalDisplay::DISPLAY_WIDTH || rotatedY < 0 || rotatedY >= HalDisplay::DISPLAY_HEIGHT) { - Serial.printf("[%lu] [GFX] !! Outside range (%d, %d) -> (%d, %d)\n", millis(), x, y, rotatedX, rotatedY); + if (phyX < 0 || phyX >= HalDisplay::DISPLAY_WIDTH || phyY < 0 || phyY >= HalDisplay::DISPLAY_HEIGHT) { + Serial.printf("[%lu] [GFX] !! Outside range (%d, %d) -> (%d, %d)\n", millis(), x, y, phyX, phyY); return; } // Calculate byte position and bit position - const uint16_t byteIndex = rotatedY * HalDisplay::DISPLAY_WIDTH_BYTES + (rotatedX / 8); - const uint8_t bitPosition = 7 - (rotatedX % 8); // MSB first + const uint16_t byteIndex = phyY * HalDisplay::DISPLAY_WIDTH_BYTES + (phyX / 8); + const uint8_t bitPosition = 7 - (phyX % 8); // MSB first if (state) { frameBuffer[byteIndex] &= ~(1 << bitPosition); // Clear bit @@ -376,7 +383,7 @@ void GfxRenderer::fillRoundedRect(const int x, const int y, const int width, con void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const { int rotatedX = 0; int rotatedY = 0; - rotateCoordinates(x, y, &rotatedX, &rotatedY); + rotateCoordinates(orientation, x, y, &rotatedX, &rotatedY); // Rotate origin corner switch (orientation) { case Portrait: @@ -632,20 +639,23 @@ void GfxRenderer::fillPolygon(const int* xPoints, const int* yPoints, int numPoi free(nodeX); } -void GfxRenderer::clearScreen(const uint8_t color) const { display.clearScreen(color); } +// For performance measurement (using static to allow "const" methods) +static unsigned long start_ms = 0; + +void GfxRenderer::clearScreen(const uint8_t color) const { + start_ms = millis(); + display.clearScreen(color); +} void GfxRenderer::invertScreen() const { - uint8_t* buffer = display.getFrameBuffer(); - if (!buffer) { - Serial.printf("[%lu] [GFX] !! No framebuffer in invertScreen\n", millis()); - return; - } for (int i = 0; i < HalDisplay::BUFFER_SIZE; i++) { - buffer[i] = ~buffer[i]; + frameBuffer[i] = ~frameBuffer[i]; } } void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const { + auto elapsed = millis() - start_ms; + Serial.printf("[%lu] [GFX] Time = %lu ms from clearScreen to displayBuffer\n", millis(), elapsed); display.displayBuffer(refreshMode, fadingFix); } @@ -829,16 +839,16 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y } } -uint8_t* GfxRenderer::getFrameBuffer() const { return display.getFrameBuffer(); } +uint8_t* GfxRenderer::getFrameBuffer() const { return frameBuffer; } size_t GfxRenderer::getBufferSize() { return HalDisplay::BUFFER_SIZE; } // unused // void GfxRenderer::grayscaleRevert() const { display.grayscaleRevert(); } -void GfxRenderer::copyGrayscaleLsbBuffers() const { display.copyGrayscaleLsbBuffers(display.getFrameBuffer()); } +void GfxRenderer::copyGrayscaleLsbBuffers() const { display.copyGrayscaleLsbBuffers(frameBuffer); } -void GfxRenderer::copyGrayscaleMsbBuffers() const { display.copyGrayscaleMsbBuffers(display.getFrameBuffer()); } +void GfxRenderer::copyGrayscaleMsbBuffers() const { display.copyGrayscaleMsbBuffers(frameBuffer); } void GfxRenderer::displayGrayBuffer() const { display.displayGrayBuffer(fadingFix); } @@ -858,12 +868,6 @@ void GfxRenderer::freeBwBufferChunks() { * Returns true if buffer was stored successfully, false if allocation failed. */ bool GfxRenderer::storeBwBuffer() { - const uint8_t* frameBuffer = display.getFrameBuffer(); - if (!frameBuffer) { - Serial.printf("[%lu] [GFX] !! No framebuffer in storeBwBuffer\n", millis()); - return false; - } - // Allocate and copy each chunk for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) { // Check if any chunks are already allocated @@ -913,13 +917,6 @@ void GfxRenderer::restoreBwBuffer() { return; } - uint8_t* frameBuffer = display.getFrameBuffer(); - if (!frameBuffer) { - Serial.printf("[%lu] [GFX] !! No framebuffer in restoreBwBuffer\n", millis()); - freeBwBufferChunks(); - return; - } - for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) { // Check if chunk is missing if (!bwBufferChunks[i]) { @@ -943,7 +940,6 @@ void GfxRenderer::restoreBwBuffer() { * Use this when BW buffer was re-rendered instead of stored/restored. */ void GfxRenderer::cleanupGrayscaleWithFrameBuffer() const { - uint8_t* frameBuffer = display.getFrameBuffer(); if (frameBuffer) { display.cleanupGrayscaleBuffers(frameBuffer); } diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index 014349dc..4540774e 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -33,12 +33,12 @@ class GfxRenderer { RenderMode renderMode; Orientation orientation; bool fadingFix; + uint8_t* frameBuffer = nullptr; uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr}; std::map fontMap; void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState, EpdFontFamily::Style style) const; void freeBwBufferChunks(); - void rotateCoordinates(int x, int y, int* rotatedX, int* rotatedY) const; template void drawPixelDither(int x, int y) const; template @@ -55,6 +55,7 @@ class GfxRenderer { static constexpr int VIEWABLE_MARGIN_LEFT = 3; // Setup + void begin(); // must be called right after display.begin() void insertFont(int fontId, EpdFontFamily font); // Orientation control (affects logical width/height and coordinate transforms) @@ -72,6 +73,7 @@ class GfxRenderer { // void displayWindow(int x, int y, int width, int height) const; void invertScreen() const; void clearScreen(uint8_t color = 0xFF) const; + void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const; // Drawing void drawPixel(int x, int y, bool state = true) const; @@ -125,6 +127,4 @@ class GfxRenderer { // Low level functions uint8_t* getFrameBuffer() const; static size_t getBufferSize(); - void grayscaleRevert() const; - void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const; }; diff --git a/src/main.cpp b/src/main.cpp index f18c47e0..4895be38 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -253,6 +253,7 @@ void onGoHome() { void setupDisplayAndFonts() { display.begin(); + renderer.begin(); Serial.printf("[%lu] [ ] Display initialized\n", millis()); renderer.insertFont(BOOKERLY_14_FONT_ID, bookerly14FontFamily); #ifndef OMIT_FONTS From 5e52a4683719bc6f2ec8e3615a83b2905e4d15c5 Mon Sep 17 00:00:00 2001 From: Jake Kenneally Date: Sun, 8 Feb 2026 12:34:06 -0500 Subject: [PATCH 11/29] refactor: Rename "Embedded Style" to "Book's Embedded Style" (#746) ## Summary **What is the goal of this PR?** - Just a simple rename after feedback in #738 **What changes are included?** - Renamed "Embedded Style" to "Book's Embedded Style" to more clearly associate it with "Book's Style" option in "Paragraph Alignment" settings --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**NO**_ --- src/activities/settings/SettingsActivity.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index efe5b5ed..7a8fc261 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -43,7 +43,7 @@ const SettingInfo readerSettings[readerSettingsCount] = { SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}), SettingInfo::Enum("Paragraph Alignment", &CrossPointSettings::paragraphAlignment, {"Justify", "Left", "Center", "Right", "Book's Style"}), - SettingInfo::Toggle("Embedded Style", &CrossPointSettings::embeddedStyle), + SettingInfo::Toggle("Book's Embedded Style", &CrossPointSettings::embeddedStyle), SettingInfo::Toggle("Hyphenation", &CrossPointSettings::hyphenationEnabled), SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation, {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}), From 21e7d29286a81c58d749e1ac37b6f807879b46ab Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Mon, 9 Feb 2026 08:08:19 +1100 Subject: [PATCH 12/29] fix: Allow OTA update from RC build to full release (#778) ## Summary * Allow OTA update from RC build to full release * If all the segments match, then also check if the current version contains "-rc" --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? No --- src/network/OtaUpdater.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/network/OtaUpdater.cpp b/src/network/OtaUpdater.cpp index 1733e136..3c2dda8a 100644 --- a/src/network/OtaUpdater.cpp +++ b/src/network/OtaUpdater.cpp @@ -185,7 +185,16 @@ bool OtaUpdater::isUpdateNewer() const { /* * Check patch versions. */ - return latestPatch > currentPatch; + if (latestPatch != currentPatch) return latestPatch > currentPatch; + + // If we reach here, it means all segments are equal. + // One final check, if we're on an RC build (contains "-rc"), we should consider the latest version as newer even if + // the segments are equal, since RC builds are pre-release versions. + if (strstr(currentVersion, "-rc") != nullptr) { + return true; + } + + return false; } const std::string& OtaUpdater::getLatestVersion() const { return latestVersion; } From 7538e557959260f2f8a5a171bc92a70ac8d116fc Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Mon, 9 Feb 2026 08:15:13 +1100 Subject: [PATCH 13/29] Move release candidate workflow to manual dispatch --- .github/workflows/release_candidate.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release_candidate.yml b/.github/workflows/release_candidate.yml index e9bbc636..4df20d02 100644 --- a/.github/workflows/release_candidate.yml +++ b/.github/workflows/release_candidate.yml @@ -1,11 +1,11 @@ name: Compile Release Candidate on: - pull_request: + workflow_dispatch: jobs: build-release-candidate: - if: startsWith(github.head_ref, 'release/') + if: startsWith(github.ref_name, 'release/') runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 From def1094411f32c5323c486f9c09eb791a441963e Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Mon, 9 Feb 2026 08:22:20 +1100 Subject: [PATCH 14/29] Use GITHUB_REF_NAME over GITHUB_HEAD_REF in release candidate workflow --- .github/workflows/release_candidate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release_candidate.yml b/.github/workflows/release_candidate.yml index 4df20d02..d247fa78 100644 --- a/.github/workflows/release_candidate.yml +++ b/.github/workflows/release_candidate.yml @@ -29,7 +29,7 @@ jobs: - name: Extract env run: | echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV - echo "BRANCH_SUFFIX=${GITHUB_HEAD_REF#release/}" >> $GITHUB_ENV + echo "BRANCH_SUFFIX=${GITHUB_REF_NAME#release/}" >> $GITHUB_ENV - name: Build CrossPoint Release Candidate env: From 9e04eec072b1db251cca6dc570b619c3e39b5c36 Mon Sep 17 00:00:00 2001 From: Jake Kenneally Date: Sun, 8 Feb 2026 16:31:52 -0500 Subject: [PATCH 15/29] feat: Add percentage support to CSS properties (#738) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Closes #730 **What is the goal of this PR?** - Adds percentage-based value support to CSS properties that accept percentages (padding, margin, text-indent) **What changes are included?** - Adds `Percent` as another CSS unit - Passes the viewport width to `fromCssStyle` so that we can resolve percentage-based values - Adds a fallback of using an emspace for text-indent if we have an unresolvable value for whatever reason ## Additional Context - This was missed in my CSS support feature, and the fallback when we encounter a percentage value is to use px instead. This means 5% (which would be ~30px on the screen) turns into 5px. When percentages are used in `text-indent`, this fallback behavior makes the indent look like a single space character. Whoops! 😬 My test EPUB has been updated [here](https://github.com/jdk2pq/css-test-epub) with percentage based CSS values at the end of the book. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**YES**_, Claude Code --- lib/Epub/Epub/blocks/BlockStyle.h | 30 +++++++++++-------- lib/Epub/Epub/css/CssParser.cpp | 4 ++- lib/Epub/Epub/css/CssStyle.h | 17 +++++++++-- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 6 ++-- 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/lib/Epub/Epub/blocks/BlockStyle.h b/lib/Epub/Epub/blocks/BlockStyle.h index 63b054c9..a5a616bf 100644 --- a/lib/Epub/Epub/blocks/BlockStyle.h +++ b/lib/Epub/Epub/blocks/BlockStyle.h @@ -64,21 +64,27 @@ struct BlockStyle { // Create a BlockStyle from CSS style properties, resolving CssLength values to pixels // emSize is the current font line height, used for em/rem unit conversion // paragraphAlignment is the user's paragraphAlignment setting preference - static BlockStyle fromCssStyle(const CssStyle& cssStyle, const float emSize, const CssTextAlign paragraphAlignment) { + static BlockStyle fromCssStyle(const CssStyle& cssStyle, const float emSize, const CssTextAlign paragraphAlignment, + const uint16_t viewportWidth = 0) { BlockStyle blockStyle; - // Resolve all CssLength values to pixels using the current font's em size - blockStyle.marginTop = cssStyle.marginTop.toPixelsInt16(emSize); - blockStyle.marginBottom = cssStyle.marginBottom.toPixelsInt16(emSize); - blockStyle.marginLeft = cssStyle.marginLeft.toPixelsInt16(emSize); - blockStyle.marginRight = cssStyle.marginRight.toPixelsInt16(emSize); + const float vw = viewportWidth; + // Resolve all CssLength values to pixels using the current font's em size and viewport width + blockStyle.marginTop = cssStyle.marginTop.toPixelsInt16(emSize, vw); + blockStyle.marginBottom = cssStyle.marginBottom.toPixelsInt16(emSize, vw); + blockStyle.marginLeft = cssStyle.marginLeft.toPixelsInt16(emSize, vw); + blockStyle.marginRight = cssStyle.marginRight.toPixelsInt16(emSize, vw); - blockStyle.paddingTop = cssStyle.paddingTop.toPixelsInt16(emSize); - blockStyle.paddingBottom = cssStyle.paddingBottom.toPixelsInt16(emSize); - blockStyle.paddingLeft = cssStyle.paddingLeft.toPixelsInt16(emSize); - blockStyle.paddingRight = cssStyle.paddingRight.toPixelsInt16(emSize); + blockStyle.paddingTop = cssStyle.paddingTop.toPixelsInt16(emSize, vw); + blockStyle.paddingBottom = cssStyle.paddingBottom.toPixelsInt16(emSize, vw); + blockStyle.paddingLeft = cssStyle.paddingLeft.toPixelsInt16(emSize, vw); + blockStyle.paddingRight = cssStyle.paddingRight.toPixelsInt16(emSize, vw); - blockStyle.textIndent = cssStyle.textIndent.toPixelsInt16(emSize); - blockStyle.textIndentDefined = cssStyle.hasTextIndent(); + // For textIndent: if it's a percentage we can't resolve (no viewport width), + // leave textIndentDefined=false so the EmSpace fallback in applyParagraphIndent() is used + if (cssStyle.hasTextIndent() && cssStyle.textIndent.isResolvable(vw)) { + blockStyle.textIndent = cssStyle.textIndent.toPixelsInt16(emSize, vw); + blockStyle.textIndentDefined = true; + } blockStyle.textAlignDefined = cssStyle.hasTextAlign(); // User setting overrides CSS, unless "Book's Style" alignment setting is selected if (paragraphAlignment == CssTextAlign::None) { diff --git a/lib/Epub/Epub/css/CssParser.cpp b/lib/Epub/Epub/css/CssParser.cpp index d51ebba7..ba187e4c 100644 --- a/lib/Epub/Epub/css/CssParser.cpp +++ b/lib/Epub/Epub/css/CssParser.cpp @@ -283,6 +283,8 @@ CssLength CssParser::interpretLength(const std::string& val) { unit = CssUnit::Rem; } else if (unitPart == "pt") { unit = CssUnit::Points; + } else if (unitPart == "%") { + unit = CssUnit::Percent; } // px and unitless default to Pixels @@ -518,7 +520,7 @@ CssStyle CssParser::parseInlineStyle(const std::string& styleValue) { return par // Cache serialization // Cache format version - increment when format changes -constexpr uint8_t CSS_CACHE_VERSION = 1; +constexpr uint8_t CSS_CACHE_VERSION = 2; bool CssParser::saveToCache(FsFile& file) const { if (!file) { diff --git a/lib/Epub/Epub/css/CssStyle.h b/lib/Epub/Epub/css/CssStyle.h index adbc19e2..b90fa7ab 100644 --- a/lib/Epub/Epub/css/CssStyle.h +++ b/lib/Epub/Epub/css/CssStyle.h @@ -4,7 +4,7 @@ // Matches order of PARAGRAPH_ALIGNMENT in CrossPointSettings enum class CssTextAlign : uint8_t { Justify = 0, Left = 1, Center = 2, Right = 3, None = 4 }; -enum class CssUnit : uint8_t { Pixels = 0, Em = 1, Rem = 2, Points = 3 }; +enum class CssUnit : uint8_t { Pixels = 0, Em = 1, Rem = 2, Points = 3, Percent = 4 }; // Represents a CSS length value with its unit, allowing deferred resolution to pixels struct CssLength { @@ -17,21 +17,32 @@ struct CssLength { // Convenience constructor for pixel values (most common case) explicit CssLength(const float pixels) : value(pixels) {} + // Returns true if this length can be resolved to pixels with the given context. + // Percentage units require a non-zero containerWidth to resolve. + [[nodiscard]] bool isResolvable(const float containerWidth = 0) const { + return unit != CssUnit::Percent || containerWidth > 0; + } + // Resolve to pixels given the current em size (font line height) - [[nodiscard]] float toPixels(const float emSize) const { + // containerWidth is needed for percentage units (e.g. viewport width) + [[nodiscard]] float toPixels(const float emSize, const float containerWidth = 0) const { switch (unit) { case CssUnit::Em: case CssUnit::Rem: return value * emSize; case CssUnit::Points: return value * 1.33f; // Approximate pt to px conversion + case CssUnit::Percent: + return value * containerWidth / 100.0f; default: return value; } } // Resolve to int16_t pixels (for BlockStyle fields) - [[nodiscard]] int16_t toPixelsInt16(const float emSize) const { return static_cast(toPixels(emSize)); } + [[nodiscard]] int16_t toPixelsInt16(const float emSize, const float containerWidth = 0) const { + return static_cast(toPixels(emSize, containerWidth)); + } }; // Font style options matching CSS font-style property diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 043cfca7..a4d10832 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -213,12 +213,12 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* } const float emSize = static_cast(self->renderer.getLineHeight(self->fontId)) * self->lineCompression; - const auto userAlignmentBlockStyle = - BlockStyle::fromCssStyle(cssStyle, emSize, static_cast(self->paragraphAlignment)); + const auto userAlignmentBlockStyle = BlockStyle::fromCssStyle( + cssStyle, emSize, static_cast(self->paragraphAlignment), self->viewportWidth); if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) { self->currentCssStyle = cssStyle; - auto headerBlockStyle = BlockStyle::fromCssStyle(cssStyle, emSize, CssTextAlign::Center); + auto headerBlockStyle = BlockStyle::fromCssStyle(cssStyle, emSize, CssTextAlign::Center, self->viewportWidth); headerBlockStyle.textAlignDefined = true; if (self->embeddedStyle && cssStyle.hasTextAlign()) { headerBlockStyle.alignment = cssStyle.textAlign; From e73bb3213ff5ae1b4f0e8e695fcc4e18415d6e09 Mon Sep 17 00:00:00 2001 From: Fabio Barbon Date: Mon, 9 Feb 2026 09:55:58 +0100 Subject: [PATCH 16/29] feat: Add Italian hyphenation support (#584) ## Summary * **What is the goal of this PR?** Add Italian language hyphenation support to improve text rendering for Italian books. * **What changes are included?** * Added Italian hyphenation trie (hyph-it.trie.h) generated from Typst's hypher patterns * Registered italianHyphenator in LanguageRegistry.cpp for language tag it * Added Italian to the hyphenation evaluation test suite * Added Italian test data file with 5000 test cases ## Additional Context * Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks, specific areas to focus on). --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**NO**_ --------- Co-authored-by: drbourbon --- bin/clang-format-fix | 2 + .../Epub/hyphenation/LanguageRegistry.cpp | 7 +- .../Epub/hyphenation/generated/hyph-it.trie.h | 113 + .../HyphenationEvaluationTest.cpp | 1 + .../resources/italian_hyphenation_tests.txt | 5012 +++++++++++++++++ 5 files changed, 5133 insertions(+), 2 deletions(-) create mode 100644 lib/Epub/Epub/hyphenation/generated/hyph-it.trie.h create mode 100644 test/hyphenation_eval/resources/italian_hyphenation_tests.txt diff --git a/bin/clang-format-fix b/bin/clang-format-fix index 4339ea36..206cb217 100755 --- a/bin/clang-format-fix +++ b/bin/clang-format-fix @@ -13,7 +13,9 @@ fi # --modified: files tracked by git that have been modified (staged or unstaged) # --exclude-standard: ignores files in .gitignore # Additionally exclude files in 'lib/EpdFont/builtinFonts/' as they are script-generated. +# Also exclude files in 'lib/Epub/Epub/hyphenation/generated/' as they are script-generated. git ls-files --exclude-standard ${GIT_LS_FILES_FLAGS} \ | grep -E '\.(c|cpp|h|hpp)$' \ | grep -v -E '^lib/EpdFont/builtinFonts/' \ + | grep -v -E '^lib/Epub/Epub/hyphenation/generated/' \ | xargs -r clang-format -style=file -i diff --git a/lib/Epub/Epub/hyphenation/LanguageRegistry.cpp b/lib/Epub/Epub/hyphenation/LanguageRegistry.cpp index 5efd76bb..c36ea64e 100644 --- a/lib/Epub/Epub/hyphenation/LanguageRegistry.cpp +++ b/lib/Epub/Epub/hyphenation/LanguageRegistry.cpp @@ -8,6 +8,7 @@ #include "generated/hyph-en.trie.h" #include "generated/hyph-es.trie.h" #include "generated/hyph-fr.trie.h" +#include "generated/hyph-it.trie.h" #include "generated/hyph-ru.trie.h" namespace { @@ -18,15 +19,17 @@ LanguageHyphenator frenchHyphenator(fr_patterns, isLatinLetter, toLowerLatin); LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin); LanguageHyphenator russianHyphenator(ru_ru_patterns, isCyrillicLetter, toLowerCyrillic); LanguageHyphenator spanishHyphenator(es_patterns, isLatinLetter, toLowerLatin); +LanguageHyphenator italianHyphenator(it_patterns, isLatinLetter, toLowerLatin); -using EntryArray = std::array; +using EntryArray = std::array; const EntryArray& entries() { static const EntryArray kEntries = {{{"english", "en", &englishHyphenator}, {"french", "fr", &frenchHyphenator}, {"german", "de", &germanHyphenator}, {"russian", "ru", &russianHyphenator}, - {"spanish", "es", &spanishHyphenator}}}; + {"spanish", "es", &spanishHyphenator}, + {"italian", "it", &italianHyphenator}}}; return kEntries; } diff --git a/lib/Epub/Epub/hyphenation/generated/hyph-it.trie.h b/lib/Epub/Epub/hyphenation/generated/hyph-it.trie.h new file mode 100644 index 00000000..22f4d044 --- /dev/null +++ b/lib/Epub/Epub/hyphenation/generated/hyph-it.trie.h @@ -0,0 +1,113 @@ +#pragma once + +#include +#include + +#include "../SerializedHyphenationTrie.h" + +// Auto-generated by generate_hyphenation_trie.py. Do not edit manually. +alignas(4) constexpr uint8_t it_trie_data[] = { + 0x00, 0x00, 0x05, 0xC4, 0x17, 0x0C, 0x33, 0x35, 0x0C, 0x29, 0x22, 0x0D, 0x3E, 0x0B, 0x47, 0x20, + 0x0D, 0x16, 0x0B, 0x34, 0x0D, 0x21, 0x0C, 0x3D, 0x1F, 0x0C, 0x2A, 0x17, 0x2A, 0x0B, 0x02, 0x0C, + 0x01, 0x02, 0x16, 0x02, 0x0D, 0x0C, 0x0C, 0x0D, 0x03, 0x0C, 0x01, 0x0C, 0x0E, 0x0D, 0x04, 0x02, + 0x0B, 0xA0, 0x00, 0x42, 0x21, 0x6E, 0xFD, 0xA0, 0x00, 0x72, 0x21, 0x6E, 0xFD, 0xA1, 0x00, 0x61, + 0x6D, 0xFD, 0x21, 0x69, 0xFB, 0x21, 0x74, 0xFD, 0x22, 0x70, 0x6E, 0xEC, 0xFD, 0xA0, 0x00, 0x91, + 0x21, 0x6F, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x00, 0xA2, 0x21, 0x73, 0xFD, 0x21, 0x70, 0xFD, 0xA0, + 0x00, 0xC2, 0x21, 0x6D, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x72, 0xFD, 0xA0, 0x00, + 0xE1, 0x21, 0x6F, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0xA3, 0x01, 0x11, + 0x61, 0x69, 0x6F, 0xDF, 0xEE, 0xFD, 0xA0, 0x00, 0xF2, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, + 0x69, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0xA1, 0x01, 0x11, 0x69, 0xFD, 0xA0, 0x01, 0x12, + 0x21, 0x75, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x78, 0xFD, 0xA0, 0x01, 0x32, 0x21, 0x6B, 0xFD, 0x21, + 0x6E, 0xFD, 0xA0, 0x00, 0x71, 0x21, 0x65, 0xFD, 0x22, 0x61, 0x65, 0xF7, 0xFD, 0x21, 0x72, 0xFB, + 0xA0, 0x01, 0x52, 0x21, 0x61, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x70, 0xFD, 0x21, 0x69, 0xFD, 0xA0, + 0x01, 0x71, 0x21, 0x6F, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x61, 0xFD, 0xA0, 0x00, + 0x61, 0x21, 0x6F, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x70, 0xFF, 0x50, 0x21, 0x6F, 0xFC, 0x21, 0x74, + 0xFD, 0x22, 0x70, 0x72, 0xF3, 0xFD, 0x21, 0x61, 0xE8, 0x21, 0x72, 0xFD, 0xA0, 0x00, 0xF1, 0x22, + 0x6C, 0x72, 0xFD, 0xFD, 0x21, 0x69, 0xE3, 0x21, 0x6C, 0xFD, 0x41, 0x65, 0xFF, 0x43, 0xA0, 0x01, + 0x11, 0x25, 0x61, 0x68, 0x6F, 0x72, 0x73, 0xE8, 0xEE, 0xF6, 0xF9, 0xFD, 0xA0, 0x01, 0x82, 0x21, + 0x72, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x65, 0xFD, 0xA0, 0x01, + 0xA2, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x61, 0xFD, 0x41, 0x75, 0xFF, 0x4C, 0x42, 0x6C, + 0x72, 0xFF, 0xFC, 0xFF, 0x48, 0x21, 0x62, 0xF9, 0x22, 0x68, 0x75, 0xEF, 0xFD, 0x47, 0x63, 0x64, + 0x6C, 0x6E, 0x70, 0x72, 0x74, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, + 0x5C, 0xFF, 0x5C, 0x21, 0x73, 0xEA, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0xA1, 0x01, 0x11, 0x72, + 0xFD, 0x41, 0x6E, 0xFF, 0x15, 0x21, 0x67, 0xFC, 0xA0, 0x01, 0xC2, 0x21, 0x74, 0xFD, 0x21, 0x6C, + 0xFD, 0x22, 0x61, 0x65, 0xF4, 0xFD, 0x52, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x6C, 0x6E, 0x6F, + 0x70, 0x72, 0x73, 0x74, 0x77, 0x68, 0x6A, 0x6B, 0x7A, 0xFE, 0xC2, 0xFE, 0xCD, 0xFE, 0xF7, 0xFF, + 0x12, 0xFF, 0x20, 0xFF, 0x37, 0xFF, 0x46, 0xFF, 0x55, 0xFF, 0x6B, 0xFF, 0x8B, 0xFF, 0xA5, 0xFF, + 0xC2, 0xFF, 0xE6, 0xFF, 0xFB, 0xFF, 0x88, 0xFF, 0x88, 0xFF, 0x88, 0xFF, 0x88, 0xA0, 0x01, 0xE2, + 0xA0, 0x00, 0xD1, 0x24, 0x61, 0x65, 0x6F, 0x75, 0xFD, 0xFD, 0xFD, 0xFD, 0x21, 0x6F, 0xF4, 0x21, + 0x61, 0xF1, 0xA0, 0x01, 0xE1, 0x21, 0x2E, 0xFD, 0x24, 0x69, 0x75, 0x79, 0x74, 0xEB, 0xF4, 0xF7, + 0xFD, 0x21, 0x75, 0xDF, 0xA0, 0x00, 0x51, 0x22, 0x69, 0x77, 0xFA, 0xFD, 0x21, 0x69, 0xD7, 0xAE, + 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x76, 0x6C, 0x72, 0x2E, 0x27, + 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xF5, 0xF5, 0xE3, 0xE3, 0x22, 0x2E, + 0x27, 0xC4, 0xC7, 0xC6, 0x00, 0x51, 0x68, 0x2E, 0x27, 0x62, 0x72, 0x6E, 0xFF, 0xBF, 0xFF, 0xBF, + 0xFF, 0xFB, 0xFF, 0xBF, 0xFE, 0xFB, 0xFF, 0xBF, 0xD0, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x6B, + 0x6D, 0x6E, 0x71, 0x73, 0x74, 0x7A, 0x68, 0x6C, 0x72, 0x2E, 0x27, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, + 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, + 0xAA, 0xFF, 0xEB, 0xFF, 0xBC, 0xFF, 0xBC, 0xFF, 0xAA, 0xFF, 0xAA, 0xCE, 0x02, 0x01, 0x62, 0x64, + 0x67, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x2E, 0x27, 0xFF, 0x77, 0xFF, 0x77, + 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x89, 0xFF, 0x77, 0xFF, 0x77, + 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xCA, 0x02, 0x01, 0x62, 0x67, 0x66, 0x6E, 0x6C, + 0x72, 0x73, 0x74, 0x2E, 0x27, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x5C, 0xFF, + 0x5C, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xA0, 0x02, 0x12, 0xA1, 0x00, 0x51, 0x74, + 0xFD, 0xD1, 0x02, 0x01, 0x62, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, + 0x76, 0x77, 0x7A, 0x2E, 0x27, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0xFB, 0xFF, + 0x33, 0xFF, 0x21, 0xFF, 0x33, 0xFF, 0x21, 0xFF, 0x33, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, + 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0x41, 0x70, 0xFD, 0x4D, 0xCB, 0x02, 0x01, 0x62, 0x64, + 0x68, 0x69, 0x6C, 0x6D, 0x6E, 0x72, 0x76, 0x2E, 0x27, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFF, + 0xFC, 0xFE, 0xF9, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xC2, + 0x02, 0x01, 0x2E, 0x27, 0xFE, 0xC3, 0xFE, 0xC3, 0xCB, 0x02, 0x01, 0x67, 0x66, 0x68, 0x6B, 0x6C, + 0x6D, 0x72, 0x73, 0x74, 0x2E, 0x27, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xCC, 0xFE, 0xBA, 0xFE, 0xCC, + 0xFE, 0xBA, 0xFE, 0xCC, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xBA, 0xA0, 0x02, 0x33, 0x42, + 0x2E, 0x27, 0xFE, 0x93, 0xFE, 0x93, 0xD5, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, + 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x7A, 0x2E, 0x27, 0xFE, 0x8C, + 0xFE, 0x8C, 0xFE, 0x8C, 0xFF, 0xF6, 0xFE, 0x8C, 0xFE, 0x9E, 0xFE, 0x9E, 0xFE, 0x8C, 0xFE, 0x8C, + 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, + 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFF, 0xF9, 0xCF, 0x02, 0x01, 0x62, 0x63, 0x66, 0x6C, 0x6D, + 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x2E, 0x27, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, + 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, + 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xA0, 0x02, 0x62, 0xA1, 0x01, 0xE1, 0x6E, 0xFD, + 0x21, 0x72, 0xF8, 0x21, 0x65, 0xFD, 0xA1, 0x01, 0xE1, 0x66, 0xFD, 0x41, 0x74, 0xFE, 0x07, 0x21, + 0x69, 0xFC, 0x21, 0x65, 0xFD, 0xD3, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6B, 0x6C, 0x6D, + 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x7A, 0x68, 0x2E, 0x27, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, + 0xFD, 0xFD, 0xFD, 0xFF, 0xE6, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, + 0xFD, 0xFD, 0xFD, 0xFF, 0xF1, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFF, 0xFD, 0xFD, 0xFD, 0xFD, + 0xFD, 0xA0, 0x02, 0x82, 0xA1, 0x01, 0xE1, 0x65, 0xFD, 0x21, 0x63, 0xF8, 0xA1, 0x01, 0xE1, 0x69, + 0xFD, 0xCB, 0x02, 0x01, 0x64, 0x68, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x7A, 0x2E, 0x27, 0xFD, + 0xB1, 0xFD, 0xC3, 0xFD, 0xC3, 0xFF, 0xF3, 0xFD, 0xB1, 0xFD, 0xC3, 0xFF, 0xFB, 0xFD, 0xB1, 0xFD, + 0xB1, 0xFD, 0xB1, 0xFD, 0xB1, 0xC3, 0x02, 0x01, 0x71, 0x2E, 0x27, 0xFD, 0x8D, 0xFD, 0x8D, 0xFD, + 0x8D, 0xA0, 0x02, 0x53, 0xA1, 0x01, 0xE1, 0x73, 0xFD, 0xD5, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, + 0x68, 0x67, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x78, 0x77, 0x7A, 0x2E, + 0x27, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x8B, 0xFD, 0x79, 0xFD, 0x79, 0xFD, + 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFF, 0xFB, 0xFD, + 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0x43, 0x6D, 0x2E, 0x27, 0xFD, + 0x37, 0xFD, 0x37, 0xFD, 0x37, 0xA0, 0x02, 0xC2, 0xA1, 0x02, 0x32, 0x6D, 0xFD, 0x41, 0x6E, 0xFE, + 0x8F, 0x4B, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x76, 0xFD, 0x21, 0xFD, + 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, + 0x21, 0xFD, 0x21, 0xA0, 0x02, 0xE1, 0x22, 0x2E, 0x27, 0xFD, 0xFD, 0xC7, 0x02, 0xA2, 0x68, 0x73, + 0x70, 0x74, 0x7A, 0x2E, 0x27, 0xFF, 0xC0, 0xFF, 0xCD, 0xFF, 0xD2, 0xFF, 0xD6, 0xFC, 0xF7, 0xFF, + 0xF8, 0xFF, 0xFB, 0xC1, 0x00, 0x51, 0x2E, 0xFC, 0xDF, 0x41, 0x68, 0xFF, 0x18, 0xA1, 0x00, 0x51, + 0x63, 0xFC, 0xC1, 0x01, 0xE1, 0x73, 0xFE, 0xB6, 0xC2, 0x00, 0x51, 0x6B, 0x73, 0xFC, 0xCA, 0xFC, + 0x06, 0xD2, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, + 0x74, 0x76, 0x77, 0x7A, 0x2E, 0x27, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, + 0xFF, 0xE2, 0xFC, 0xD3, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xD3, 0xFF, 0xEC, 0xFF, 0xF1, + 0xFC, 0xC1, 0xFC, 0xC1, 0xFF, 0xF7, 0xFC, 0xC1, 0xFE, 0x2E, 0xC6, 0x02, 0x01, 0x63, 0x6C, 0x72, + 0x76, 0x2E, 0x27, 0xFC, 0x88, 0xFC, 0x9A, 0xFC, 0x9A, 0xFC, 0x88, 0xFC, 0x88, 0xFD, 0xF5, 0x41, + 0x72, 0xFB, 0xAF, 0xA0, 0x02, 0xF2, 0xC5, 0x02, 0x01, 0x68, 0x61, 0x79, 0x2E, 0x27, 0xFC, 0x7E, + 0xFF, 0xF9, 0xFF, 0xFD, 0xFC, 0x6C, 0xFC, 0x6C, 0xCA, 0x02, 0x01, 0x62, 0x63, 0x66, 0x68, 0x6D, + 0x70, 0x74, 0x77, 0x2E, 0x27, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, + 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0x42, 0x6F, 0x69, 0xFC, 0x48, 0xFC, 0x27, + 0xCB, 0x02, 0x01, 0x62, 0x64, 0x6C, 0x6E, 0x70, 0x74, 0x73, 0x76, 0x7A, 0x2E, 0x27, 0xFC, 0x32, + 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, + 0xFC, 0x32, 0xFD, 0x9F, 0x5A, 0x2E, 0x27, 0x61, 0x65, 0x6F, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, + 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0xFB, + 0xC2, 0xFB, 0xF9, 0xFC, 0x14, 0xFC, 0x23, 0xFC, 0x28, 0xFC, 0x2B, 0xFC, 0x64, 0xFC, 0x97, 0xFC, + 0xC4, 0xFC, 0xED, 0xFD, 0x27, 0xFD, 0x4B, 0xFD, 0x54, 0xFD, 0x82, 0xFD, 0xC4, 0xFE, 0x11, 0xFE, + 0x5D, 0xFE, 0x81, 0xFE, 0x95, 0xFF, 0x17, 0xFF, 0x4D, 0xFF, 0x86, 0xFF, 0xA2, 0xFF, 0xB4, 0xFF, + 0xD5, 0xFF, 0xDC, +}; + +constexpr SerializedHyphenationPatterns it_patterns = { + it_trie_data, + sizeof(it_trie_data), +}; diff --git a/test/hyphenation_eval/HyphenationEvaluationTest.cpp b/test/hyphenation_eval/HyphenationEvaluationTest.cpp index e01b647f..a56467a9 100644 --- a/test/hyphenation_eval/HyphenationEvaluationTest.cpp +++ b/test/hyphenation_eval/HyphenationEvaluationTest.cpp @@ -43,6 +43,7 @@ const std::vector kSupportedLanguages = { {"german", "test/hyphenation_eval/resources/german_hyphenation_tests.txt", "de"}, {"russian", "test/hyphenation_eval/resources/russian_hyphenation_tests.txt", "ru"}, {"spanish", "test/hyphenation_eval/resources/spanish_hyphenation_tests.txt", "es"}, + {"italian", "test/hyphenation_eval/resources/italian_hyphenation_tests.txt", "it"}, }; std::vector expectedPositionsFromAnnotatedWord(const std::string& annotated) { diff --git a/test/hyphenation_eval/resources/italian_hyphenation_tests.txt b/test/hyphenation_eval/resources/italian_hyphenation_tests.txt new file mode 100644 index 00000000..8291728f --- /dev/null +++ b/test/hyphenation_eval/resources/italian_hyphenation_tests.txt @@ -0,0 +1,5012 @@ +# Hyphenation Test Data +# Source: manzoni_i_promessi_sposi.epub +# Language: it_IT +# Min prefix: 2 +# Min suffix: 2 +# Total words: 5000 +# Format: word | hyphenated_form | frequency_in_source +# +# Hyphenation points are marked with '=' +# Example: Silbentrennung -> Sil=ben=tren=nung +# + +quella|quel=la|746 +questo|que=sto|510 +qualche|qual=che|456 +quando|quan=do|392 +questa|que=sta|378 +perché|per=ché|369 +sempre|sem=pre|315 +quello|quel=lo|283 +subito|su=bi=to|280 +quelle|quel=le|265 +strada|stra=da|260 +parole|pa=ro=le|244 +giorno|gior=no|240 +Abbondio|Ab=bon=dio|234 +momento|mo=men=to|226 +Agnese|Agne=se|219 +quelli|quel=li|204 +sarebbe|sa=reb=be|204 +allora|al=lo=ra|191 +quanto|quan=to|191 +poteva|po=te=va|190 +ancora|an=co=ra|187 +insieme|in=sie=me|185 +essere|es=se=re|178 +rispose|ri=spo=se|176 +avrebbe|avreb=be|173 +avesse|aves=se|171 +Rodrigo|Ro=dri=go|170 +vedere|ve=de=re|162 +Milano|Mi=la=no|157 +nessuno|nes=su=no|152 +signor|si=gnor|149 +troppo|trop=po|147 +bisogno|bi=so=gno|143 +giovine|gio=vi=ne|143 +stesso|stes=so|138 +Cristoforo|Cri=sto=fo=ro|136 +meglio|me=glio|136 +faceva|fa=ce=va|133 +avanti|avan=ti|127 +queste|que=ste|127 +andare|an=da=re|123 +maniera|ma=nie=ra|123 +proprio|pro=prio|123 +diceva|di=ce=va|122 +questi|que=sti|121 +andava|an=da=va|120 +dietro|die=tro|119 +signore|si=gno=re|119 +pensiero|pen=sie=ro|117 +potuto|po=tu=to|116 +curato|cu=ra=to|113 +davanti|da=van=ti|110 +povero|po=ve=ro|110 +presto|pre=sto|110 +faccia|fac=cia|109 +storia|sto=ria|108 +veniva|ve=ni=va|107 +indietro|in=die=tro|105 +potesse|po=tes=se|105 +uomini|uo=mi=ni|105 +intorno|in=tor=no|104 +fretta|fret=ta|102 +avevan|ave=van|101 +Gertrude|Ger=tru=de|99 +padrone|pa=dro=ne|99 +voluto|vo=lu=to|97 +doveva|do=ve=va|96 +cardinale|car=di=na=le|95 +contro|con=tro|95 +dentro|den=tro|94 +povera|po=ve=ra|94 +innominato|in=no=mi=na=to|93 +intanto|in=tan=to|93 +parola|pa=ro=la|92 +giustizia|giu=sti=zia|91 +lontano|lon=ta=no|91 +Signore|Si=gno=re|91 +Perpetua|Per=pe=tua|90 +vicino|vi=ci=no|90 +voglio|vo=glio|88 +voleva|vo=le=va|88 +appena|ap=pe=na|85 +ragione|ra=gio=ne|85 +dunque|dun=que|83 +nostro|no=stro|83 +secondo|se=con=do|82 +addosso|ad=dos=so|81 +parlare|par=la=re|81 +riprese|ri=pre=se|81 +carità|ca=ri=tà|80 +pareva|pa=re=va|80 +bisogna|bi=so=gna|78 +venire|ve=ni=re|77 +almeno|al=me=no|76 +fossero|fos=se=ro|76 +volete|vo=le=te|76 +pensare|pen=sa=re|75 +dicendo|di=cen=do|74 +qualcosa|qual=co=sa|74 +sapeva|sa=pe=va|74 +voglia|vo=glia|74 +alcuni|al=cu=ni|73 +pensieri|pen=sie=ri|72 +Quando|Quan=do|72 +chiesa|chie=sa|71 +sentire|sen=ti=re|71 +sapere|sa=pe=re|70 +signori|si=gno=ri|70 +abbiam|ab=biam|69 +appunto|ap=pun=to|69 +vostra|vo=stra|69 +dovere|do=ve=re|68 +convento|con=ven=to|67 +Federigo|Fe=de=ri=go|67 +giorni|gior=ni|67 +quattro|quat=tro|67 +ordine|or=di=ne|66 +sentito|sen=ti=to|66 +compagnia|com=pa=gnia|65 +coraggio|co=rag=gio|65 +esclamò|escla=mò|65 +Ferrer|Fer=rer|65 +finalmente|fi=nal=men=te|65 +sentiva|sen=ti=va|65 +tratto|trat=to|65 +persone|per=so=ne|64 +giacché|giac=ché|63 +niente|nien=te|63 +occhio|oc=chio|62 +stessa|stes=sa|62 +venuto|ve=nu=to|62 +persona|per=so=na|61 +risposta|ri=spo=sta|61 +signora|si=gno=ra|60 +trovava|tro=va=va|60 +vedeva|ve=de=va|60 +carrozza|car=roz=za|59 +diavolo|dia=vo=lo|59 +ognuno|ognu=no|59 +principe|prin=ci=pe|59 +sapete|sa=pe=te|58 +braccia|brac=cia|57 +castello|ca=stel=lo|57 +cominciò|co=min=ciò|57 +occasione|oc=ca=sio=ne|57 +coloro|co=lo=ro|56 +galantuomo|ga=lan=tuo=mo|56 +mentre|men=tre|56 +pericolo|pe=ri=co=lo|56 +poveri|po=ve=ri|56 +vostro|vo=stro|56 +avevano|ave=va=no|55 +peggio|peg=gio|55 +qualcheduno|qual=che=du=no|55 +tavola|ta=vo=la|55 +trovar|tro=var|55 +lazzeretto|laz=ze=ret=to|54 +prender|pren=der|54 +vecchia|vec=chia|54 +domandò|do=man=dò|53 +andato|an=da=to|52 +figliuoli|fi=gliuo=li|52 +figliuolo|fi=gliuo=lo|52 +parere|pa=re=re|52 +passare|pas=sa=re|52 +ragioni|ra=gio=ni|52 +sentimento|sen=ti=men=to|52 +sicuro|si=cu=ro|52 +spesso|spes=so|52 +trovato|tro=va=to|52 +braccio|brac=cio|51 +grazia|gra=zia|51 +qualunque|qua=lun=que|51 +stanza|stan=za|51 +affatto|af=fat=to|50 +lettera|let=te=ra|50 +monastero|mo=na=ste=ro|50 +pensato|pen=sa=to|50 +discorso|di=scor=so|49 +facesse|fa=ces=se|49 +famiglia|fa=mi=glia|49 +innanzi|in=nan=zi|49 +pensava|pen=sa=va|49 +promessi|pro=mes=si|49 +destra|de=stra|48 +dovuto|do=vu=to|48 +potrebbe|po=treb=be|48 +sinistra|si=ni=stra|48 +volesse|vo=les=se|48 +davvero|dav=ve=ro|47 +effetto|ef=fet=to|47 +vicario|vi=ca=rio|47 +chiaro|chia=ro|46 +continuò|con=ti=nuò|46 +costui|co=stui|46 +lettore|let=to=re|46 +metter|met=ter|46 +occhiata|oc=chia=ta|46 +sguardo|sguar=do|46 +silenzio|si=len=zio|46 +spalle|spal=le|46 +Alessandro|Ales=san=dro|45 +discorsi|di=scor=si|45 +giudizio|giu=di=zio|45 +momenti|mo=men=ti|45 +nostri|no=stri|45 +sospetto|so=spet=to|45 +trovarsi|tro=var=si|45 +verità|ve=ri=tà|45 +comune|co=mu=ne|44 +figlia|fi=glia|44 +piuttosto|piut=to=sto|44 +servizio|ser=vi=zio|44 +teneva|te=ne=va|44 +vedete|ve=de=te|44 +veduto|ve=du=to|44 +affare|af=fa=re|43 +autorità|au=to=ri=tà|43 +Manzoni|Man=zo=ni|43 +parlato|par=la=to|43 +prendere|pren=de=re|43 +presso|pres=so|43 +strade|stra=de|43 +viaggio|viag=gio|43 +vossignoria|vos=si=gno=ria|43 +circostanze|cir=co=stan=ze|42 +Perché|Per=ché|42 +quegli|que=gli|42 +soggiunse|sog=giun=se|42 +speranza|spe=ran=za|42 +terrore|ter=ro=re|42 +Attilio|At=ti=lio|41 +dottore|dot=to=re|41 +grande|gran=de|41 +moglie|mo=glie|41 +piacere|pia=ce=re|41 +principio|prin=ci=pio|41 +soltanto|sol=tan=to|41 +tornare|tor=na=re|41 +aspetto|aspet=to|40 +entrare|en=tra=re|40 +generale|ge=ne=ra=le|40 +mattina|mat=ti=na|40 +medesimo|me=de=si=mo|40 +orecchio|orec=chio|40 +trovare|tro=va=re|40 +volontà|vo=lon=tà|40 +alzando|al=zan=do|39 +fargli|far=gli|39 +orecchi|orec=chi|39 +parlar|par=lar|39 +sedere|se=de=re|39 +solito|so=li=to|39 +abbastanza|ab=ba=stan=za|38 +andate|an=da=te|38 +Capitolo|Ca=pi=to=lo|38 +danari|da=na=ri|38 +dovesse|do=ves=se|38 +guerra|guer=ra|38 +Questo|Que=sto|38 +rispetto|ri=spet=to|38 +certamente|cer=ta=men=te|37 +facevan|fa=ce=van|37 +finestra|fi=ne=stra|37 +guardiano|guar=dia=no|37 +mangiare|man=gia=re|37 +pubblico|pub=bli=co|37 +abbiamo|ab=bia=mo|36 +compassione|com=pas=sio=ne|36 +contento|con=ten=to|36 +facendo|fa=cen=do|36 +furono|fu=ro=no|36 +illustrissima|il=lu=stris=si=ma|36 +maraviglia|ma=ra=vi=glia|36 +nessun|nes=sun|36 +pazienza|pa=zien=za|36 +podestà|po=de=stà|36 +popolo|po=po=lo|36 +domanda|do=man=da|35 +mettere|met=te=re|35 +misericordia|mi=se=ri=cor=dia|35 +monatti|mo=nat=ti|35 +passato|pas=sa=to|35 +rumore|ru=mo=re|35 +sentir|sen=tir|35 +cercare|cer=ca=re|34 +contagio|con=ta=gio|34 +diritto|di=rit=to|34 +Intanto|In=tan=to|34 +interruppe|in=ter=rup=pe|34 +lasciato|la=scia=to|34 +numero|nu=me=ro|34 +ordini|or=di=ni|34 +quantunque|quan=tun=que|34 +rimase|ri=ma=se|34 +terribile|ter=ri=bi=le|34 +tribunale|tri=bu=na=le|34 +venisse|ve=nis=se|34 +alcune|al=cu=ne|33 +consolazione|con=so=la=zio=ne|33 +credere|cre=de=re|33 +guardando|guar=dan=do|33 +guardava|guar=da=va|33 +intendere|in=ten=de=re|33 +luoghi|luo=ghi|33 +mestiere|me=stie=re|33 +particolare|par=ti=co=la=re|33 +passar|pas=sar|33 +sangue|san=gue|33 +saputo|sa=pu=to|33 +segreto|se=gre=to|33 +sentite|sen=ti=te|33 +talvolta|tal=vol=ta|33 +avviso|av=vi=so|32 +domani|do=ma=ni|32 +impresa|im=pre=sa|32 +incontro|in=con=tro|32 +nostra|no=stra|32 +simili|si=mi=li|32 +soldati|sol=da=ti|32 +specie|spe=cie|32 +timore|ti=mo=re|32 +vecchio|vec=chio|32 +vedendo|ve=den=do|32 +avvenire|av=ve=ni=re|31 +cagione|ca=gio=ne|31 +cappuccini|cap=puc=ci=ni|31 +continuo|con=ti=nuo|31 +costoro|co=sto=ro|31 +fratello|fra=tel=lo|31 +giornata|gior=na=ta|31 +giusto|giu=sto|31 +guardò|guar=dò|31 +intenzione|in=ten=zio=ne|31 +lasciò|la=sciò|31 +memoria|me=mo=ria|31 +neppure|nep=pu=re|31 +osteria|oste=ria|31 +partito|par=ti=to|31 +perdono|per=do=no|31 +proposito|pro=po=si=to|31 +render|ren=der|31 +rimaneva|ri=ma=ne=va|31 +risoluzione|ri=so=lu=zio=ne|31 +rispondere|ri=spon=de=re|31 +ultimo|ul=ti=mo|31 +adagio|ada=gio|30 +condizione|con=di=zio=ne|30 +cugino|cu=gi=no|30 +desiderio|de=si=de=rio|30 +fronte|fron=te|30 +governatore|go=ver=na=to=re|30 +guarda|guar=da|30 +lasciar|la=sciar|30 +mettersi|met=ter=si|30 +moltitudine|mol=ti=tu=di=ne|30 +morire|mo=ri=re|30 +notaio|no=ta=io|30 +parenti|pa=ren=ti|30 +piccola|pic=co=la|30 +portato|por=ta=to|30 +Questa|Que=sta|30 +veramente|ve=ra=men=te|30 +abbiate|ab=bia=te|29 +andata|an=da=ta|29 +avendo|aven=do|29 +birboni|bir=bo=ni|29 +cappuccino|cap=puc=ci=no|29 +confuso|con=fu=so|29 +fatica|fa=ti=ca|29 +ultima|ul=ti=ma|29 +alquanto|al=quan=to|28 +aperta|aper=ta|28 +bisognava|bi=so=gna=va|28 +camera|ca=me=ra|28 +contegno|con=te=gno|28 +esempio|esem=pio|28 +facevano|fa=ce=va=no|28 +pensate|pen=sa=te|28 +possibile|pos=si=bi=le|28 +protezione|pro=te=zio=ne|28 +Ripamonti|Ri=pa=mon=ti|28 +sicurezza|si=cu=rez=za|28 +spavento|spa=ven=to|28 +vivere|vi=ve=re|28 +altrove|al=tro=ve|27 +andasse|an=das=se|27 +andavano|an=da=va=no|27 +arrivare|ar=ri=va=re|27 +avessero|aves=se=ro|27 +compagno|com=pa=gno|27 +essersi|es=ser=si|27 +fecero|fe=ce=ro|27 +guardia|guar=dia|27 +Lodovico|Lo=do=vi=co|27 +matrimonio|ma=tri=mo=nio|27 +monaca|mo=na=ca|27 +monsignore|mon=si=gno=re|27 +passava|pas=sa=va|27 +piazza|piaz=za|27 +principalmente|prin=ci=pal=men=te|27 +promessa|pro=mes=sa|27 +quante|quan=te|27 +stette|stet=te|27 +venivano|ve=ni=va=no|27 +venuta|ve=nu=ta|27 +antica|an=ti=ca|26 +arrivato|ar=ri=va=to|26 +arrivò|ar=ri=vò|26 +conoscere|co=no=sce=re|26 +difficoltà|dif=fi=col=tà|26 +facile|fa=ci=le|26 +Ferrante|Fer=ran=te|26 +giovani|gio=va=ni|26 +guardare|guar=da=re|26 +inteso|in=te=so|26 +metteva|met=te=va|26 +parlava|par=la=va|26 +potevano|po=te=va=no|26 +rimasto|ri=ma=sto|26 +rispondeva|ri=spon=de=va|26 +simile|si=mi=le|26 +specialmente|spe=cial=men=te|26 +studio|stu=dio|26 +Tadino|Ta=di=no|26 +visita|vi=si=ta|26 +vorrei|vor=rei|26 +altrui|al=trui|25 +antico|an=ti=co|25 +avrebbero|avreb=be=ro|25 +cercar|cer=car|25 +chiamare|chia=ma=re|25 +Dunque|Dun=que|25 +galantuomini|ga=lan=tuo=mi=ni|25 +genere|ge=ne=re|25 +lasciando|la=scian=do|25 +lontana|lon=ta=na|25 +medesima|me=de=si=ma|25 +naturale|na=tu=ra=le|25 +neppur|nep=pur|25 +notizia|no=ti=zia|25 +presenza|pre=sen=za|25 +proposta|pro=po=sta|25 +quindi|quin=di|25 +soggetto|sog=get=to|25 +tuttavia|tut=ta=via|25 +affari|af=fa=ri|24 +andiamo|an=dia=mo|24 +arriva|ar=ri=va|24 +Bortolo|Bor=to=lo|24 +capitano|ca=pi=ta=no|24 +cappello|cap=pel=lo|24 +cercava|cer=ca=va|24 +cortile|cor=ti=le|24 +curiosità|cu=rio=si=tà|24 +dargli|dar=gli|24 +disegno|di=se=gno|24 +dolore|do=lo=re|24 +domande|do=man=de|24 +figura|fi=gu=ra|24 +gridando|gri=dan=do|24 +lacrime|la=cri=me|24 +memorie|me=mo=rie|24 +Mentre|Men=tre|24 +nascosto|na=sco=sto|24 +nemico|ne=mi=co|24 +nessuna|nes=su=na|24 +potete|po=te=te|24 +Prassede|Pras=se=de|24 +prigione|pri=gio=ne|24 +promesse|pro=mes=se|24 +promesso|pro=mes=so|24 +pronto|pron=to|24 +sarebbero|sa=reb=be=ro|24 +stavano|sta=va=no|24 +tristo|tri=sto|24 +vostri|vo=stri|24 +accanto|ac=can=to|23 +andando|an=dan=do|23 +avessi|aves=si|23 +Bergamo|Ber=ga=mo|23 +capelli|ca=pel=li|23 +compagni|com=pa=gni|23 +confidenza|con=fi=den=za|23 +confusione|con=fu=sio=ne|23 +creduto|cre=du=to|23 +dubbio|dub=bio|23 +facilmente|fa=cil=men=te|23 +Gonzalo|Gon=za=lo|23 +labbra|lab=bra|23 +Madonna|Ma=don=na|23 +necessità|ne=ces=si=tà|23 +personaggio|per=so=nag=gio|23 +piccolo|pic=co=lo|23 +potente|po=ten=te|23 +presente|pre=sen=te|23 +provinciale|pro=vin=cia=le|23 +rabbia|rab=bia|23 +riguardo|ri=guar=do|23 +rimasti|ri=ma=sti|23 +servitori|ser=vi=to=ri|23 +solenne|so=len=ne|23 +spettacolo|spet=ta=co=lo|23 +volentieri|vo=len=tie=ri|23 +Allora|Al=lo=ra|22 +aspettare|aspet=ta=re|22 +bottega|bot=te=ga|22 +cavaliere|ca=va=lie=re|22 +comando|co=man=do|22 +dicono|di=co=no|22 +dormire|dor=mi=re|22 +entrar|en=trar|22 +fiducia|fi=du=cia|22 +immagini|im=ma=gi=ni|22 +maniere|ma=nie=re|22 +necessario|ne=ces=sa=rio|22 +notizie|no=ti=zie|22 +portar|por=tar|22 +possiamo|pos=sia=mo|22 +poverina|po=ve=ri=na|22 +prendeva|pren=de=va|22 +raccontar|rac=con=tar|22 +seguito|se=gui=to|22 +sentimenti|sen=ti=men=ti|22 +singolare|sin=go=la=re|22 +soglia|so=glia|22 +trovata|tro=va=ta|22 +uscire|usci=re|22 +venite|ve=ni=te|22 +vergogna|ver=go=gna|22 +abbondanza|ab=bon=dan=za|21 +amicizia|ami=ci=zia|21 +autore|au=to=re|21 +benedetto|be=ne=det=to|21 +brigata|bri=ga=ta|21 +cammino|cam=mi=no|21 +campagna|cam=pa=gna|21 +capire|ca=pi=re|21 +cercando|cer=can=do|21 +condotta|con=dot=ta|21 +essendo|es=sen=do|21 +farebbe|fa=reb=be|21 +grazie|gra=zie|21 +immagine|im=ma=gi=ne|21 +importanza|im=por=tan=za|21 +infatti|in=fat=ti|21 +lasciata|la=scia=ta|21 +milanese|mi=la=ne=se|21 +Nibbio|Nib=bio|21 +pensando|pen=san=do|21 +poverino|po=ve=ri=no|21 +pubblica|pub=bli=ca|21 +Quello|Quel=lo|21 +raccontare|rac=con=ta=re|21 +ragazzo|ra=gaz=zo|21 +rimedio|ri=me=dio|21 +scrivere|scri=ve=re|21 +soccorso|soc=cor=so|21 +strano|stra=no|21 +tornava|tor=na=va|21 +vedova|ve=do=va|21 +vicenda|vi=cen=da|21 +vicina|vi=ci=na|21 +aspettando|aspet=tan=do|20 +benché|ben=ché|20 +birbone|bir=bo=ne|20 +capanna|ca=pan=na|20 +cavallo|ca=val=lo|20 +chiamava|chia=ma=va|20 +chieder|chie=der|20 +cominciava|co=min=cia=va|20 +consiglio|con=si=glio|20 +contenta|con=ten=ta|20 +costei|co=stei|20 +fermarsi|fer=mar=si|20 +gliene|glie=ne|20 +impegno|im=pe=gno|20 +lavoro|la=vo=ro|20 +maggior|mag=gior|20 +mettendo|met=ten=do|20 +motivo|mo=ti=vo|20 +passata|pas=sa=ta|20 +portava|por=ta=va|20 +Quella|Quel=la|20 +sanità|sa=ni=tà|20 +Sapete|Sa=pe=te|20 +sappia|sap=pia|20 +seguente|se=guen=te|20 +servitore|ser=vi=to=re|20 +solamente|so=la=men=te|20 +stesse|stes=se|20 +titolo|ti=to=lo|20 +tumulto|tu=mul=to|20 +ugualmente|ugual=men=te|20 +accade|ac=ca=de|19 +arcivescovo|ar=ci=ve=sco=vo|19 +badessa|ba=des=sa|19 +benissimo|be=nis=si=mo|19 +chiunque|chiun=que|19 +desinare|de=si=na=re|19 +dinanzi|di=nan=zi|19 +disegni|di=se=gni|19 +disgrazia|di=sgra=zia|19 +Ebbene|Eb=be=ne|19 +esercito|eser=ci=to|19 +Finalmente|Fi=nal=men=te|19 +improvviso|im=prov=vi=so|19 +lasciava|la=scia=va|19 +marito|ma=ri=to|19 +passione|pas=sio=ne|19 +pianto|pian=to|19 +preghiera|pre=ghie=ra|19 +prezzo|prez=zo|19 +principale|prin=ci=pa=le|19 +professione|pro=fes=sio=ne|19 +ribrezzo|ri=brez=zo|19 +sconosciuto|sco=no=sciu=to|19 +sottosopra|sot=to=so=pra|19 +sottovoce|sot=to=vo=ce|19 +stessi|stes=si|19 +tornato|tor=na=to|19 +tratta|trat=ta|19 +trista|tri=sta|19 +venivan|ve=ni=van|19 +vennero|ven=ne=ro|19 +andarsene|an=dar=se=ne|18 +andati|an=da=ti|18 +andavan|an=da=van|18 +anonimo|ano=ni=mo|18 +aperto|aper=to|18 +attenzione|at=ten=zio=ne|18 +Bisogna|Bi=so=gna|18 +cervello|cer=vel=lo|18 +creatura|crea=tu=ra|18 +dispiacere|di=spia=ce=re|18 +divenuto|di=ve=nu=to|18 +diverso|di=ver=so|18 +dovette|do=vet=te|18 +finestre|fi=ne=stre|18 +fornai|for=nai|18 +guardar|guar=dar|18 +illustrissimo|il=lu=stris=si=mo|18 +impiccio|im=pic=cio|18 +indizio|in=di=zio|18 +intento|in=ten=to|18 +mandato|man=da=to|18 +materia|ma=te=ria|18 +monache|mo=na=che|18 +movimento|mo=vi=men=to|18 +natura|na=tu=ra|18 +ognuna|ognu=na|18 +ordinario|or=di=na=rio|18 +palazzo|pa=laz=zo|18 +pericoli|pe=ri=co=li|18 +Pescarenico|Pe=sca=re=ni=co|18 +possono|pos=so=no|18 +premura|pre=mu=ra|18 +probabilmente|pro=ba=bil=men=te|18 +processione|pro=ces=sio=ne|18 +qualità|qua=li=tà|18 +quartiere|quar=tie=re|18 +racconto|rac=con=to|18 +ricovero|ri=co=ve=ro|18 +rimasta|ri=ma=sta|18 +ripiego|ri=pie=go|18 +risoluto|ri=so=lu=to|18 +Sanità|Sa=ni=tà|18 +scelta|scel=ta|18 +seconda|se=con=da|18 +sperare|spe=ra=re|18 +tenere|te=ne=re|18 +tenerezza|te=ne=rez=za|18 +tenuto|te=nu=to|18 +terreno|ter=re=no|18 +territorio|ter=ri=to=rio|18 +usciva|usci=va|18 +vengono|ven=go=no|18 +Volete|Vo=le=te|18 +voltandosi|vol=tan=do=si|18 +accaduto|ac=ca=du=to|17 +Antonio|An=to=nio|17 +attento|at=ten=to|17 +averne|aver=ne|17 +cappellano|cap=pel=la=no|17 +cardinal|car=di=nal|17 +carestia|ca=re=stia|17 +circostanza|cir=co=stan=za|17 +contadini|con=ta=di=ni|17 +crescendo|cre=scen=do|17 +distante|di=stan=te|17 +diversi|di=ver=si|17 +dolorosa|do=lo=ro=sa|17 +faccende|fac=cen=de|17 +fantasia|fan=ta=sia|17 +finire|fi=ni=re|17 +fracasso|fra=cas=so|17 +grandi|gran=di|17 +gridava|gri=da=va|17 +lascia|la=scia|17 +latino|la=ti=no|17 +lettiga|let=ti=ga|17 +libertà|li=ber=tà|17 +medici|me=di=ci|17 +Menico|Me=ni=co|17 +miglia|mi=glia|17 +naturalmente|na=tu=ral=men=te|17 +opinione|opi=nio=ne|17 +orrore|or=ro=re|17 +paglia|pa=glia|17 +portata|por=ta=ta|17 +pronti|pron=ti|17 +raccontò|rac=con=tò|17 +replicò|re=pli=cò|17 +secolo|se=co=lo|17 +sentendo|sen=ten=do|17 +spedizione|spe=di=zio=ne|17 +Tramaglino|Tra=ma=gli=no|17 +trovarono|tro=va=ro=no|17 +vedevano|ve=de=va=no|17 +vicini|vi=ci=ni|17 +accordo|ac=cor=do|16 +accostò|ac=co=stò|16 +Appena|Ap=pe=na|16 +aspettava|aspet=ta=va|16 +averlo|aver=lo|16 +bicchiere|bic=chie=re|16 +cadaveri|ca=da=ve=ri|16 +carico|ca=ri=co|16 +chiama|chia=ma|16 +chiamò|chia=mò|16 +chiese|chie=se|16 +codesto|co=de=sto|16 +comodo|co=mo=do|16 +condurre|con=dur=re|16 +conosco|co=no=sco|16 +decurioni|de=cu=rio=ni|16 +desiderato|de=si=de=ra=to|16 +difficile|dif=fi=ci=le|16 +effetti|ef=fet=ti|16 +finito|fi=ni=to|16 +freddo|fred=do|16 +governo|go=ver=no|16 +gridare|gri=da=re|16 +incertezza|in=cer=tez=za|16 +innocente|in=no=cen=te|16 +Italia|Ita=lia|16 +lasciati|la=scia=ti|16 +mercante|mer=can=te|16 +miseria|mi=se=ria|16 +nemici|ne=mi=ci|16 +nipote|ni=po=te|16 +padroni|pa=dro=ni|16 +passioni|pas=sio=ni|16 +patire|pa=ti=re|16 +personaggi|per=so=nag=gi|16 +potevan|po=te=van|16 +pregare|pre=ga=re|16 +proseguì|pro=se=guì|16 +prossimo|pros=si=mo|16 +quanti|quan=ti|16 +regola|re=go=la|16 +sicura|si=cu=ra|16 +soddisfazione|sod=di=sfa=zio=ne|16 +sospetti|so=spet=ti|16 +sportello|spor=tel=lo|16 +stento|sten=to|16 +volendo|vo=len=do|16 +vorrebbe|vor=reb=be|16 +vostre|vo=stre|16 +accidente|ac=ci=den=te|15 +addirittura|ad=di=rit=tu=ra|15 +alcuno|al=cu=no|15 +andarono|an=da=ro=no|15 +aspetta|aspet=ta|15 +aspettar|aspet=tar|15 +batter|bat=ter|15 +cantuccio|can=tuc=cio|15 +capanne|ca=pan=ne|15 +casetta|ca=set=ta|15 +chiamato|chia=ma=to|15 +chiara|chia=ra|15 +chiedere|chie=de=re|15 +chiostro|chio=stro|15 +cognome|co=gno=me|15 +complimenti|com=pli=men=ti|15 +comuni|co=mu=ni|15 +correre|cor=re=re|15 +dignità|di=gni=tà|15 +distanza|di=stan=za|15 +foglie|fo=glie|15 +impazienza|im=pa=zien=za|15 +importa|im=por=ta|15 +invano|in=va=no|15 +merito|me=ri=to|15 +particolari|par=ti=co=la=ri|15 +pellegrino|pel=le=gri=no|15 +portare|por=ta=re|15 +povere|po=ve=re|15 +Provvidenza|Prov=vi=den=za|15 +prudenza|pru=den=za|15 +questione|que=stio=ne|15 +ricevuto|ri=ce=vu=to|15 +rimanente|ri=ma=nen=te|15 +riparare|ri=pa=ra=re|15 +riuscì|riu=scì|15 +servizi|ser=vi=zi|15 +Signor|Si=gnor|15 +solita|so=li=ta|15 +sostenere|so=ste=ne=re|15 +sproposito|spro=po=si=to|15 +storie|sto=rie|15 +straordinario|straor=di=na=rio|15 +strinse|strin=se|15 +termine|ter=mi=ne|15 +trattato|trat=ta=to|15 +ufizio|ufi=zio|15 +ultimi|ul=ti=mi|15 +vendetta|ven=det=ta|15 +venuti|ve=nu=ti|15 +videro|vi=de=ro|15 +violenza|vio=len=za|15 +volevano|vo=le=va=no|15 +accorse|ac=cor=se|14 +antichi|an=ti=chi|14 +arrivava|ar=ri=va=va|14 +attentamente|at=ten=ta=men=te|14 +avvezzo|av=vez=zo|14 +bambino|bam=bi=no|14 +bestia|be=stia|14 +campanello|cam=pa=nel=lo|14 +cavalli|ca=val=li|14 +certezza|cer=tez=za|14 +chiave|chia=ve|14 +comparire|com=pa=ri=re|14 +composta|com=po=sta|14 +condotto|con=dot=to|14 +conforto|con=for=to|14 +confusa|con=fu=sa|14 +cucina|cu=ci=na|14 +delitto|de=lit=to|14 +diritta|di=rit=ta|14 +dispetto|di=spet=to|14 +dobbiamo|dob=bia=mo|14 +doloroso|do=lo=ro=so|14 +elemosina|ele=mo=si=na|14 +entrata|en=tra=ta|14 +fanciulli|fan=ciul=li|14 +fianco|fian=co|14 +fiasco|fia=sco|14 +fortuna|for=tu=na|14 +fratelli|fra=tel=li|14 +girava|gi=ra=va|14 +glielo|glie=lo|14 +inchino|in=chi=no|14 +iniquità|ini=qui=tà|14 +inquietudine|in=quie=tu=di=ne|14 +insegnar|in=se=gnar|14 +leggere|leg=ge=re|14 +licenza|li=cen=za|14 +lontani|lon=ta=ni|14 +Lorenzo|Lo=ren=zo|14 +magistrati|ma=gi=stra=ti|14 +martello|mar=tel=lo|14 +mercato|mer=ca=to|14 +miserie|mi=se=rie|14 +nobili|no=bi=li|14 +oggetto|og=get=to|14 +ordinò|or=di=nò|14 +ospite|ospi=te|14 +parlando|par=lan=do|14 +piccol|pic=col|14 +pregato|pre=ga=to|14 +principessa|prin=ci=pes=sa|14 +provvisione|prov=vi=sio=ne|14 +quanta|quan=ta|14 +quantità|quan=ti=tà|14 +quieto|quie=to|14 +ragazzi|ra=gaz=zi|14 +regole|re=go=le|14 +religioso|re=li=gio=so|14 +respiro|re=spi=ro|14 +ricevere|ri=ce=ve=re|14 +rimanevano|ri=ma=ne=va=no|14 +risolvette|ri=sol=vet=te|14 +ronzìo|ron=zìo|14 +scienza|scien=za|14 +sentiero|sen=tie=ro|14 +siccome|sic=co=me|14 +sparse|spar=se|14 +spazio|spa=zio|14 +stabilito|sta=bi=li=to|14 +tornar|tor=nar|14 +trovasse|tro=vas=se|14 +vecchi|vec=chi|14 +vederlo|ve=der=lo|14 +volere|vo=le=re|14 +abbiano|ab=bia=no|13 +aiutare|aiu=ta=re|13 +Andate|An=da=te|13 +apposta|ap=po=sta|13 +arrivar|ar=ri=var|13 +arrivati|ar=ri=va=ti|13 +averla|aver=la|13 +bambini|bam=bi=ni|13 +bastava|ba=sta=va|13 +bergamasco|ber=ga=ma=sco|13 +boccone|boc=co=ne|13 +cerimonie|ce=ri=mo=nie|13 +chiasso|chias=so|13 +chiuse|chiu=se|13 +codesta|co=de=sta|13 +comandato|co=man=da=to|13 +commissario|com=mis=sa=rio|13 +compagne|com=pa=gne|13 +concluse|con=clu=se|13 +consigli|con=si=gli|13 +contenti|con=ten=ti|13 +conversazione|con=ver=sa=zio=ne|13 +coscienza|co=scien=za|13 +desiderava|de=si=de=ra=va|13 +dimostrazioni|di=mo=stra=zio=ni|13 +dirgli|dir=gli|13 +diventato|di=ven=ta=to|13 +dovevano|do=ve=va=no|13 +entrava|en=tra=va|13 +esecuzione|ese=cu=zio=ne|13 +figliuola|fi=gliuo=la|13 +finita|fi=ni=ta|13 +finora|fi=no=ra|13 +formalità|for=ma=li=tà|13 +Francia|Fran=cia|13 +gentiluomo|gen=ti=luo=mo|13 +gloria|glo=ria|13 +immaginare|im=ma=gi=na=re|13 +infelice|in=fe=li=ce|13 +infermi|in=fer=mi|13 +informato|in=for=ma=to|13 +inutile|inu=ti=le|13 +istrada|istra=da|13 +liberamente|li=be=ra=men=te|13 +libero|li=be=ro|13 +lunghi|lun=ghi|13 +mancava|man=ca=va|13 +mandar|man=dar|13 +marchese|mar=che=se|13 +medesimi|me=de=si=mi|13 +menzione|men=zio=ne|13 +mucchio|muc=chio|13 +nascere|na=sce=re|13 +nostre|no=stre|13 +oggetti|og=get=ti|13 +palazzotto|pa=laz=zot=to|13 +paragone|pa=ra=go=ne|13 +pochino|po=chi=no|13 +potere|po=te=re|13 +potessero|po=tes=se=ro|13 +pratica|pra=ti=ca|13 +proposto|pro=po=sto|13 +provare|pro=va=re|13 +raccolta|rac=col=ta|13 +rimanere|ri=ma=ne=re|13 +rimorso|ri=mor=so|13 +ringrazio|rin=gra=zio|13 +risposte|ri=spo=ste|13 +riuscita|riu=sci=ta|13 +salute|sa=lu=te|13 +sapesse|sa=pes=se|13 +saranno|sa=ran=no|13 +scosse|scos=se|13 +servito|ser=vi=to|13 +sicché|sic=ché|13 +smania|sma=nia|13 +spuntar|spun=tar|13 +strana|stra=na|13 +stretta|stret=ta|13 +tenendo|te=nen=do|13 +terribili|ter=ri=bi=li|13 +toccato|toc=ca=to|13 +tremante|tre=man=te|13 +ubbidire|ub=bi=di=re|13 +unghie|un=ghie|13 +vestito|ve=sti=to|13 +adesso|ades=so|12 +allegria|al=le=gria|12 +alzato|al=za=to|12 +antecedente|an=te=ce=den=te|12 +assedio|as=se=dio|12 +avrete|avre=te|12 +avvicinarsi|av=vi=ci=nar=si|12 +baroccio|ba=roc=cio|12 +bianco|bian=co|12 +bocche|boc=che|12 +burrasca|bur=ra=sca|12 +bussola|bus=so=la|12 +cancelliere|can=cel=lie=re|12 +Casale|Ca=sa=le|12 +casuccia|ca=suc=cia|12 +ciarle|ciar=le|12 +concetto|con=cet=to|12 +condusse|con=dus=se|12 +congetture|con=get=tu=re|12 +conseguenza|con=se=guen=za|12 +continuava|con=ti=nua=va|12 +contorno|con=tor=no|12 +corona|co=ro=na|12 +dabbene|dab=be=ne|12 +dicevano|di=ce=va=no|12 +difesa|di=fe=sa|12 +disposto|di=spo=sto|12 +distinguere|di=stin=gue=re|12 +dodici|do=di=ci|12 +dolori|do=lo=ri|12 +domandare|do=man=da=re|12 +dottor|dot=tor|12 +dovessero|do=ves=se=ro|12 +espressamente|espres=sa=men=te|12 +facessero|fa=ces=se=ro|12 +farina|fa=ri=na|12 +finché|fin=ché|12 +grossa|gros=sa|12 +grosso|gros=so|12 +imbroglio|im=bro=glio|12 +immobile|im=mo=bi=le|12 +incamminò|in=cam=mi=nò|12 +ingegno|in=ge=gno|12 +interno|in=ter=no|12 +lamenti|la=men=ti|12 +lasciare|la=scia=re|12 +lunghe|lun=ghe|12 +maggiore|mag=gio=re|12 +Mantova|Man=to=va|12 +mettesse|met=tes=se|12 +miglior|mi=glior|12 +ministero|mi=ni=ste=ro|12 +novità|no=vi=tà|12 +offeso|of=fe=so|12 +opposta|op=po=sta|12 +partire|par=ti=re|12 +passaggio|pas=sag=gio|12 +passando|pas=san=do|12 +patito|pa=ti=to|12 +pensarci|pen=sar=ci|12 +perder|per=der|12 +piglia|pi=glia|12 +prendesse|pren=des=se|12 +prepotente|pre=po=ten=te|12 +propria|pro=pria|12 +proseguiva|pro=se=gui=va|12 +Qualche|Qual=che|12 +Quelli|Quel=li|12 +Questi|Que=sti|12 +quiete|quie=te|12 +rendeva|ren=de=va|12 +ricerche|ri=cer=che|12 +risoluta|ri=so=lu=ta|12 +riusciva|riu=sci=va|12 +saluti|sa=lu=ti|12 +sappiamo|sap=pia=mo|12 +saprei|sa=prei|12 +scoperta|sco=per=ta|12 +scritto|scrit=to|12 +scrittore|scrit=to=re|12 +sentirete|sen=ti=re=te|12 +Sentite|Sen=ti=te|12 +servire|ser=vi=re|12 +Sicuro|Si=cu=ro|12 +sinistro|si=ni=stro|12 +sollievo|sol=lie=vo|12 +stando|stan=do|12 +stanno|stan=no|12 +stanze|stan=ze|12 +stizza|stiz=za|12 +superiore|su=pe=rio=re|12 +tavolino|ta=vo=li=no|12 +tirare|ti=ra=re|12 +toccava|toc=ca=va|12 +tratti|trat=ti|12 +trovandosi|tro=van=do=si|12 +vedrete|ve=dre=te|12 +veduti|ve=du=ti|12 +vestiti|ve=sti=ti|12 +voltava|vol=ta=va|12 +alcuna|al=cu=na|11 +allegra|al=le=gra|11 +altrimenti|al=tri=men=ti|11 +Ambrogio|Am=bro=gio|11 +ammalati|am=ma=la=ti|11 +Andava|An=da=va|11 +angolo|an=go=lo|11 +appetito|ap=pe=ti=to|11 +avreste|avre=ste|11 +avvenimenti|av=ve=ni=men=ti|11 +bastone|ba=sto=ne|11 +benedica|be=ne=di=ca|11 +caccia|cac=cia|11 +cacciò|cac=ciò|11 +cadere|ca=de=re|11 +cagioni|ca=gio=ni|11 +camminando|cam=mi=nan=do|11 +camminare|cam=mi=na=re|11 +campana|cam=pa=na|11 +canaglia|ca=na=glia|11 +cavalieri|ca=va=lie=ri|11 +cercato|cer=ca=to|11 +chiamata|chia=ma=ta|11 +chiaramente|chia=ra=men=te|11 +chiuso|chiu=so|11 +cinque|cin=que|11 +cominciarono|co=min=cia=ro=no|11 +comitiva|co=mi=ti=va|11 +conveniva|con=ve=ni=va|11 +costretto|co=stret=to|11 +costrutto|co=strut=to|11 +debolezza|de=bo=lez=za|11 +disperazione|di=spe=ra=zio=ne|11 +dispiace|di=spia=ce|11 +divenuta|di=ve=nu=ta|11 +domattina|do=mat=ti=na|11 +esprimeva|espri=me=va|11 +fuorché|fuor=ché|11 +gastigo|ga=sti=go|11 +Gervaso|Ger=va=so|11 +imbrogli|im=bro=gli|11 +imparato|im=pa=ra=to|11 +importante|im=por=tan=te|11 +incomodo|in=co=mo=do|11 +intenzioni|in=ten=zio=ni|11 +interesse|in=te=res=se|11 +lasciate|la=scia=te|11 +lasciatemi|la=scia=te=mi|11 +lettere|let=te=re|11 +levato|le=va=to|11 +maestra|mae=stra|11 +maledetto|ma=le=det=to|11 +mancare|man=ca=re|11 +mancato|man=ca=to|11 +mandare|man=da=re|11 +manoscritto|ma=no=scrit=to|11 +mistero|mi=ste=ro|11 +motivi|mo=ti=vi|11 +moversi|mo=ver=si|11 +nemmeno|nem=me=no|11 +opposto|op=po=sto|11 +ospiti|ospi=ti|11 +pareri|pa=re=ri|11 +passeggieri|pas=seg=gie=ri|11 +perdere|per=de=re|11 +perduto|per=du=to|11 +piangere|pian=ge=re|11 +piazzetta|piaz=zet=ta|11 +politica|po=li=ti=ca|11 +popolazione|po=po=la=zio=ne|11 +portando|por=tan=do|11 +portate|por=ta=te|11 +potessi|po=tes=si|11 +preghiere|pre=ghie=re|11 +principino|prin=ci=pi=no|11 +profondo|pro=fon=do|11 +pronta|pron=ta|11 +ragion|ra=gion|11 +relazione|re=la=zio=ne|11 +ricerca|ri=cer=ca|11 +rimprovero|rim=pro=ve=ro|11 +ringraziamenti|rin=gra=zia=men=ti|11 +ripreso|ri=pre=so|11 +ripugnanza|ri=pu=gnan=za|11 +sarete|sa=re=te|11 +scappare|scap=pa=re|11 +scappato|scap=pa=to|11 +sconosciuta|sco=no=sciu=ta|11 +sentirsi|sen=tir=si|11 +sentivano|sen=ti=va=no|11 +serviva|ser=vi=va|11 +sforzo|sfor=zo|11 +sicuramente|si=cu=ra=men=te|11 +sorpresa|sor=pre=sa|11 +sorriso|sor=ri=so|11 +Spagna|Spa=gna|11 +speranze|spe=ran=ze|11 +superiorità|su=pe=rio=ri=tà|11 +tenersi|te=ner=si|11 +tornando|tor=nan=do|11 +traspariva|tra=spa=ri=va|11 +trovati|tro=va=ti|11 +untori|un=to=ri|11 +uscito|usci=to|11 +vederla|ve=der=la|11 +vedesse|ve=des=se|11 +vedremo|ve=dre=mo|11 +verrebbe|ver=reb=be|11 +Vorrei|Vor=rei|11 +abitudini|abi=tu=di=ni|10 +accennava|ac=cen=na=va|10 +accennò|ac=cen=nò|10 +accidenti|ac=ci=den=ti|10 +affinché|af=fin=ché|10 +agitazione|agi=ta=zio=ne|10 +andassero|an=das=se=ro|10 +anderebbe|an=de=reb=be|10 +annunzio|an=nun=zio|10 +apparenza|ap=pa=ren=za|10 +aprire|apri=re|10 +arbitrio|ar=bi=trio|10 +arrivata|ar=ri=va=ta|10 +aspettativa|aspet=ta=ti=va|10 +attaccato|at=tac=ca=to|10 +avesser|aves=ser|10 +battaglia|bat=ta=glia|10 +bellezza|bel=lez=za|10 +bianca|bian=ca|10 +brutte|brut=te|10 +cagion|ca=gion|10 +calpestìo|cal=pe=stìo|10 +camminava|cam=mi=na=va|10 +capisco|ca=pi=sco|10 +capito|ca=pi=to|10 +celebre|ce=le=bre|10 +cercavano|cer=ca=va=no|10 +chiacchiere|chiac=chie=re|10 +chiamar|chia=mar|10 +chiare|chia=re|10 +codeste|co=de=ste|10 +collera|col=le=ra|10 +colloquio|col=lo=quio|10 +compiacenza|com=pia=cen=za|10 +confine|con=fi=ne|10 +congiuntura|con=giun=tu=ra|10 +conosce|co=no=sce|10 +conosceva|co=no=sce=va|10 +conseguenze|con=se=guen=ze|10 +Consiglio|Con=si=glio|10 +contadino|con=ta=di=no|10 +conversione|con=ver=sio=ne|10 +convertito|con=ver=ti=to|10 +cristiano|cri=stia=no|10 +Diavolo|Dia=vo=lo|10 +dicesse|di=ces=se|10 +dimodoché|di=mo=do=ché|10 +disprezzo|di=sprez=zo|10 +distintamente|di=stin=ta=men=te|10 +diversa|di=ver=sa|10 +diverse|di=ver=se|10 +dottrina|dot=tri=na|10 +dovrebbe|do=vreb=be|10 +ebbero|eb=be=ro|10 +eccellenza|ec=cel=len=za|10 +entrato|en=tra=to|10 +Eppure|Ep=pu=re|10 +esitazione|esi=ta=zio=ne|10 +fastidio|fa=sti=dio|10 +fattoressa|fat=to=res=sa|10 +fissato|fis=sa=to|10 +forestiero|fo=re=stie=ro|10 +fortemente|for=te=men=te|10 +galera|ga=le=ra|10 +girare|gi=ra=re|10 +giusta|giu=sta|10 +grosse|gros=se|10 +immediatamente|im=me=dia=ta=men=te|10 +impaziente|im=pa=zien=te|10 +impicciato|im=pic=cia=to|10 +impressione|im=pres=sio=ne|10 +inaspettata|ina=spet=ta=ta|10 +infame|in=fa=me|10 +intende|in=ten=de|10 +intendo|in=ten=do|10 +intera|in=te=ra|10 +inutili|inu=ti=li|10 +lasciarlo|la=sciar=lo|10 +lentamente|len=ta=men=te|10 +linguaggio|lin=guag=gio|10 +mantenere|man=te=ne=re|10 +minaccioso|mi=nac=cio=so|10 +monatto|mo=nat=to|10 +mormorìo|mor=mo=rìo|10 +nascosta|na=sco=sta|10 +necessaria|ne=ces=sa=ria|10 +nominò|no=mi=nò|10 +occhiate|oc=chia=te|10 +operai|ope=rai|10 +opposte|op=po=ste|10 +orientale|orien=ta=le|10 +osservare|os=ser=va=re|10 +paresse|pa=res=se|10 +passate|pas=sa=te|10 +passati|pas=sa=ti|10 +pensasse|pen=sas=se|10 +perciò|per=ciò|10 +persuaso|per=sua=so|10 +piacesse|pia=ces=se|10 +piatto|piat=to|10 +posson|pos=son|10 +poveretta|po=ve=ret=ta|10 +predica|pre=di=ca|10 +predicare|pre=di=ca=re|10 +presidente|pre=si=den=te|10 +Presto|Pre=sto|10 +pubblici|pub=bli=ci|10 +quattr|quat=tr|10 +ragionevole|ra=gio=ne=vo=le|10 +rammentò|ram=men=tò|10 +recinto|re=cin=to|10 +relazioni|re=la=zio=ni|10 +rendere|ren=de=re|10 +reverendo|re=ve=ren=do|10 +ricevette|ri=ce=vet=te|10 +ricevuta|ri=ce=vu=ta|10 +riconobbe|ri=co=nob=be|10 +riconoscere|ri=co=no=sce=re|10 +rimasero|ri=ma=se=ro|10 +ripeté|ri=pe=té|10 +riputazione|ri=pu=ta=zio=ne|10 +ritirarsi|ri=ti=rar=si|10 +ritirò|ri=ti=rò|10 +rivedremo|ri=ve=dre=mo|10 +sapendo|sa=pen=do|10 +scorrere|scor=re=re|10 +scritta|scrit=ta|10 +sdegno|sde=gno|10 +semplice|sem=pli=ce|10 +sentenza|sen=ten=za|10 +sentirlo|sen=tir=lo|10 +sentisse|sen=tis=se|10 +servir|ser=vir|10 +severo|se=ve=ro|10 +sicuri|si=cu=ri|10 +solitudine|so=li=tu=di=ne|10 +sospeso|so=spe=so|10 +sospiro|so=spi=ro|10 +sostanza|so=stan=za|10 +stavan|sta=van|10 +tacere|ta=ce=re|10 +temeva|te=me=va|10 +tentato|ten=ta=to|10 +testimoni|te=sti=mo=ni|10 +testimonio|te=sti=mo=nio|10 +tirava|ti=ra=va|10 +toccare|toc=ca=re|10 +tornate|tor=na=te|10 +tristamente|tri=sta=men=te|10 +troncò|tron=cò|10 +trovavano|tro=va=va=no|10 +turbamento|tur=ba=men=to|10 +umiltà|umil=tà|10 +uscirne|uscir=ne|10 +vantaggio|van=tag=gio|10 +Vedete|Ve=de=te|10 +Venezia|Ve=ne=zia|10 +venticinque|ven=ti=cin=que|10 +ventura|ven=tu=ra|10 +Vergine|Ver=gi=ne|10 +vigore|vi=go=re|10 +viottole|viot=to=le|10 +voglion|vo=glion|10 +volevan|vo=le=van|10 +abbattimento|ab=bat=ti=men=to|9 +abilità|abi=li=tà|9 +accennando|ac=cen=nan=do|9 +accompagnato|ac=com=pa=gna=to|9 +addietro|ad=die=tro|9 +affetto|af=fet=to|9 +albero|al=be=ro|9 +alzava|al=za=va|9 +ammazzare|am=maz=za=re|9 +Andiamo|An=dia=mo|9 +angoscia|an=go=scia|9 +angustie|an=gu=stie|9 +ansietà|an=sie=tà|9 +apparato|ap=pa=ra=to|9 +ardore|ar=do=re|9 +argomento|ar=go=men=to|9 +arrivarono|ar=ri=va=ro=no|9 +Arrivato|Ar=ri=va=to|9 +aspettato|aspet=ta=to|9 +avrebber|avreb=ber|9 +avvertire|av=ver=ti=re|9 +avvezzi|av=vez=zi|9 +bambina|bam=bi=na|9 +battendo|bat=ten=do|9 +battenti|bat=ten=ti|9 +benevolenza|be=ne=vo=len=za|9 +bestie|be=stie|9 +bocconi|boc=co=ni|9 +botteghe|bot=te=ghe|9 +brulichìo|bru=li=chìo|9 +brutto|brut=to|9 +cacciato|cac=cia=to|9 +cadavere|ca=da=ve=re|9 +cadeva|ca=de=va|9 +calamaio|ca=la=ma=io|9 +cantonata|can=to=na=ta|9 +carattere|ca=rat=te=re|9 +carezze|ca=rez=ze|9 +cattura|cat=tu=ra|9 +chiamano|chia=ma=no|9 +chiesto|chie=sto|9 +chiusa|chiu=sa|9 +cinquanta|cin=quan=ta|9 +comanda|co=man=da|9 +comandare|co=man=da=re|9 +comandi|co=man=di|9 +cominciato|co=min=cia=to|9 +commensali|com=men=sa=li|9 +complimento|com=pli=men=to|9 +concorso|con=cor=so|9 +condotti|con=dot=ti|9 +congratulazioni|con=gra=tu=la=zio=ni|9 +conosciuto|co=no=sciu=to|9 +consenso|con=sen=so|9 +consolazioni|con=so=la=zio=ni|9 +console|con=so=le|9 +Cordova|Cor=do=va|9 +cortiletto|cor=ti=let=to|9 +credete|cre=de=te|9 +crescere|cre=sce=re|9 +cresciuto|cre=sciu=to|9 +crocchio|croc=chio|9 +curiosi|cu=rio=si|9 +debole|de=bo=le|9 +discorrere|di=scor=re=re|9 +disparte|di=spar=te|9 +distinta|di=stin=ta|9 +ditemi|di=te=mi|9 +domandar|do=man=dar|9 +domandate|do=man=da=te|9 +dovevan|do=ve=van|9 +ducato|du=ca=to|9 +eppure|ep=pu=re|9 +espressione|espres=sio=ne|9 +faccenda|fac=cen=da|9 +facciamo|fac=cia=mo|9 +facoltà|fa=col=tà|9 +fanciullo|fan=ciul=lo|9 +fatale|fa=ta=le|9 +fatemi|fa=te=mi|9 +Felice|Fe=li=ce|9 +Fernandez|Fer=nan=dez|9 +figure|fi=gu=re|9 +forestieri|fo=re=stie=ri|9 +Francesco|Fran=ce=sco|9 +fresco|fre=sco|9 +Galdino|Gal=di=no|9 +garzone|gar=zo=ne|9 +generalmente|ge=ne=ral=men=te|9 +gravità|gra=vi=tà|9 +impedimento|im=pe=di=men=to|9 +impicci|im=pic=ci|9 +impiego|im=pie=go|9 +imprese|im=pre=se|9 +incerto|in=cer=to|9 +inchini|in=chi=ni|9 +indicava|in=di=ca=va|9 +interrogazioni|in=ter=ro=ga=zio=ni|9 +istruzioni|istru=zio=ni|9 +lavorare|la=vo=ra=re|9 +lingua|lin=gua|9 +macchie|mac=chie|9 +malizia|ma=li=zia|9 +massime|mas=si=me|9 +metterli|met=ter=li|9 +migliori|mi=glio=ri|9 +minacce|mi=nac=ce|9 +minaccia|mi=nac=cia|9 +miracolo|mi=ra=co=lo|9 +miserabile|mi=se=ra=bi=le|9 +misero|mi=se=ro|9 +mortalità|mor=ta=li=tà|9 +negozio|ne=go=zio|9 +nobile|no=bi=le|9 +obbligo|ob=bli=go|9 +occasioni|oc=ca=sio=ni|9 +oscuro|oscu=ro|9 +paesetto|pae=set=to|9 +parente|pa=ren=te|9 +parevano|pa=re=va=no|9 +parolina|pa=ro=li=na|9 +patimenti|pa=ti=men=ti|9 +patria|pa=tria|9 +peccato|pec=ca=to|9 +persuasione|per=sua=sio=ne|9 +pianta|pian=ta|9 +piccino|pic=ci=no|9 +polenta|po=len=ta|9 +potersi|po=ter=si|9 +predicatore|pre=di=ca=to=re|9 +preparato|pre=pa=ra=to|9 +presentò|pre=sen=tò|9 +principi|prin=ci=pi|9 +proferire|pro=fe=ri=re|9 +prometteva|pro=met=te=va|9 +punizione|pu=ni=zio=ne|9 +Queste|Que=ste|9 +quieta|quie=ta|9 +realtà|real=tà|9 +ricever|ri=ce=ver|9 +ricordo|ri=cor=do|9 +rifiuto|ri=fiu=to|9 +riuscire|riu=sci=re|9 +saggio|sag=gio|9 +salita|sa=li=ta|9 +salutò|sa=lu=tò|9 +saperne|sa=per=ne|9 +saprebbe|sa=preb=be|9 +scappò|scap=pò|9 +schioppo|schiop=po|9 +sciagurata|scia=gu=ra=ta|9 +scoprì|sco=prì|9 +scorgere|scor=ge=re|9 +seguitò|se=gui=tò|9 +signoria|si=gno=ria|9 +somigliante|so=mi=glian=te|9 +soprattutto|so=prat=tut=to|9 +sparso|spar=so|9 +spasso|spas=so|9 +spiraglio|spi=ra=glio|9 +staccò|stac=cò|9 +stamattina|sta=mat=ti=na|9 +strepito|stre=pi=to|9 +strette|stret=te|9 +superiori|su=pe=rio=ri|9 +sventura|sven=tu=ra|9 +tenerlo|te=ner=lo|9 +termini|ter=mi=ni|9 +tiranno|ti=ran=no|9 +tradimento|tra=di=men=to|9 +trovan|tro=van|9 +trovate|tro=va=te|9 +ubbidienza|ub=bi=dien=za|9 +uguale|ugua=le|9 +unzioni|un=zio=ni|9 +uscita|usci=ta|9 +vedervi|ve=der=vi|9 +veduta|ve=du=ta|9 +venissero|ve=nis=se=ro|9 +venute|ve=nu=te|9 +vescovo|ve=sco=vo|9 +vivente|vi=ven=te|9 +Voglio|Vo=glio|9 +abbassò|ab=bas=sò|8 +abitanti|abi=tan=ti|8 +accoglienze|ac=co=glien=ze|8 +accompagnò|ac=com=pa=gnò|8 +affacciò|af=fac=ciò|8 +aggiunse|ag=giun=se|8 +alabardieri|ala=bar=die=ri|8 +allegri|al=le=gri|8 +allontanarsi|al=lon=ta=nar=si|8 +anderò|an=de=rò|8 +annunziava|an=nun=zia=va|8 +aperte|aper=te|8 +apertura|aper=tu=ra|8 +apparire|ap=pa=ri=re|8 +appoggiata|ap=pog=gia=ta|8 +arrabbiato|ar=rab=bia=to|8 +assenza|as=sen=za|8 +assito|as=si=to|8 +assolutamente|as=so=lu=ta=men=te|8 +attitudine|at=ti=tu=di=ne|8 +attività|at=ti=vi=tà|8 +attonito|at=to=ni=to|8 +autori|au=to=ri|8 +avanzi|avan=zi|8 +aveste|ave=ste|8 +avvedersene|av=ve=der=se=ne|8 +Azzecca|Az=zec=ca|8 +benefattore|be=ne=fat=to=re|8 +Bisognerebbe|Bi=so=gne=reb=be|8 +brutta|brut=ta|8 +cammina|cam=mi=na|8 +cappella|cap=pel=la|8 +carica|ca=ri=ca|8 +cattive|cat=ti=ve|8 +cessato|ces=sa=to|8 +cittadini|cit=ta=di=ni|8 +ciuffo|ciuf=fo|8 +codesti|co=de=sti|8 +cogliere|co=glie=re|8 +colore|co=lo=re|8 +colori|co=lo=ri|8 +cominciando|co=min=cian=do|8 +comparve|com=par=ve|8 +composto|com=po=sto|8 +concerti|con=cer=ti|8 +confusi|con=fu=si|8 +conoscer|co=no=scer|8 +continuato|con=ti=nua=to|8 +contorni|con=tor=ni|8 +convoglio|con=vo=glio|8 +coperte|co=per=te|8 +correva|cor=re=va|8 +costole|co=sto=le|8 +cristiani|cri=stia=ni|8 +curati|cu=ra=ti|8 +debito|de=bi=to|8 +deboli|de=bo=li|8 +descrivere|de=scri=ve=re|8 +destrezza|de=strez=za|8 +diavoli|dia=vo=li|8 +dimenticare|di=men=ti=ca=re|8 +direbbe|di=reb=be|8 +discosto|di=sco=sto|8 +diventar|di=ven=tar|8 +dolorosi|do=lo=ro=si|8 +Domani|Do=ma=ni|8 +eccetera|ec=ce=te=ra|8 +Egidio|Egi=dio|8 +entrarono|en=tra=ro=no|8 +errori|er=ro=ri|8 +eseguire|ese=gui=re|8 +espresso|espres=so|8 +esserne|es=ser=ne|8 +estremità|estre=mi=tà|8 +famiglie|fa=mi=glie|8 +faremo|fa=re=mo|8 +farsetto|far=set=to|8 +fermandosi|fer=man=do=si|8 +fermare|fer=ma=re|8 +feroce|fe=ro=ce|8 +fidato|fi=da=to|8 +frastono|fra=sto=no|8 +fuggitivo|fug=gi=ti=vo|8 +garbugli|gar=bu=gli|8 +generali|ge=ne=ra=li|8 +giudizi|giu=di=zi|8 +giunta|giun=ta|8 +giuste|giu=ste|8 +gliela|glie=la|8 +guardavano|guar=da=va=no|8 +impunità|im=pu=ni=tà|8 +inaspettato|ina=spet=ta=to|8 +inclinazione|in=cli=na=zio=ne|8 +incontrava|in=con=tra=va|8 +indosso|in=dos=so|8 +inferno|in=fer=no|8 +insidie|in=si=die|8 +intollerabile|in=tol=le=ra=bi=le|8 +invidia|in=vi=dia|8 +invito|in=vi=to|8 +istanza|istan=za|8 +laggiù|lag=giù|8 +lanzichenecchi|lan=zi=che=nec=chi|8 +Lasciatemi|La=scia=te=mi|8 +lascio|la=scio|8 +lettori|let=to=ri|8 +lettuccio|let=tuc=cio|8 +libera|li=be=ra|8 +liberi|li=be=ri|8 +lingue|lin=gue|8 +Madrid|Ma=drid|8 +mandava|man=da=va|8 +mantener|man=te=ner|8 +Martino|Mar=ti=no|8 +metterla|met=ter=la|8 +mezzogiorno|mez=zo=gior=no|8 +milanesi|mi=la=ne=si|8 +misterioso|mi=ste=rio=so|8 +mutata|mu=ta=ta|8 +mutato|mu=ta=to|8 +necessarie|ne=ces=sa=rie|8 +Nessuno|Nes=su=no|8 +notabile|no=ta=bi=le|8 +omicidio|omi=ci=dio|8 +oppresso|op=pres=so|8 +orribile|or=ri=bi=le|8 +ostacolo|osta=co=lo|8 +panche|pan=che|8 +parete|pa=re=te|8 +parlarne|par=lar=ne|8 +parrochi|par=ro=chi|8 +passano|pas=sa=no|8 +passeggiata|pas=seg=gia=ta|8 +passeggiero|pas=seg=gie=ro|8 +pastore|pa=sto=re|8 +Pasturo|Pa=stu=ro|8 +pestilenza|pe=sti=len=za|8 +piccole|pic=co=le|8 +pietre|pie=tre|8 +polvere|pol=ve=re|8 +portarsi|por=tar=si|8 +potrete|po=tre=te|8 +potuta|po=tu=ta|8 +poverini|po=ve=ri=ni|8 +premeva|pre=me=va|8 +prende|pren=de|8 +presenti|pre=sen=ti|8 +principali|prin=ci=pa=li|8 +proponeva|pro=po=ne=va|8 +propriamente|pro=pria=men=te|8 +raccogliere|rac=co=glie=re|8 +regolare|re=go=la=re|8 +restare|re=sta=re|8 +riconoscenza|ri=co=no=scen=za|8 +riesce|rie=sce|8 +riferire|ri=fe=ri=re|8 +rimane|ri=ma=ne|8 +Rimini|Ri=mi=ni|8 +rimproveri|rim=pro=ve=ri|8 +ringraziare|rin=gra=zia=re|8 +ringraziato|rin=gra=zia=to|8 +ripetendo|ri=pe=ten=do|8 +ripetere|ri=pe=te=re|8 +rischio|ri=schio|8 +risponde|ri=spon=de|8 +ritorno|ri=tor=no|8 +riverenza|ri=ve=ren=za|8 +rubare|ru=ba=re|8 +saccheggio|sac=cheg=gio|8 +sacrifizio|sa=cri=fi=zio|8 +saluto|sa=lu=to|8 +Sarebbe|Sa=reb=be|8 +Savoia|Sa=vo=ia|8 +scalini|sca=li=ni|8 +scandolo|scan=do=lo|8 +scansare|scan=sa=re|8 +scegliere|sce=glie=re|8 +scoprire|sco=pri=re|8 +scorta|scor=ta|8 +scrisse|scris=se|8 +seduto|se=du=to|8 +segretario|se=gre=ta=rio|8 +sentita|sen=ti=ta|8 +sgherri|sgher=ri|8 +Siccome|Sic=co=me|8 +sommossa|som=mos=sa|8 +sparsa|spar=sa|8 +sperava|spe=ra=va|8 +spiegare|spie=ga=re|8 +spinti|spin=ti|8 +Stette|Stet=te|8 +strani|stra=ni|8 +straordinaria|straor=di=na=ria|8 +suddetto|sud=det=to|8 +suggezione|sug=ge=zio=ne|8 +supplica|sup=pli=ca|8 +temerario|te=me=ra=rio|8 +temere|te=me=re|8 +temuto|te=mu=to|8 +tenebre|te=ne=bre|8 +terrori|ter=ro=ri|8 +toccar|toc=car|8 +tornarono|tor=na=ro=no|8 +traccia|trac=cia|8 +tranquilla|tran=quil=la|8 +tristi|tri=sti|8 +trovavan|tro=va=van|8 +troverà|tro=ve=rà|8 +troviamo|tro=via=mo|8 +ucciso|uc=ci=so|8 +untore|un=to=re|8 +vedevan|ve=de=van|8 +Vediamo|Ve=dia=mo|8 +viottola|viot=to=la|8 +visite|vi=si=te|8 +vogliam|vo=gliam|8 +vogliamo|vo=glia=mo|8 +vogliono|vo=glio=no|8 +volessero|vo=les=se=ro|8 +abbian|ab=bian|7 +accadeva|ac=ca=de=va|7 +accattoni|ac=cat=to=ni|7 +accoglienza|ac=co=glien=za|7 +accompagnando|ac=com=pa=gnan=do|7 +accorgersi|ac=cor=ger=si|7 +accorrere|ac=cor=re=re|7 +accorreva|ac=cor=re=va|7 +affanno|af=fan=no|7 +aggiunga|ag=giun=ga|7 +allungando|al=lun=gan=do|7 +altare|al=ta=re|7 +amiche|ami=che|7 +andaron|an=da=ron|7 +ansiosamente|an=sio=sa=men=te|7 +antiche|an=ti=che|7 +appariva|ap=pa=ri=va|7 +appestati|ap=pe=sta=ti|7 +aprirsi|aprir=si|7 +armati|ar=ma=ti|7 +arrivavano|ar=ri=va=va=no|7 +aspettata|aspet=ta=ta|7 +aspetti|aspet=ti|7 +attaccò|at=tac=cò|7 +attenti|at=ten=ti|7 +autunno|au=tun=no|7 +avanzava|avan=za=va|7 +avventura|av=ven=tu=ra|7 +avviato|av=via=to|7 +badate|ba=da=te|7 +benedetta|be=ne=det=ta|7 +biblioteca|bi=blio=te=ca|7 +brevemente|bre=ve=men=te|7 +breviario|bre=via=rio|7 +brontolando|bron=to=lan=do|7 +calamità|ca=la=mi=tà|7 +casato|ca=sa=to|7 +cavalleria|ca=val=le=ria|7 +cencio|cen=cio|7 +cercate|cer=ca=te|7 +chiedeva|chie=de=va|7 +chiedo|chie=do|7 +Chiesa|Chie=sa|7 +chiudere|chiu=de=re|7 +cintura|cin=tu=ra|7 +cipiglio|ci=pi=glio|7 +cocchiere|coc=chie=re|7 +cominciavano|co=min=cia=va=no|7 +commissione|com=mis=sio=ne|7 +commosso|com=mos=so|7 +commozione|com=mo=zio=ne|7 +conclusione|con=clu=sio=ne|7 +concluso|con=clu=so|7 +conduceva|con=du=ce=va|7 +confidare|con=fi=da=re|7 +contare|con=ta=re|7 +contentezza|con=ten=tez=za|7 +continua|con=ti=nua|7 +contrario|con=tra=rio|7 +contrasti|con=tra=sti|7 +correr|cor=rer|7 +cortesia|cor=te=sia|7 +credeva|cre=de=va|7 +credito|cre=di=to|7 +creduta|cre=du=ta|7 +crescente|cre=scen=te|7 +curava|cu=ra=va|7 +degnazione|de=gna=zio=ne|7 +delitti|de=lit=ti|7 +deposito|de=po=si=to|7 +destinato|de=sti=na=to|7 +destino|de=sti=no|7 +diedero|die=de=ro|7 +difendere|di=fen=de=re|7 +dirimpetto|di=rim=pet=to|7 +discrezione|di=scre=zio=ne|7 +disgraziato|di=sgra=zia=to|7 +distesa|di=ste=sa|7 +dolorosamente|do=lo=ro=sa=men=te|7 +domandava|do=man=da=va|7 +dovevo|do=ve=vo|7 +Eccellentissimo|Ec=cel=len=tis=si=mo|7 +edifizio|edi=fi=zio|7 +eminenza|emi=nen=za|7 +eravamo|era=va=mo|7 +estremo|estre=mo|7 +Europa|Eu=ro=pa|7 +famoso|fa=mo=so|7 +fascio|fa=scio|7 +fazzoletto|faz=zo=let=to|7 +fermata|fer=ma=ta|7 +fermava|fer=ma=va|7 +filatore|fi=la=to=re|7 +filosofia|fi=lo=so=fia|7 +frequenti|fre=quen=ti|7 +fuggire|fug=gi=re|7 +furore|fu=ro=re|7 +ginocchio|gi=noc=chio|7 +granaglie|gra=na=glie|7 +grandine|gran=di=ne|7 +gridarono|gri=da=ro=no|7 +guadagno|gua=da=gno|7 +guardandolo|guar=dan=do=lo|7 +guasto|gua=sto|7 +ignorante|igno=ran=te|7 +Illustrissimo|Il=lu=stris=si=mo|7 +immaginazione|im=ma=gi=na=zio=ne|7 +incarico|in=ca=ri=co|7 +indicato|in=di=ca=to|7 +innocenti|in=no=cen=ti|7 +innocenza|in=no=cen=za|7 +inquieto|in=quie=to|7 +insegna|in=se=gna|7 +intendeva|in=ten=de=va|7 +interrogare|in=ter=ro=ga=re|7 +ispirazione|ispi=ra=zio=ne|7 +istantaneo|istan=ta=neo|7 +istanze|istan=ze|7 +leggermente|leg=ger=men=te|7 +letterato|let=te=ra=to|7 +lettighiero|let=ti=ghie=ro|7 +levare|le=va=re|7 +licenziò|li=cen=ziò|7 +magnificenza|ma=gni=fi=cen=za|7 +malgrado|mal=gra=do|7 +mancina|man=ci=na|7 +mascalzone|ma=scal=zo=ne|7 +merita|me=ri=ta|7 +mettendosi|met=ten=do=si|7 +metterlo|met=ter=lo|7 +mettervi|met=ter=vi|7 +migliaia|mi=glia=ia|7 +minuto|mi=nu=to|7 +mirabile|mi=ra=bi=le|7 +misura|mi=su=ra|7 +Monferrato|Mon=fer=ra=to|7 +montanaro|mon=ta=na=ro|7 +necessari|ne=ces=sa=ri|7 +negare|ne=ga=re|7 +nemmen|nem=men|7 +Nevers|Ne=vers|7 +Niente|Nien=te|7 +novembre|no=vem=bre|7 +Ognuno|Ognu=no|7 +onesta|one=sta|7 +ostacoli|osta=co=li|7 +ostante|ostan=te|7 +ottenere|ot=te=ne=re|7 +pagare|pa=ga=re|7 +parendogli|pa=ren=do=gli|7 +parlate|par=la=te|7 +parlatorio|par=la=to=rio|7 +partenza|par=ten=za|7 +partiti|par=ti=ti|7 +paternità|pa=ter=ni=tà|7 +pavimento|pa=vi=men=to|7 +pendìo|pen=dìo|7 +pensar|pen=sar|7 +penuria|pe=nu=ria|7 +perduta|per=du=ta|7 +pericoloso|pe=ri=co=lo=so|7 +pesante|pe=san=te|7 +pescatore|pe=sca=to=re|7 +piazze|piaz=ze|7 +picchiare|pic=chia=re|7 +poiché|poi=ché|7 +portati|por=ta=ti|7 +portico|por=ti=co|7 +potendo|po=ten=do|7 +potenza|po=ten=za|7 +poterono|po=te=ro=no|7 +potrei|po=trei|7 +Povera|Po=ve=ra|7 +precipitosamente|pre=ci=pi=to=sa=men=te|7 +prendendo|pren=den=do|7 +prendersi|pren=der=si|7 +prepotenti|pre=po=ten=ti|7 +presero|pre=se=ro|7 +pretesto|pre=te=sto|7 +privato|pri=va=to|7 +prometto|pro=met=to|7 +proporzione|pro=por=zio=ne|7 +proteste|pro=te=ste|7 +provato|pro=va=to|7 +proverbio|pro=ver=bio|7 +provvidenza|prov=vi=den=za|7 +provvisioni|prov=vi=sio=ni|7 +qualcheduna|qual=che=du=na|7 +Quanto|Quan=to|7 +raccontava|rac=con=ta=va|7 +ragazza|ra=gaz=za|7 +rapidamente|ra=pi=da=men=te|7 +religione|re=li=gio=ne|7 +resistenza|re=si=sten=za|7 +ricchezze|ric=chez=ze|7 +ricevuti|ri=ce=vu=ti|7 +Richelieu|Ri=che=lieu|7 +richiesta|ri=chie=sta|7 +richiuse|ri=chiu=se|7 +rimanesse|ri=ma=nes=se|7 +rimembranza|ri=mem=bran=za|7 +rispettosamente|ri=spet=to=sa=men=te|7 +ritornò|ri=tor=nò|7 +riuscisse|riu=scis=se|7 +salvar|sal=var|7 +santissima|san=tis=si=ma|7 +sapevate|sa=pe=va=te|7 +sarebber|sa=reb=ber|7 +sbalordito|sba=lor=di=to|7 +scappata|scap=pa=ta|7 +scellerata|scel=le=ra=ta|7 +scellerato|scel=le=ra=to|7 +scodella|sco=del=la|7 +scoprir|sco=prir|7 +scorrer|scor=rer|7 +sebbene|seb=be=ne|7 +seggiolone|seg=gio=lo=ne|7 +segreti|se=gre=ti|7 +separazione|se=pa=ra=zio=ne|7 +sereno|se=re=no|7 +servitù|ser=vi=tù|7 +solite|so=li=te|7 +sparsi|spar=si|7 +spedito|spe=di=to|7 +sperato|spe=ra=to|7 +sporta|spor=ta|7 +stagione|sta=gio=ne|7 +stanco|stan=co|7 +storico|sto=ri=co|7 +strane|stra=ne|7 +strumento|stru=men=to|7 +supporre|sup=por=re|7 +taglio|ta=glio|7 +tenerli|te=ner=li|7 +tentativo|ten=ta=ti=vo|7 +terrena|ter=re=na|7 +tiranni|ti=ran=ni|7 +toccasse|toc=cas=se|7 +toccata|toc=ca=ta|7 +tocchi|toc=chi|7 +tormento|tor=men=to|7 +tranquillamente|tran=quil=la=men=te|7 +trattandosi|trat=tan=do=si|7 +trattava|trat=ta=va|7 +trionfo|trion=fo|7 +troppa|trop=pa|7 +trovarla|tro=var=la|7 +universale|uni=ver=sa=le|7 +uscivan|usci=van|7 +uscivano|usci=va=no|7 +veleno|ve=le=no|7 +Veramente|Ve=ra=men=te|7 +viandante|vian=dan=te|7 +villaggio|vil=lag=gio|7 +vincere|vin=ce=re|7 +vivamente|vi=va=men=te|7 +viventi|vi=ven=ti|7 +vocazione|vo=ca=zio=ne|7 +voltata|vol=ta=ta|7 +abbandonata|ab=ban=do=na=ta|6 +abbandono|ab=ban=do=no|6 +accenna|ac=cen=na|6 +accresciuta|ac=cre=sciu=ta|6 +acquisto|ac=qui=sto|6 +afferma|af=fer=ma|6 +affetti|af=fet=ti|6 +affezione|af=fe=zio=ne|6 +afflitto|af=flit=to|6 +aggiungeva|ag=giun=ge=va|6 +agitato|agi=ta=to|6 +aiutante|aiu=tan=te|6 +aiutar|aiu=tar|6 +Alcuni|Al=cu=ni|6 +allegro|al=le=gro|6 +altrettanto|al=tret=tan=to|6 +alzandosi|al=zan=do=si|6 +alzarsi|al=zar=si|6 +alzata|al=za=ta|6 +ammalato|am=ma=la=to|6 +ammazzato|am=maz=za=to|6 +ammirazione|am=mi=ra=zio=ne|6 +amorevole|amo=re=vo=le|6 +Andando|An=dan=do|6 +andarci|an=dar=ci|6 +andarne|an=dar=ne|6 +anderà|an=de=rà|6 +andirivieni|an=di=ri=vie=ni|6 +andito|an=di=to|6 +angosce|an=go=sce|6 +annata|an=na=ta|6 +antecedenti|an=te=ce=den=ti|6 +appoggiato|ap=pog=gia=to|6 +apriva|apri=va|6 +argomenti|ar=go=men=ti|6 +arrivasse|ar=ri=vas=se|6 +arrivo|ar=ri=vo|6 +Aspetta|Aspet=ta|6 +aspettarsi|aspet=tar=si|6 +assalto|as=sal=to|6 +atroce|atro=ce|6 +avanza|avan=za|6 +avergli|aver=gli|6 +avranno|avran=no|6 +Avrebbe|Avreb=be|6 +avvenimento|av=ve=ni=men=to|6 +avvenne|av=ven=ne|6 +avventure|av=ven=tu=re|6 +avvenuto|av=ve=nu=to|6 +avvezza|av=vez=za|6 +avvisi|av=vi=si|6 +azione|azio=ne|6 +azioni|azio=ni|6 +badava|ba=da=va|6 +barlume|bar=lu=me|6 +bastonate|ba=sto=na=te|6 +battente|bat=ten=te|6 +benedizione|be=ne=di=zio=ne|6 +biancheria|bian=che=ria|6 +bianchi|bian=chi|6 +birbante|bir=ban=te|6 +Bonaventura|Bo=na=ven=tu=ra|6 +Borromeo|Bor=ro=meo|6 +buttato|but=ta=to|6 +capace|ca=pa=ce|6 +capitato|ca=pi=ta=to|6 +carabina|ca=ra=bi=na|6 +carrozze|car=roz=ze|6 +casolare|ca=so=la=re|6 +cattiva|cat=ti=va|6 +cattivo|cat=ti=vo|6 +cercasse|cer=cas=se|6 +cerimonia|ce=ri=mo=nia|6 +chiamati|chia=ma=ti|6 +chiede|chie=de|6 +chiuder|chiu=der|6 +chiusi|chiu=si|6 +classe|clas=se|6 +cognizione|co=gni=zio=ne|6 +colonna|co=lon=na|6 +coltellaccio|col=tel=lac=cio|6 +commettere|com=met=te=re|6 +comparsa|com=par=sa|6 +confessare|con=fes=sa=re|6 +confratelli|con=fra=tel=li|6 +coperta|co=per=ta|6 +coppie|cop=pie|6 +costeggia|co=steg=gia|6 +covile|co=vi=le|6 +creature|crea=tu=re|6 +crediate|cre=dia=te|6 +cresciuta|cre=sciu=ta|6 +danaro|da=na=ro|6 +davano|da=va=no|6 +delirio|de=li=rio|6 +demonio|de=mo=nio|6 +deputati|de=pu=ta=ti|6 +deserto|de=ser=to|6 +desidèri|de=si=dè=ri|6 +diamine|dia=mi=ne|6 +dicevan|di=ce=van|6 +dicevo|di=ce=vo|6 +Dietro|Die=tro|6 +difetto|di=fet=to|6 +difficili|dif=fi=ci=li|6 +disciplina|di=sci=pli=na|6 +disegnato|di=se=gna=to|6 +disinvoltura|di=sin=vol=tu=ra|6 +disperata|di=spe=ra=ta|6 +disposta|di=spo=sta|6 +disposti|di=spo=sti|6 +divenne|di=ven=ne|6 +diversamente|di=ver=sa=men=te|6 +dobbiam|dob=biam|6 +dolorose|do=lo=ro=se|6 +domandando|do=man=dan=do|6 +domandato|do=man=da=to|6 +dottori|dot=to=ri|6 +dovete|do=ve=te|6 +Eccellenza|Ec=cel=len=za|6 +esempi|esem=pi|6 +esercizio|eser=ci=zio|6 +espediente|espe=dien=te|6 +esperienza|espe=rien=za|6 +evidenza|evi=den=za|6 +fabbrica|fab=bri=ca|6 +facciam|fac=ciam|6 +facciano|fac=cia=no|6 +fatiche|fa=ti=che|6 +felice|fe=li=ce|6 +fermato|fer=ma=to|6 +figlio|fi=glio|6 +figurava|fi=gu=ra=va|6 +figuri|fi=gu=ri|6 +Filippo|Fi=lip=po|6 +flagello|fla=gel=lo|6 +fornaio|for=na=io|6 +fosser|fos=ser|6 +frequente|fre=quen=te|6 +funesto|fu=ne=sto|6 +funzioni|fun=zio=ni|6 +gemiti|ge=mi=ti|6 +genitori|ge=ni=to=ri|6 +godeva|go=de=va|6 +Gorgonzola|Gor=gon=zo=la|6 +gratitudine|gra=ti=tu=di=ne|6 +gridato|gri=da=to|6 +guardarono|guar=da=ro=no|6 +guardasse|guar=das=se|6 +guardate|guar=da=te|6 +guerre|guer=re|6 +ignoranza|igno=ran=za|6 +imbasciata|im=ba=scia=ta|6 +immaginava|im=ma=gi=na=va|6 +immobili|im=mo=bi=li|6 +impegni|im=pe=gni|6 +imposte|im=po=ste|6 +incerti|in=cer=ti|6 +incessante|in=ces=san=te|6 +incidentemente|in=ci=den=te=men=te|6 +incrociate|in=cro=cia=te|6 +indegnazione|in=de=gna=zio=ne|6 +indeterminato|in=de=ter=mi=na=to|6 +indicavano|in=di=ca=va=no|6 +indice|in=di=ce|6 +indomani|in=do=ma=ni|6 +indovinare|in=do=vi=na=re|6 +infermo|in=fer=mo|6 +informarsi|in=for=mar=si|6 +informazioni|in=for=ma=zio=ni|6 +inginocchioni|in=gi=noc=chio=ni|6 +insistenza|in=si=sten=za|6 +intelligenza|in=tel=li=gen=za|6 +interrotto|in=ter=rot=to|6 +intervalli|in=ter=val=li|6 +intesa|in=te=sa|6 +inutilmente|inu=til=men=te|6 +invase|in=va=se|6 +invitato|in=vi=ta=to|6 +larghe|lar=ghe|6 +lasciavano|la=scia=va=no|6 +lavori|la=vo=ri|6 +lecito|le=ci=to|6 +leggiera|leg=gie=ra|6 +levata|le=va=ta|6 +macchina|mac=chi=na|6 +maggio|mag=gio|6 +maggiori|mag=gio=ri|6 +malandrini|ma=lan=dri=ni|6 +Malanotte|Ma=la=not=te|6 +malattia|ma=lat=tia|6 +mandarla|man=dar=la|6 +manico|ma=ni=co|6 +maravigliato|ma=ra=vi=glia=to|6 +maritati|ma=ri=ta=ti|6 +marmaglia|mar=ma=glia|6 +membra|mem=bra|6 +metterci|met=ter=ci|6 +metton|met=ton|6 +minacciare|mi=nac=cia=re|6 +minestra|mi=ne=stra|6 +misera|mi=se=ra|6 +Misericordia|Mi=se=ri=cor=dia|6 +montagna|mon=ta=gna|6 +mossero|mos=se=ro|6 +mostrava|mo=stra=va|6 +moveva|mo=ve=va|6 +obbligato|ob=bli=ga=to|6 +occorrenza|oc=cor=ren=za|6 +occupato|oc=cu=pa=to|6 +odioso|odio=so|6 +offerto|of=fer=to|6 +operazione|ope=ra=zio=ne|6 +opinion|opi=nion|6 +opposti|op=po=sti|6 +orgoglio|or=go=glio|6 +ottenuto|ot=te=nu=to|6 +ottobre|ot=to=bre|6 +padrona|pa=dro=na|6 +paletto|pa=let=to|6 +pallido|pal=li=do|6 +parata|pa=ra=ta|6 +parrocchia|par=roc=chia|6 +pazzie|paz=zie|6 +penitenza|pe=ni=ten=za|6 +penoso|pe=no=so|6 +pentimento|pen=ti=men=to|6 +perdeva|per=de=va|6 +perdonato|per=do=na=to|6 +perpetuo|per=pe=tuo|6 +perversità|per=ver=si=tà|6 +portamento|por=ta=men=to|6 +posato|po=sa=to|6 +possesso|pos=ses=so|6 +poverelli|po=ve=rel=li|6 +precauzioni|pre=cau=zio=ni|6 +pregando|pre=gan=do|6 +pregata|pre=ga=ta|6 +pregava|pre=ga=va|6 +pregherò|pre=ghe=rò|6 +premio|pre=mio|6 +prenderlo|pren=der=lo|6 +preparare|pre=pa=ra=re|6 +privilegi|pri=vi=le=gi|6 +probabilità|pro=ba=bi=li=tà|6 +proferì|pro=fe=rì|6 +propose|pro=po=se|6 +proprie|pro=prie|6 +provata|pro=va=ta|6 +provava|pro=va=va|6 +qualchedun|qual=che=dun|6 +quaranta|qua=ran=ta|6 +quindici|quin=di=ci|6 +racconterò|rac=con=te=rò|6 +radunati|ra=du=na=ti|6 +rammentava|ram=men=ta=va|6 +realmente|real=men=te|6 +renderà|ren=de=rà|6 +replicava|re=pli=ca=va|6 +restar|re=star|6 +ribaldi|ri=bal=di|6 +ricompensa|ri=com=pen=sa|6 +ricorrere|ri=cor=re=re|6 +ricoverata|ri=co=ve=ra=ta|6 +ricoverati|ri=co=ve=ra=ti|6 +ridotti|ri=dot=ti|6 +riferì|ri=fe=rì|6 +rifugio|ri=fu=gio|6 +rigore|ri=go=re|6 +riguardi|ri=guar=di|6 +rimaner|ri=ma=ner|6 +rimedi|ri=me=di|6 +rimise|ri=mi=se|6 +rinfusa|rin=fu=sa|6 +ringraziò|rin=gra=ziò|6 +ripeteva|ri=pe=te=va|6 +ripetuto|ri=pe=tu=to|6 +ripose|ri=po=se|6 +riposo|ri=po=so|6 +risentita|ri=sen=ti=ta|6 +risolutamente|ri=so=lu=ta=men=te|6 +risolvere|ri=sol=ve=re|6 +risponder|ri=spon=der|6 +risposto|ri=spo=sto|6 +ritenne|ri=ten=ne|6 +ritirata|ri=ti=ra=ta|6 +rivolse|ri=vol=se|6 +sagrestano|sa=gre=sta=no|6 +salire|sa=li=re|6 +salotto|sa=lot=to|6 +salvezza|sal=vez=za|6 +saperlo|sa=per=lo|6 +sappiate|sap=pia=te|6 +saremo|sa=re=mo|6 +sbocco|sboc=co|6 +scarsa|scar=sa|6 +scarsezza|scar=sez=za|6 +scherno|scher=no|6 +schiena|schie=na|6 +schiera|schie=ra|6 +sciagura|scia=gu=ra|6 +sciagurato|scia=gu=ra=to|6 +sconosciuti|sco=no=sciu=ti|6 +seguitava|se=gui=ta=va|6 +sembiante|sem=bian=te|6 +senato|se=na=to|6 +serietà|se=rie=tà|6 +serventi|ser=ven=ti|6 +Settala|Set=ta=la|6 +settimana|set=ti=ma=na|6 +sfuggita|sfug=gi=ta|6 +Sicché|Sic=ché|6 +signorile|si=gno=ri=le|6 +sincerità|sin=ce=ri=tà|6 +sincero|sin=ce=ro|6 +singhiozzi|sin=ghioz=zi|6 +singolari|sin=go=la=ri|6 +soggetti|sog=get=ti|6 +soggiorno|sog=gior=no|6 +soliti|so=li=ti|6 +somiglianti|so=mi=glian=ti|6 +sommissione|som=mis=sio=ne|6 +sorpreso|sor=pre=so|6 +sospensione|so=spen=sio=ne|6 +sospesa|so=spe=sa|6 +sospettoso|so=spet=to=so|6 +sostanze|so=stan=ze|6 +spagnolo|spa=gno=lo|6 +spalancò|spa=lan=cò|6 +speciale|spe=cia=le|6 +spiare|spia=re|6 +sposina|spo=si=na|6 +stomaco|sto=ma=co|6 +stradetta|stra=det=ta|6 +stretto|stret=to|6 +stringendo|strin=gen=do|6 +Subito|Su=bi=to|6 +successione|suc=ces=sio=ne|6 +supplichevole|sup=pli=che=vo=le|6 +tariffa|ta=rif=fa|6 +taschino|ta=schi=no|6 +temporale|tem=po=ra=le|6 +tenesse|te=nes=se|6 +tirata|ti=ra=ta|6 +tirato|ti=ra=to|6 +toccate|toc=ca=te|6 +tornerà|tor=ne=rà|6 +tragitto|tra=git=to|6 +trattenne|trat=ten=ne|6 +troncare|tron=ca=re|6 +troverò|tro=ve=rò|6 +tumulti|tu=mul=ti|6 +ultime|ul=ti=me|6 +valore|va=lo=re|6 +vantaggi|van=tag=gi|6 +vendere|ven=de=re|6 +vendette|ven=det=te|6 +venendo|ve=nen=do|6 +venerazione|ve=ne=ra=zio=ne|6 +vestiario|ve=stia=rio|6 +villani|vil=la=ni|6 +violento|vio=len=to|6 +volerlo|vo=ler=lo|6 +volontariamente|vo=lon=ta=ria=men=te|6 +vorrebbero|vor=reb=be=ro|6 +abbandonate|ab=ban=do=na=te|5 +abbandonati|ab=ban=do=na=ti|5 +abitudine|abi=tu=di=ne|5 +accertarsi|ac=cer=tar=si|5 +accettare|ac=cet=ta=re|5 +accolto|ac=col=to|5 +accoramento|ac=co=ra=men=to|5 +accorata|ac=co=ra=ta|5 +accorto|ac=cor=to|5 +adagino|ada=gi=no|5 +adempire|adem=pi=re|5 +adunque|adun=que|5 +affrettò|af=fret=tò|5 +agevole|age=vo=le|5 +aiutarci|aiu=tar=ci|5 +aiutarmi|aiu=tar=mi|5 +aiutava|aiu=ta=va|5 +alberi|al=be=ri|5 +allegrezza|al=le=grez=za|5 +alloggiare|al=log=gia=re|5 +alloggio|al=log=gio|5 +alzate|al=za=te|5 +ammessa|am=mes=sa|5 +amorevolmente|amo=re=vol=men=te|5 +angherie|an=ghe=rie|5 +animata|ani=ma=ta|5 +ansante|an=san=te|5 +apparizione|ap=pa=ri=zio=ne|5 +approvare|ap=pro=va=re|5 +aprite|apri=te|5 +ardire|ar=di=re|5 +ardito|ar=di=to|5 +argento|ar=gen=to|5 +argomentare|ar=go=men=ta=re|5 +Arrivò|Ar=ri=vò|5 +artigli|ar=ti=gli|5 +ascoltato|ascol=ta=to|5 +ascoltatori|ascol=ta=to=ri|5 +aspettate|aspet=ta=te|5 +assistenza|as=si=sten=za|5 +assistere|as=si=ste=re|5 +assunto|as=sun=to|5 +attaccata|at=tac=ca=ta|5 +attaccava|at=tac=ca=va|5 +attenta|at=ten=ta|5 +attraversò|at=tra=ver=sò|5 +augùri|au=gù=ri|5 +avermi|aver=mi|5 +avremo|avre=mo|5 +avvertito|av=ver=ti=to|5 +avviandosi|av=vian=do=si|5 +avvicinava|av=vi=ci=na=va|5 +badare|ba=da=re|5 +bagattella|ba=gat=tel=la|5 +baggiano|bag=gia=no|5 +bastante|ba=stan=te|5 +bastate|ba=sta=te|5 +berlinghe|ber=lin=ghe|5 +bestione|be=stio=ne|5 +bisaccia|bi=sac=cia|5 +bravacci|bra=vac=ci|5 +buttar|but=tar|5 +buttarsi|but=tar=si|5 +buttata|but=ta=ta|5 +buttati|but=ta=ti|5 +cacciarsi|cac=ciar=si|5 +calcagni|cal=ca=gni|5 +calzoni|cal=zo=ni|5 +cambiamento|cam=bia=men=to|5 +canale|ca=na=le|5 +cantare|can=ta=re|5 +cantonate|can=to=na=te|5 +capitale|ca=pi=ta=le|5 +capitare|ca=pi=ta=re|5 +capitolo|ca=pi=to=lo|5 +capricci|ca=pric=ci|5 +castellaccio|ca=stel=lac=cio|5 +castellano|ca=stel=la=no|5 +cercatore|cer=ca=to=re|5 +cercherò|cer=che=rò|5 +chiarezza|chia=rez=za|5 +chiarore|chia=ro=re|5 +ciarlare|ciar=la=re|5 +ciascuno|cia=scu=no|5 +cinquecento|cin=que=cen=to|5 +circonvicini|cir=con=vi=ci=ni|5 +citato|ci=ta=to|5 +civile|ci=vi=le|5 +collegio|col=le=gio|5 +Coloro|Co=lo=ro|5 +combattimento|com=bat=ti=men=to|5 +comincia|co=min=cia|5 +commossa|com=mos=sa|5 +comparir|com=pa=rir|5 +compariva|com=pa=ri=va|5 +compunzione|com=pun=zio=ne|5 +conciato|con=cia=to|5 +condiscendenza|con=di=scen=den=za|5 +conduce|con=du=ce|5 +condurlo|con=dur=lo|5 +conduttore|con=dut=to=re|5 +conoscenza|co=no=scen=za|5 +conoscete|co=no=sce=te|5 +consegnò|con=se=gnò|5 +conserva|con=ser=va|5 +contado|con=ta=do|5 +contemporanei|con=tem=po=ra=nei|5 +continuare|con=ti=nua=re|5 +convenga|con=ven=ga|5 +conveniente|con=ve=nien=te|5 +conversa|con=ver=sa|5 +Convien|Con=vien|5 +conviene|con=vie=ne|5 +coperto|co=per=to|5 +cordiale|cor=dia=le|5 +cordone|cor=do=ne|5 +corporale|cor=po=ra=le|5 +corredo|cor=re=do|5 +correvano|cor=re=va=no|5 +corrispondenza|cor=ri=spon=den=za|5 +corrono|cor=ro=no|5 +costernazione|co=ster=na=zio=ne|5 +costretti|co=stret=ti|5 +Costui|Co=stui|5 +crederlo|cre=der=lo|5 +credette|cre=det=te|5 +cresceva|cre=sce=va|5 +crocchi|croc=chi|5 +curiose|cu=rio=se|5 +curioso|cu=rio=so|5 +deciso|de=ci=so|5 +decoro|de=co=ro|5 +desolazione|de=so=la=zio=ne|5 +digiuno|di=giu=no|5 +dimenticando|di=men=ti=can=do|5 +dipende|di=pen=de|5 +diremo|di=re=mo|5 +direte|di=re=te|5 +direttamente|di=ret=ta=men=te|5 +disastro|di=sa=stro|5 +disparve|di=spar=ve|5 +dispensa|di=spen=sa|5 +disperato|di=spe=ra=to|5 +dispetti|di=spet=ti|5 +disputa|di=spu=ta|5 +dissero|dis=se=ro|5 +distinte|di=stin=te|5 +distinto|di=stin=to|5 +diveniva|di=ve=ni=va|5 +diventa|di=ven=ta|5 +divertimento|di=ver=ti=men=to|5 +diviato|di=via=to|5 +dolcezza|dol=cez=za|5 +domenica|do=me=ni=ca|5 +dormir|dor=mir|5 +doveri|do=ve=ri|5 +dovettero|do=vet=te=ro|5 +dovrebbero|do=vreb=be=ro|5 +dubitare|du=bi=ta=re|5 +dubitarne|du=bi=tar=ne|5 +durata|du=ra=ta|5 +educazione|edu=ca=zio=ne|5 +enfasi|en=fa=si|5 +enorme|enor=me|5 +entrando|en=tran=do|5 +Entrati|En=tra=ti|5 +esclamava|escla=ma=va|5 +esporre|espor=re|5 +esprimere|espri=me=re|5 +esserci|es=ser=ci|5 +facciate|fac=cia=te|5 +famigliari|fa=mi=glia=ri|5 +famosa|fa=mo=sa|5 +fantasie|fan=ta=sie|5 +faranno|fa=ran=no|5 +favore|fa=vo=re|5 +febbre|feb=bre|5 +filatoio|fi=la=to=io|5 +foglio|fo=glio|5 +forzato|for=za=to|5 +francamente|fran=ca=men=te|5 +generazione|ge=ne=ra=zio=ne|5 +gentiluomini|gen=ti=luo=mi=ni|5 +Giacché|Giac=ché|5 +giacere|gia=ce=re|5 +giaceva|gia=ce=va|5 +gioventù|gio=ven=tù|5 +giudici|giu=di=ci|5 +godersi|go=der=si|5 +gomito|go=mi=to|5 +Gonzaga|Gon=za=ga|5 +governare|go=ver=na=re|5 +Governatore|Go=ver=na=to=re|5 +gravezza|gra=vez=za|5 +grillo|gril=lo|5 +grucce|gruc=ce|5 +guardi|guar=di|5 +guarire|gua=ri=re|5 +guarita|gua=ri=ta|5 +guastare|gua=sta=re|5 +immaginarsi|im=ma=gi=nar=si|5 +immaginazioni|im=ma=gi=na=zio=ni|5 +impannata|im=pan=na=ta|5 +impedire|im=pe=di=re|5 +imperatore|im=pe=ra=to=re|5 +imperioso|im=pe=rio=so|5 +impiccarli|im=pic=car=li|5 +inchinandosi|in=chi=nan=do=si|5 +incontrato|in=con=tra=to|5 +indegno|in=de=gno|5 +indicata|in=di=ca=ta|5 +indipendente|in=di=pen=den=te|5 +indole|in=do=le|5 +infelici|in=fe=li=ci|5 +informata|in=for=ma=ta|5 +Insieme|In=sie=me|5 +insolita|in=so=li=ta|5 +insolito|in=so=li=to|5 +intender|in=ten=der|5 +interamente|in=te=ra=men=te|5 +interessato|in=te=res=sa=to|5 +interna|in=ter=na|5 +intero|in=te=ro|5 +intervallo|in=ter=val=lo|5 +intese|in=te=se|5 +intimazione|in=ti=ma=zio=ne|5 +intrigo|in=tri=go|5 +inverno|in=ver=no|5 +istruzione|istru=zio=ne|5 +labbro|lab=bro|5 +lamentarsi|la=men=tar=si|5 +lamento|la=men=to|5 +Lascia|La=scia|5 +Lasciate|La=scia=te|5 +leggiero|leg=gie=ro|5 +levarsi|le=var=si|5 +liberalità|li=be=ra=li=tà|5 +liberazione|li=be=ra=zio=ne|5 +libreria|li=bre=ria|5 +livrea|li=vrea|5 +lontane|lon=ta=ne|5 +lucerna|lu=cer=na|5 +maestro|mae=stro|5 +mancherebbe|man=che=reb=be|5 +mandata|man=da=ta|5 +manderebbe|man=de=reb=be|5 +maniche|ma=ni=che|5 +manifesta|ma=ni=fe=sta|5 +mariti|ma=ri=ti|5 +materiale|ma=te=ria=le|5 +materie|ma=te=rie|5 +medico|me=di=co|5 +meriti|me=ri=ti|5 +metterle|met=ter=le|5 +mettevano|met=te=va=no|5 +micheletti|mi=che=let=ti|5 +miglio|mi=glio|5 +migliore|mi=glio=re|5 +militare|mi=li=ta=re|5 +minacciata|mi=nac=cia=ta|5 +ministro|mi=ni=stro|5 +minutamente|mi=nu=ta=men=te|5 +minuti|mi=nu=ti|5 +modesta|mo=de=sta|5 +modestia|mo=de=stia|5 +mollemente|mol=le=men=te|5 +montagne|mon=ta=gne|5 +moribondi|mo=ri=bon=di|5 +moriva|mo=ri=va|5 +mormorava|mor=mo=ra=va|5 +mortale|mor=ta=le|5 +mostra|mo=stra|5 +mostrare|mo=stra=re|5 +mostrò|mo=strò|5 +nascer|na=scer|5 +nascondere|na=scon=de=re|5 +nastro|na=stro|5 +noioso|no=io=so|5 +notabili|no=ta=bi=li|5 +novizio|no=vi=zio|5 +occasion|oc=ca=sion|5 +occuparsi|oc=cu=par=si|5 +occupata|oc=cu=pa=ta|5 +onesto|one=sto|5 +operare|ope=ra=re|5 +ordinaria|or=di=na=ria|5 +oscura|oscu=ra|5 +oscurità|oscu=ri=tà|5 +osservar|os=ser=var|5 +osterie|oste=rie|5 +ottener|ot=te=ner|5 +padron|pa=dron|5 +paesello|pae=sel=lo|5 +pallida|pal=li=da|5 +paniera|pa=nie=ra|5 +paniere|pa=nie=re|5 +parentado|pa=ren=ta=do|5 +pareti|pa=re=ti|5 +parlan|par=lan|5 +parlargli|par=lar=gli|5 +parroco|par=ro=co|5 +partir|par=tir|5 +passasse|pas=sas=se|5 +passavano|pas=sa=va=no|5 +passerà|pas=se=rà|5 +pativa|pa=ti=va|5 +pensieroso|pen=sie=ro=so|5 +Perciò|Per=ciò|5 +perdersi|per=der=si|5 +perdita|per=di=ta|5 +perfidia|per=fi=dia|5 +permesso|per=mes=so|5 +pezzetto|pez=zet=to|5 +piaciuto|pia=ciu=to|5 +piangendo|pian=gen=do|5 +piangeva|pian=ge=va|5 +pistola|pi=sto=la|5 +polveri|pol=ve=ri|5 +portici|por=ti=ci|5 +positiva|po=si=ti=va|5 +possano|pos=sa=no|5 +potranno|po=tran=no|5 +potrebbero|po=treb=be=ro|5 +poveretto|po=ve=ret=to|5 +Povero|Po=ve=ro|5 +pratico|pra=ti=co|5 +precise|pre=ci=se|5 +pregar|pre=gar|5 +preghiamo|pre=ghia=mo|5 +preparar|pre=pa=rar|5 +preparate|pre=pa=ra=te|5 +preparati|pre=pa=ra=ti|5 +prescritto|pre=scrit=to|5 +presentato|pre=sen=ta=to|5 +presentimento|pre=sen=ti=men=to|5 +pretesti|pre=te=sti|5 +prevedere|pre=ve=de=re|5 +processi|pro=ces=si|5 +proferir|pro=fe=rir|5 +proferite|pro=fe=ri=te|5 +proferito|pro=fe=ri=to|5 +proibito|proi=bi=to|5 +promettere|pro=met=te=re|5 +proporre|pro=por=re|5 +proposizioni|pro=po=si=zio=ni|5 +protegge|pro=teg=ge|5 +pudore|pu=do=re|5 +pulpito|pul=pi=to|5 +puntiglio|pun=ti=glio|5 +qualsivoglia|qual=si=vo=glia|5 +Quante|Quan=te|5 +quattrini|quat=tri=ni|5 +quattromila|quat=tro=mi=la|5 +questioni|que=stio=ni|5 +raccolti|rac=col=ti|5 +raccolto|rac=col=to|5 +raccontato|rac=con=ta=to|5 +ragguaglio|rag=gua=glio|5 +ragionamento|ra=gio=na=men=to|5 +rammarico|ram=ma=ri=co|5 +rapida|ra=pi=da|5 +rassegnazione|ras=se=gna=zio=ne|5 +rattenendo|rat=te=nen=do|5 +regnava|re=gna=va|5 +ricchi|ric=chi|5 +riceveva|ri=ce=ve=va|5 +richiede|ri=chie=de|5 +richiedeva|ri=chie=de=va|5 +riconosce|ri=co=no=sce|5 +riconosciuto|ri=co=no=sciu=to|5 +ricoverarsi|ri=co=ve=rar=si|5 +ridotto|ri=dot=to|5 +riferito|ri=fe=ri=to|5 +rimangono|ri=man=go=no|5 +rinchiusa|rin=chiu=sa|5 +rincorsa|rin=cor=sa|5 +riparo|ri=pa=ro|5 +risoluti|ri=so=lu=ti|5 +risparmio|ri=spar=mio|5 +rispondendo|ri=spon=den=do|5 +ritirandosi|ri=ti=ran=do=si|5 +ritirati|ri=ti=ra=ti|5 +ritratto|ri=trat=to|5 +riunirsi|riu=nir=si|5 +riuscito|riu=sci=to|5 +riverito|ri=ve=ri=to|5 +rivolgendosi|ri=vol=gen=do=si|5 +rovina|ro=vi=na|5 +rumoroso|ru=mo=ro=so|5 +santità|san=ti=tà|5 +sapessero|sa=pes=se=ro|5 +sapeste|sa=pe=ste|5 +sapienza|sa=pien=za|5 +saremmo|sa=rem=mo|5 +sbagliato|sba=glia=to|5 +sbalordimento|sba=lor=di=men=to|5 +scaletta|sca=let=ta|5 +scappa|scap=pa|5 +scelto|scel=to|5 +scendeva|scen=de=va|5 +scherzo|scher=zo|5 +schivare|schi=va=re|5 +sciocco|scioc=co|5 +scommessa|scom=mes=sa|5 +scomparso|scom=par=so|5 +scoperto|sco=per=to|5 +scorreva|scor=re=va|5 +scorso|scor=so|5 +scrittori|scrit=to=ri|5 +seggiola|seg=gio=la|5 +segreta|se=gre=ta|5 +semplici|sem=pli=ci|5 +Sentiamo|Sen=tia=mo|5 +sentirne|sen=tir=ne|5 +separati|se=pa=ra=ti|5 +servirsi|ser=vir=si|5 +sicure|si=cu=re|5 +signorina|si=gno=ri=na|5 +sistema|si=ste=ma|5 +smarrita|smar=ri=ta|5 +soddisfare|sod=di=sfa=re|5 +sofferto|sof=fer=to|5 +soffio|sof=fio|5 +soleva|so=le=va|5 +sollecitudine|sol=le=ci=tu=di=ne|5 +sospetta|so=spet=ta|5 +sospirando|so=spi=ran=do|5 +spadaio|spa=da=io|5 +sparge|spar=ge|5 +spaventati|spa=ven=ta=ti|5 +spaventato|spa=ven=ta=to|5 +sperando|spe=ran=do|5 +spettatori|spet=ta=to=ri|5 +spianata|spia=na=ta|5 +spiccava|spic=ca=va|5 +spiegazione|spie=ga=zio=ne|5 +spinta|spin=ta|5 +spinte|spin=te|5 +spinto|spin=to|5 +spirito|spi=ri=to|5 +sposare|spo=sa=re|5 +spropositi|spro=po=si=ti|5 +staffa|staf=fa|5 +stampa|stam=pa|5 +starci|star=ci|5 +statua|sta=tua|5 +stecconato|stec=co=na=to|5 +stendeva|sten=de=va|5 +stessero|stes=se=ro|5 +stiamo|stia=mo|5 +stizzosa|stiz=zo=sa|5 +stoppa|stop=pa|5 +striscia|stri=scia|5 +suggerimento|sug=ge=ri=men=to|5 +supplire|sup=pli=re|5 +susurro|su=sur=ro|5 +tacque|tac=que|5 +temendo|te=men=do|5 +temperata|tem=pe=ra=ta|5 +tempesta|tem=pe=sta|5 +tenete|te=ne=te|5 +tenevano|te=ne=va=no|5 +tentazione|ten=ta=zio=ne|5 +tentennando|ten=ten=nan=do|5 +tenuta|te=nu=ta|5 +tornasse|tor=nas=se|5 +Tornate|Tor=na=te|5 +tornavano|tor=na=va=no|5 +tornerò|tor=ne=rò|5 +torrente|tor=ren=te|5 +trasparire|tra=spa=ri=re|5 +trattar|trat=tar|5 +trattata|trat=ta=ta|5 +travaglio|tra=va=glio|5 +traverso|tra=ver=so|5 +travestito|tra=ve=sti=to|5 +troppe|trop=pe|5 +trovarmi|tro=var=mi|5 +troverebbe|tro=ve=reb=be|5 +troverete|tro=ve=re=te|5 +turbato|tur=ba=to|5 +ubbidito|ub=bi=di=to|5 +udienza|udien=za|5 +uditorio|udi=to=rio|5 +ufiziale|ufi=zia=le|5 +ufiziali|ufi=zia=li|5 +vagabondi|va=ga=bon=di|5 +vantarsi|van=tar=si|5 +vedessero|ve=des=se=ro|5 +vediamo|ve=dia=mo|5 +vengano|ven=ga=no|5 +viandanti|vian=dan=ti|5 +violenta|vio=len=ta|5 +violenti|vio=len=ti|5 +violenze|vio=len=ze|5 +volevo|vo=le=vo|5 +volgare|vol=ga=re|5 +vorranno|vor=ran=no|5 +abbandonar|ab=ban=do=nar|4 +abbandonato|ab=ban=do=na=to|4 +abbassando|ab=bas=san=do|4 +abbattuto|ab=bat=tu=to|4 +abboccamento|ab=boc=ca=men=to|4 +abitate|abi=ta=te|4 +abituale|abi=tua=le|4 +accadde|ac=cad=de|4 +accadere|ac=ca=de=re|4 +accadesse|ac=ca=des=se|4 +accaduta|ac=ca=du=ta|4 +accennar|ac=cen=nar|4 +accento|ac=cen=to|4 +accesi|ac=ce=si|4 +accettazione|ac=cet=ta=zio=ne|4 +acchiappar|ac=chiap=par|4 +acclamazioni|ac=cla=ma=zio=ni|4 +accomodar|ac=co=mo=dar|4 +accomodato|ac=co=mo=da=to|4 +accomodò|ac=co=mo=dò|4 +accompagnamento|ac=com=pa=gna=men=to|4 +accompagnata|ac=com=pa=gna=ta|4 +accompagnava|ac=com=pa=gna=va|4 +accorgesse|ac=cor=ges=se|4 +accorrevano|ac=cor=re=va=no|4 +accosta|ac=co=sta|4 +accudire|ac=cu=di=re|4 +acquietò|ac=quie=tò|4 +acquistata|ac=qui=sta=ta|4 +addormentò|ad=dor=men=tò|4 +affaccia|af=fac=cia|4 +affamati|af=fa=ma=ti|4 +afferra|af=fer=ra|4 +affettuosamente|af=fet=tuo=sa=men=te|4 +affidabilità|af=fi=da=bi=li=tà|4 +affine|af=fi=ne|4 +aggiunta|ag=giun=ta|4 +agnello|agnel=lo|4 +aiutato|aiu=ta=to|4 +aiuterà|aiu=te=rà|4 +alterato|al=te=ra=to|4 +alzati|al=za=ti|4 +alzatosi|al=za=to=si|4 +alzavano|al=za=va=no|4 +anderanno|an=de=ran=no|4 +anderemo|an=de=re=mo|4 +angosciosa|an=go=scio=sa|4 +annate|an=na=te|4 +annunziare|an=nun=zia=re|4 +ansioso|an=sio=so|4 +antichità|an=ti=chi=tà|4 +anticipatamente|an=ti=ci=pa=ta=men=te|4 +apertamente|aper=ta=men=te|4 +aperti|aper=ti|4 +apparitori|ap=pa=ri=to=ri|4 +apparve|ap=par=ve|4 +applausi|ap=plau=si|4 +applauso|ap=plau=so|4 +appoggiò|ap=pog=giò|4 +apprensione|ap=pren=sio=ne|4 +ardisce|ar=di=sce|4 +arrivarci|ar=ri=var=ci|4 +arrivi|ar=ri=vi|4 +arruffati|ar=ruf=fa=ti|4 +ascolti|ascol=ti|4 +aspettavano|aspet=ta=va=no|4 +assassino|as=sas=si=no|4 +assegnamento|as=se=gna=men=to|4 +assegnò|as=se=gnò|4 +assicurare|as=si=cu=ra=re|4 +astuzia|astu=zia|4 +atroci|atro=ci|4 +attaccare|at=tac=ca=re|4 +attaccate|at=tac=ca=te|4 +attendeva|at=ten=de=va|4 +attentato|at=ten=ta=to|4 +atterrita|at=ter=ri=ta|4 +atterrito|at=ter=ri=to|4 +attirare|at=ti=ra=re|4 +attrattiva|at=trat=ti=va|4 +attribuivano|at=tri=bu=i=va=no|4 +augurio|au=gu=rio|4 +aumento|au=men=to|4 +autorevole|au=to=re=vo=le|4 +averle|aver=le|4 +avventore|av=ven=to=re|4 +avventori|av=ven=to=ri|4 +avversario|av=ver=sa=rio|4 +avvertendo|av=ver=ten=do|4 +avvertimento|av=ver=ti=men=to|4 +avviarono|av=via=ro=no|4 +avvicinò|av=vi=ci=nò|4 +avvide|av=vi=de|4 +balzano|bal=za=no|4 +banditi|ban=di=ti|4 +barcaiolo|bar=ca=io=lo|4 +barocciaio|ba=roc=cia=io|4 +bastasse|ba=stas=se|4 +bastato|ba=sta=to|4 +battello|bat=tel=lo|4 +battere|bat=te=re|4 +benedetti|be=ne=det=ti|4 +Benissimo|Be=nis=si=mo|4 +benone|be=no=ne|4 +biancastra|bian=ca=stra|4 +biglietto|bi=gliet=to|4 +bisognerebbe|bi=so=gne=reb=be|4 +bisognerà|bi=so=gne=rà|4 +borbottando|bor=bot=tan=do|4 +bordone|bor=do=ne|4 +branco|bran=co|4 +bricconerie|bric=co=ne=rie|4 +briglia|bri=glia|4 +brillanti|bril=lan=ti|4 +brindisi|brin=di=si|4 +brontolare|bron=to=la=re|4 +bubbone|bub=bo=ne|4 +bucato|bu=ca=to|4 +buttasse|but=tas=se|4 +cabale|ca=ba=le|4 +caduta|ca=du=ta|4 +caldamente|cal=da=men=te|4 +cambiar|cam=biar|4 +cambiato|cam=bia=to|4 +Cammina|Cam=mi=na|4 +campanile|cam=pa=ni=le|4 +cancello|can=cel=lo|4 +canizie|ca=ni=zie|4 +capriccio|ca=pric=cio|4 +Cardano|Car=da=no|4 +cassetta|cas=set=ta|4 +catena|ca=te=na|4 +catenaccio|ca=te=nac=cio|4 +cavalcatura|ca=val=ca=tu=ra|4 +cavalier|ca=va=lier|4 +celebri|ce=le=bri|4 +cenere|ce=ne=re|4 +centinaia|cen=ti=na=ia|4 +cercarla|cer=car=la|4 +chiamarsi|chia=mar=si|4 +chiedesse|chie=des=se|4 +chirurgo|chi=rur=go|4 +clamorosa|cla=mo=ro=sa|4 +classi|clas=si|4 +colazione|co=la=zio=ne|4 +collana|col=la=na|4 +coltello|col=tel=lo|4 +comandava|co=man=da=va|4 +combattere|com=bat=te=re|4 +cominciar|co=min=ciar|4 +cominciasse|co=min=cias=se|4 +commissari|com=mis=sa=ri|4 +comoda|co=mo=da|4 +comparso|com=par=so|4 +compassionevole|com=pas=sio=ne=vo=le|4 +compir|com=pir|4 +compire|com=pi=re|4 +comprar|com=prar|4 +comprendere|com=pren=de=re|4 +comunemente|co=mu=ne=men=te|4 +comunicare|co=mu=ni=ca=re|4 +comunque|co=mun=que|4 +concertato|con=cer=ta=to|4 +concerto|con=cer=to|4 +condottiere|con=dot=tie=re|4 +condottieri|con=dot=tie=ri|4 +condur|con=dur|4 +confermò|con=fer=mò|4 +confessione|con=fes=sio=ne|4 +conforme|con=for=me|4 +congettura|con=get=tu=ra|4 +conoscerlo|co=no=scer=lo|4 +conoscessero|co=no=sces=se=ro|4 +conosciuta|co=no=sciu=ta|4 +conosciute|co=no=sciu=te|4 +considerare|con=si=de=ra=re|4 +consolare|con=so=la=re|4 +consuetudine|con=sue=tu=di=ne|4 +consuetudini|con=sue=tu=di=ni|4 +consulta|con=sul=ta|4 +contadina|con=ta=di=na|4 +contatto|con=tat=to|4 +contemplar|con=tem=plar|4 +contemplare|con=tem=pla=re|4 +contenersi|con=te=ner=si|4 +continuando|con=ti=nuan=do|4 +contraccambio|con=trac=cam=bio|4 +contratto|con=trat=to|4 +convalescenti|con=va=le=scen=ti|4 +convenienza|con=ve=nien=za|4 +conventi|con=ven=ti|4 +convenuto|con=ve=nu=to|4 +convitati|con=vi=ta=ti|4 +copriva|co=pri=va|4 +corbellerie|cor=bel=le=rie|4 +cordicella|cor=di=cel=la|4 +costanza|co=stan=za|4 +costretta|co=stret=ta|4 +costume|co=stu=me|4 +costumi|co=stu=mi|4 +credendo|cre=den=do|4 +crederei|cre=de=rei|4 +credesse|cre=des=se|4 +Credete|Cre=de=te|4 +credulità|cre=du=li=tà|4 +cresce|cre=sce|4 +criminale|cri=mi=na=le|4 +critiche|cri=ti=che|4 +crocicchio|cro=cic=chio|4 +cugini|cu=gi=ni|4 +custode|cu=sto=de|4 +custodia|cu=sto=dia|4 +dandogli|dan=do=gli|4 +datogli|da=to=gli|4 +decisione|de=ci=sio=ne|4 +deliberazione|de=li=be=ra=zio=ne|4 +Delrio|Del=rio|4 +deplorabile|de=plo=ra=bi=le|4 +deposizione|de=po=si=zio=ne|4 +descriver|de=scri=ver|4 +deserta|de=ser=ta|4 +desidera|de=si=de=ra|4 +desiderabile|de=si=de=ra=bi=le|4 +desiderata|de=si=de=ra=ta|4 +desolata|de=so=la=ta|4 +destinata|de=sti=na=ta|4 +destinate|de=sti=na=te|4 +destinati|de=sti=na=ti|4 +destro|de=stro|4 +determinato|de=ter=mi=na=to|4 +devono|de=vo=no|4 +diabolica|dia=bo=li=ca|4 +diavoleria|dia=vo=le=ria|4 +dicembre|di=cem=bre|4 +difese|di=fe=se|4 +diligenza|di=li=gen=za|4 +dimenticata|di=men=ti=ca=ta|4 +dimostrare|di=mo=stra=re|4 +dimostrazione|di=mo=stra=zio=ne|4 +dipinta|di=pin=ta|4 +diretta|di=ret=ta|4 +disagio|di=sa=gio|4 +disgrazie|di=sgra=zie|4 +disgustato|di=sgu=sta=to|4 +disordine|di=sor=di=ne|4 +dispersi|di=sper=si|4 +dispiaceri|di=spia=ce=ri|4 +dispiaceva|di=spia=ce=va|4 +disposizione|di=spo=si=zio=ne|4 +distinguer|di=stin=guer|4 +distintivo|di=stin=ti=vo|4 +distinzione|di=stin=zio=ne|4 +disturbare|di=stur=ba=re|4 +Ditemi|Di=te=mi|4 +diventare|di=ven=ta=re|4 +diventava|di=ven=ta=va|4 +dividersi|di=vi=der=si|4 +divisi|di=vi=si|4 +divozioni|di=vo=zio=ni|4 +dolcemente|dol=ce=men=te|4 +domandargli|do=man=dar=gli|4 +domandasse|do=man=das=se|4 +Domando|Do=man=do|4 +Domandò|Do=man=dò|4 +Domattina|Do=mat=ti=na|4 +dovessi|do=ves=si|4 +Dovete|Do=ve=te|4 +dovuta|do=vu=ta|4 +dugento|du=gen=to|4 +durava|du=ra=va|4 +eccesso|ec=ces=so|4 +eccezione|ec=ce=zio=ne|4 +ecclesiastiche|ec=cle=sia=sti=che|4 +ecclesiastici|ec=cle=sia=sti=ci|4 +editti|edit=ti|4 +EDIZIONE|EDI=ZIO=NE|4 +ELETTRONICA|ELET=TRO=NI=CA|4 +Enrico|En=ri=co|4 +entrano|en=tra=no|4 +entrarci|en=trar=ci|4 +entrati|en=tra=ti|4 +entrature|en=tra=tu=re|4 +eravate|era=va=te|4 +errore|er=ro=re|4 +esclamazione|escla=ma=zio=ne|4 +esclamazioni|escla=ma=zio=ni|4 +esemplare|esem=pla=re|4 +esordio|esor=dio|4 +esortazioni|esor=ta=zio=ni|4 +espiazione|espia=zio=ne|4 +espressa|espres=sa|4 +espressioni|espres=sio=ni|4 +essendosi|es=sen=do=si|4 +estate|esta=te|4 +esteriore|este=rio=re|4 +faccian|fac=cian|4 +facciata|fac=cia=ta|4 +facendogli|fa=cen=do=gli|4 +facendole|fa=cen=do=le|4 +facendosi|fa=cen=do=si|4 +facevo|fa=ce=vo|4 +facilità|fa=ci=li=tà|4 +fagottino|fa=got=ti=no|4 +fagotto|fa=got=to|4 +fallato|fal=la=to|4 +fallito|fal=li=to|4 +famigliare|fa=mi=glia=re|4 +fanciulla|fan=ciul=la|4 +fandonie|fan=do=nie|4 +farine|fa=ri=ne|4 +fatevi|fa=te=vi|4 +ferita|fe=ri=ta|4 +ferite|fe=ri=te|4 +fermar|fer=mar|4 +fermavano|fer=ma=va=no|4 +fiamme|fiam=me|4 +fianchi|fian=chi|4 +fieramente|fie=ra=men=te|4 +finirla|fi=nir=la|4 +finiva|fi=ni=va|4 +fissando|fis=san=do|4 +fissare|fis=sa=re|4 +formare|for=ma=re|4 +formata|for=ma=ta|4 +formola|for=mo=la|4 +fortezza|for=tez=za|4 +fortunati|for=tu=na=ti|4 +fortunato|for=tu=na=to|4 +forzata|for=za=ta|4 +Francesi|Fran=ce=si|4 +fredda|fred=da|4 +fremito|fre=mi=to|4 +fresca|fre=sca|4 +freschi|fre=schi|4 +frutti|frut=ti|4 +fuggitivi|fug=gi=ti=vi|4 +fuochi|fuo=chi|4 +furfanti|fur=fan=ti|4 +gabbia|gab=bia|4 +gelosia|ge=lo=sia|4 +gentile|gen=ti=le|4 +Germania|Ger=ma=nia|4 +ghiaccio|ghiac=cio|4 +giocare|gio=ca=re|4 +giovane|gio=va=ne|4 +giovedì|gio=ve=dì|4 +giravano|gi=ra=va=no|4 +giudice|giu=di=ce|4 +giugno|giu=gno|4 +giungendo|giun=gen=do|4 +giunte|giun=te|4 +gomita|go=mi=ta|4 +gradita|gra=di=ta|4 +gradito|gra=di=to|4 +grembiule|grem=biu=le|4 +gridar|gri=dar|4 +gruppo|grup=po|4 +guardandosi|guar=dan=do=si|4 +guardarlo|guar=dar=lo|4 +guardarsi|guar=dar=si|4 +guardato|guar=da=to|4 +Guardava|Guar=da=va|4 +guarnigione|guar=ni=gio=ne|4 +guazzabuglio|guaz=za=bu=glio|4 +imbarcato|im=bar=ca=to|4 +imbrogliato|im=bro=glia=to|4 +immaginato|im=ma=gi=na=to|4 +immagino|im=ma=gi=no|4 +immediato|im=me=dia=to|4 +impadronirsene|im=pa=dro=nir=se=ne|4 +impedimenti|im=pe=di=men=ti|4 +impicciata|im=pic=cia=ta|4 +imponeva|im=po=ne=va|4 +importanti|im=por=tan=ti|4 +imposto|im=po=sto|4 +impulso|im=pul=so|4 +incamminato|in=cam=mi=na=to|4 +incantata|in=can=ta=ta|4 +incantato|in=can=ta=to|4 +incaricato|in=ca=ri=ca=to|4 +incettatori|in=cet=ta=to=ri|4 +inchinò|in=chi=nò|4 +inclinato|in=cli=na=to|4 +incomodi|in=co=mo=di|4 +inconveniente|in=con=ve=nien=te|4 +indicare|in=di=ca=re|4 +indispettito|in=di=spet=ti=to|4 +indizi|in=di=zi|4 +Infatti|In=fat=ti|4 +inferiore|in=fe=rio=re|4 +infermeria|in=fer=me=ria|4 +inferriate|in=fer=ria=te|4 +infette|in=fet=te|4 +informare|in=for=ma=re|4 +informazione|in=for=ma=zio=ne|4 +ingegnavano|in=ge=gna=va=no|4 +ingegni|in=ge=gni|4 +inquieta|in=quie=ta|4 +insegnare|in=se=gna=re|4 +insegnarmi|in=se=gnar=mi|4 +intere|in=te=re|4 +interessi|in=te=res=si|4 +interrogato|in=ter=ro=ga=to|4 +interrogazione|in=ter=ro=ga=zio=ne|4 +interrotte|in=ter=rot=te|4 +intoppo|in=top=po|4 +intrinsichezza|in=trin=si=chez=za|4 +introdotto|in=tro=dot=to|4 +iracondo|ira=con=do|4 +irregolare|ir=re=go=la=re|4 +irritato|ir=ri=ta=to|4 +ispecie|ispe=cie|4 +istantanea|istan=ta=nea|4 +istante|istan=te|4 +istinto|istin=to|4 +lasciamo|la=scia=mo|4 +lasciarli|la=sciar=li|4 +lasciarsi|la=sciar=si|4 +lascino|la=sci=no|4 +lavorar|la=vo=rar|4 +legate|le=ga=te|4 +leggieri|leg=gie=ri|4 +lenzoli|len=zo=li|4 +levandosi|le=van=do=si|4 +Lombardia|Lom=bar=dia|4 +lungamente|lun=ga=men=te|4 +Macario|Ma=ca=rio|4 +macchia|mac=chia|4 +madrina|ma=dri=na|4 +malandrino|ma=lan=dri=no|4 +mancaron|man=ca=ron|4 +mancasse|man=cas=se|4 +mancavan|man=ca=van|4 +mancavano|man=ca=va=no|4 +mandarlo|man=dar=lo|4 +mandati|man=da=ti|4 +manderò|man=de=rò|4 +mangiato|man=gia=to|4 +manichini|ma=ni=chi=ni|4 +manifesto|ma=ni=fe=sto|4 +manoscritti|ma=no=scrit=ti|4 +maraviglie|ma=ra=vi=glie|4 +Marchese|Mar=che=se|4 +maritarsi|ma=ri=tar=si|4 +massima|mas=si=ma|4 +materiali|ma=te=ria=li|4 +medesime|me=de=si=me|4 +meritava|me=ri=ta=va|4 +meschini|me=schi=ni|4 +meschino|me=schi=no|4 +mescolando|me=sco=lan=do|4 +mettergli|met=ter=gli|4 +metterti|met=ter=ti|4 +mettono|met=to=no|4 +minacciati|mi=nac=cia=ti|4 +minacciava|mi=nac=cia=va|4 +ministri|mi=ni=stri|4 +missione|mis=sio=ne|4 +misteri|mi=ste=ri|4 +mobili|mo=bi=li|4 +moderna|mo=der=na|4 +moderni|mo=der=ni|4 +Mondella|Mon=del=la|4 +montanari|mon=ta=na=ri|4 +mucchietto|muc=chiet=to|4 +muraglie|mu=ra=glie|4 +mutazione|mu=ta=zio=ne|4 +nacque|nac=que|4 +nascita|na=sci=ta|4 +nascose|na=sco=se|4 +notato|no=ta=to|4 +nuvole|nu=vo=le|4 +obbligati|ob=bli=ga=ti|4 +occhietti|oc=chiet=ti|4 +occupava|oc=cu=pa=va|4 +occupazione|oc=cu=pa=zio=ne|4 +occupazioni|oc=cu=pa=zio=ni|4 +Olivares|Oli=va=res|4 +operazioni|ope=ra=zio=ni|4 +opportunità|op=por=tu=ni=tà|4 +orazione|ora=zio=ne|4 +orazioni|ora=zio=ni|4 +ordina|or=di=na|4 +ordinari|or=di=na=ri|4 +ordinato|or=di=na=to|4 +orecchie|orec=chie|4 +origine|ori=gi=ne|4 +orizzonte|oriz=zon=te|4 +orribili|or=ri=bi=li|4 +oscuri|oscu=ri|4 +ostinato|osti=na=to|4 +ottiene|ot=tie=ne|4 +pacatezza|pa=ca=tez=za|4 +paiolo|pa=io=lo|4 +palazzi|pa=laz=zi|4 +palese|pa=le=se|4 +parare|pa=ra=re|4 +parevan|pa=re=van|4 +parlano|par=la=no|4 +parlarle|par=lar=le|4 +parlasse|par=las=se|4 +parliamo|par=lia=mo|4 +passavan|pas=sa=van|4 +passeggiera|pas=seg=gie=ra|4 +pecora|pe=co=ra|4 +pecuniaria|pe=cu=nia=ria|4 +pendeva|pen=de=va|4 +pensassero|pen=sas=se=ro|4 +Pensate|Pen=sa=te|4 +penserà|pen=se=rà|4 +penserò|pen=se=rò|4 +pensiate|pen=sia=te|4 +pensierosa|pen=sie=ro=sa|4 +pentirsi|pen=tir=si|4 +pentito|pen=ti=to|4 +percorrere|per=cor=re=re|4 +percosse|per=cos=se|4 +perdona|per=do=na|4 +pericolosa|pe=ri=co=lo=sa|4 +periodo|pe=rio=do|4 +persuasi|per=sua=si|4 +pesciaiolo|pe=scia=io=lo|4 +piante|pian=te|4 +picchiò|pic=chiò|4 +piccoli|pic=co=li|4 +pienamente|pie=na=men=te|4 +pietosa|pie=to=sa|4 +Pietro|Pie=tro|4 +piglio|pi=glio|4 +pioggia|piog=gia|4 +poggio|pog=gio|4 +polpette|pol=pet=te|4 +porcheria|por=che=ria|4 +porpora|por=po=ra|4 +portamenti|por=ta=men=ti|4 +portasse|por=tas=se|4 +portavano|por=ta=va=no|4 +porterà|por=te=rà|4 +porzione|por=zio=ne|4 +posare|po=sa=re|4 +positive|po=si=ti=ve|4 +positivo|po=si=ti=vo|4 +Possibile|Pos=si=bi=le|4 +possibili|pos=si=bi=li|4 +posteri|po=ste=ri|4 +potenti|po=ten=ti|4 +poterla|po=ter=la|4 +poterle|po=ter=le|4 +poveretti|po=ve=ret=ti|4 +pranzo|pran=zo|4 +precipizio|pre=ci=pi=zio|4 +precisa|pre=ci=sa|4 +preciso|pre=ci=so|4 +predicava|pre=di=ca=va|4 +prelato|pre=la=to|4 +premuroso|pre=mu=ro=so|4 +prenderne|pren=der=ne|4 +Prendete|Pren=de=te|4 +prendo|pren=do|4 +prendon|pren=don|4 +preoccupazione|pre=oc=cu=pa=zio=ne|4 +prepotenza|pre=po=ten=za|4 +prescritte|pre=scrit=te|4 +presentarsi|pre=sen=tar=si|4 +pressante|pres=san=te|4 +pretendere|pre=ten=de=re|4 +prevenire|pre=ve=ni=re|4 +primato|pri=ma=to|4 +primavera|pri=ma=ve=ra|4 +principiò|prin=ci=piò|4 +princìpi|prin=cì=pi|4 +privata|pri=va=ta|4 +private|pri=va=te|4 +probabile|pro=ba=bi=le|4 +prodotto|pro=dot=to|4 +profonda|pro=fon=da|4 +profondamente|pro=fon=da=men=te|4 +progetto|pro=get=to|4 +prometter|pro=met=ter|4 +proponimento|pro=po=ni=men=to|4 +prossimi|pros=si=mi|4 +protofisico|pro=to=fi=si=co|4 +provarsi|pro=var=si|4 +provvedere|prov=ve=de=re|4 +provvedimenti|prov=ve=di=men=ti|4 +pubbliche|pub=bli=che|4 +pugnale|pu=gna=le|4 +pungente|pun=gen=te|4 +Purché|Pur=ché|4 +purché|pur=ché|4 +quarantina|qua=ran=ti=na|4 +quarto|quar=to|4 +quatto|quat=to|4 +Quelle|Quel=le|4 +raccomandato|rac=co=man=da=to|4 +raccomandò|rac=co=man=dò|4 +racconta|rac=con=ta|4 +raccontando|rac=con=tan=do|4 +raccontata|rac=con=ta=ta|4 +raddolcita|rad=dol=ci=ta|4 +radici|ra=di=ci|4 +radunarsi|ra=du=nar=si|4 +ragazzotto|ra=gaz=zot=to|4 +ragionamenti|ra=gio=na=men=ti|4 +rallegrarsi|ral=le=grar=si|4 +rallegro|ral=le=gro|4 +rallegrò|ral=le=grò|4 +rapido|ra=pi=do|4 +rasente|ra=sen=te|4 +recenti|re=cen=ti|4 +refrigerio|re=fri=ge=rio|4 +religiosi|re=li=gio=si|4 +rendevano|ren=de=va=no|4 +repentina|re=pen=ti=na|4 +residente|re=si=den=te|4 +reverenda|re=ve=ren=da|4 +ribaldo|ri=bal=do|4 +ribollimento|ri=bol=li=men=to|4 +ricavare|ri=ca=va=re|4 +ricevute|ri=ce=vu=te|4 +richiedere|ri=chie=de=re|4 +ricorda|ri=cor=da|4 +ricordarsi|ri=cor=dar=si|4 +ricordate|ri=cor=da=te|4 +ricordatevi|ri=cor=da=te=vi|4 +ricordati|ri=cor=da=ti|4 +ricorse|ri=cor=se|4 +ridente|ri=den=te|4 +ridere|ri=de=re|4 +ridotta|ri=dot=ta|4 +riempito|riem=pi=to|4 +rifinito|ri=fi=ni=to|4 +rimanendo|ri=ma=nen=do|4 +rimaste|ri=ma=ste|4 +rimembranze|ri=mem=bran=ze|4 +rimettersi|ri=met=ter=si|4 +rimetteva|ri=met=te=va|4 +ringraziamento|rin=gra=zia=men=to|4 +rinnovò|rin=no=vò|4 +ripensando|ri=pen=san=do|4 +riposto|ri=po=sto|4 +riputati|ri=pu=ta=ti|4 +riscosse|ri=scos=se|4 +riscosso|ri=scos=so|4 +riscotere|ri=sco=te=re|4 +risolversi|ri=sol=ver=si|4 +rispettosa|ri=spet=to=sa|4 +rispondevano|ri=spon=de=va=no|4 +ristretto|ri=stret=to|4 +ritegno|ri=te=gno|4 +ritenere|ri=te=ne=re|4 +ritirato|ri=ti=ra=to|4 +ritirava|ri=ti=ra=va|4 +ritornar|ri=tor=nar|4 +ritrovato|ri=tro=va=to|4 +riunione|riu=nio=ne|4 +riuscivano|riu=sci=va=no|4 +rivederci|ri=ve=der=ci|4 +rivedere|ri=ve=de=re|4 +Rivolta|Ri=vol=ta|4 +rompere|rom=pe=re|4 +rovescio|ro=ve=scio|4 +sacchi|sac=chi|4 +saccone|sac=co=ne|4 +saliva|sa=li=va|4 +salvamento|sal=va=men=to|4 +sapevan|sa=pe=van|4 +sapiente|sa=pien=te|4 +sappiam|sap=piam|4 +sbagliare|sba=glia=re|4 +sbandarsi|sban=dar=si|4 +sbigottito|sbi=got=ti=to|4 +scalino|sca=li=no|4 +scampata|scam=pa=ta|4 +scapestrato|sca=pe=stra=to|4 +scappando|scap=pan=do|4 +scarsi|scar=si|4 +scarso|scar=so|4 +scelleratezze|scel=le=ra=tez=ze|4 +schioppettata|schiop=pet=ta=ta|4 +sciolta|sciol=ta|4 +scompiglio|scom=pi=glio|4 +scoppiò|scop=piò|4 +scossa|scos=sa|4 +scritti|scrit=ti|4 +sedizione|se=di=zio=ne|4 +sedizioso|se=di=zio=so|4 +seduti|se=du=ti|4 +seguitando|se=gui=tan=do|4 +sensibile|sen=si=bi=le|4 +sentiamo|sen=tia=mo|4 +sentirono|sen=ti=ro=no|4 +sentirvi|sen=tir=vi|4 +sentirà|sen=ti=rà|4 +Sentiva|Sen=ti=va|4 +senton|sen=ton|4 +sentono|sen=to=no|4 +serrato|ser=ra=to|4 +serratura|ser=ra=tu=ra|4 +sessanta|ses=san=ta|4 +sfacciata|sfac=cia=ta|4 +sgarbatamente|sgar=ba=ta=men=te|4 +sgomberare|sgom=be=ra=re|4 +sguardi|sguar=di|4 +significato|si=gni=fi=ca=to|4 +sincera|sin=ce=ra|4 +sinceramente|sin=ce=ra=men=te|4 +singhiozzando|sin=ghioz=zan=do|4 +soccorsi|soc=cor=si|4 +sodaglia|so=da=glia|4 +soffiando|sof=fian=do|4 +soffrire|sof=fri=re|4 +soggetta|sog=get=ta|4 +solenni|so=len=ni|4 +sommessamente|som=mes=sa=men=te|4 +sommesso|som=mes=so|4 +sopraffatto|so=praf=fat=to|4 +soqquadro|soq=qua=dro|4 +sospettosa|so=spet=to=sa|4 +sostenuto|so=ste=nu=to|4 +sottana|sot=ta=na|4 +sottile|sot=ti=le|4 +sottrarre|sot=trar=re|4 +sottrarsi|sot=trar=si|4 +soverchiatore|so=ver=chia=to=re|4 +sovrastava|so=vra=sta=va|4 +spagnoli|spa=gno=li|4 +spalancati|spa=lan=ca=ti|4 +spalancato|spa=lan=ca=to|4 +spalla|spal=la|4 +sparger|spar=ger|4 +sparire|spa=ri=re|4 +sparsero|spar=se=ro|4 +spaventoso|spa=ven=to=so|4 +spenti|spen=ti|4 +spettacoli|spet=ta=co=li|4 +spettatore|spet=ta=to=re|4 +spiegar|spie=gar|4 +spiegava|spie=ga=va|4 +spingeva|spin=ge=va|4 +spirava|spi=ra=va|4 +spontanea|spon=ta=nea|4 +spontaneo|spon=ta=neo|4 +sprezzante|sprez=zan=te|4 +stabile|sta=bi=le|4 +staccato|stac=ca=to|4 +staccava|stac=ca=va|4 +stampate|stam=pa=te|4 +stampato|stam=pa=to|4 +stanchezza|stan=chez=za|4 +starebbe|sta=reb=be|4 +starsene|star=se=ne|4 +stentatamente|sten=ta=ta=men=te|4 +stranamente|stra=na=men=te|4 +strascico|stra=sci=co|4 +strascinato|stra=sci=na=to|4 +stringere|strin=ge=re|4 +stringeva|strin=ge=va|4 +struggeva|strug=ge=va|4 +studiato|stu=dia=to|4 +subitamente|su=bi=ta=men=te|4 +sudditi|sud=di=ti|4 +sudiciume|su=di=ciu=me|4 +sufficienti|suf=fi=cien=ti|4 +superba|su=per=ba|4 +sventurata|sven=tu=ra=ta|4 +sventurato|sven=tu=ra=to|4 +talento|ta=len=to|4 +taluno|ta=lu=no|4 +tasche|ta=sche|4 +temerità|te=me=ri=tà|4 +tenendosi|te=nen=do=si|4 +tenerla|te=ner=la|4 +tengono|ten=go=no|4 +tentar|ten=tar|4 +terminata|ter=mi=na=ta|4 +testimonianza|te=sti=mo=nian=za|4 +tintinnìo|tin=tin=nìo|4 +tirando|ti=ran=do|4 +titoli|ti=to=li|4 +toccherà|toc=che=rà|4 +tornerebbe|tor=ne=reb=be|4 +tradizione|tra=di=zio=ne|4 +tralci|tral=ci|4 +tranquillo|tran=quil=lo|4 +trattati|trat=ta=ti|4 +trecento|tre=cen=to|4 +tremando|tre=man=do|4 +tremava|tre=ma=va|4 +tremolante|tre=mo=lan=te|4 +tribolati|tri=bo=la=ti|4 +trionfale|trion=fa=le|4 +trionfante|trion=fan=te|4 +triste|tri=ste|4 +trovando|tro=van=do|4 +trovarci|tro=var=ci|4 +trovassero|tro=vas=se=ro|4 +truppa|trup=pa|4 +tuttora|tut=to=ra|4 +ubbidì|ub=bi=dì|4 +umiliato|umi=lia=to|4 +urgenti|ur=gen=ti|4 +urlare|ur=la=re|4 +usanza|usan=za|4 +usciron|usci=ron|4 +uscirono|usci=ro=no|4 +uscite|usci=te|4 +vadano|va=da=no|4 +vecchie|vec=chie|4 +vedono|ve=do=no|4 +vedute|ve=du=te|4 +velato|ve=la=to|4 +venefiche|ve=ne=fi=che|4 +vengon|ven=gon|4 +venirci|ve=nir=ci|4 +ventre|ven=tre|4 +verecondia|ve=re=con=dia|4 +Vergogna|Ver=go=gna|4 +verranno|ver=ran=no|4 +versacci|ver=sac=ci|4 +vestir|ve=stir|4 +viaggiare|viag=gia=re|4 +viaggiatori|viag=gia=to=ri|4 +visacci|vi=sac=ci|4 +visitare|vi=si=ta=re|4 +viveri|vi=ve=ri|4 +viveva|vi=ve=va|4 +vivevano|vi=ve=va=no|4 +vocabolo|vo=ca=bo=lo|4 +vogliano|vo=glia=no|4 +vollero|vol=le=ro|4 +voltarono|vol=ta=ro=no|4 +vorreste|vor=re=ste|4 +Vossignoria|Vos=si=gno=ria|4 +abbandonare|ab=ban=do=na=re|3 +abbandonerà|ab=ban=do=ne=rà|3 +abbatteva|ab=bat=te=va|3 +abbattuta|ab=bat=tu=ta|3 +Abbiam|Ab=biam|3 +Abbiamo|Ab=bia=mo|3 +Abbiate|Ab=bia=te|3 +abbondante|ab=bon=dan=te|3 +abbondanti|ab=bon=dan=ti|3 +abbracciato|ab=brac=cia=to|3 +abitava|abi=ta=va|3 +abitazione|abi=ta=zio=ne|3 +abitualmente|abi=tual=men=te|3 +accatto|ac=cat=to|3 +accennare|ac=cen=na=re|3 +acceso|ac=ce=so|3 +accettata|ac=cet=ta=ta|3 +accolse|ac=col=se|3 +accolti|ac=col=ti|3 +accomodata|ac=co=mo=da=ta|3 +accomodate|ac=co=mo=da=te|3 +accomodava|ac=co=mo=da=va|3 +accompagnate|ac=com=pa=gna=te|3 +accorgere|ac=cor=ge=re|3 +accorre|ac=cor=re|3 +accorso|ac=cor=so|3 +accostarsi|ac=co=star=si|3 +accostato|ac=co=sta=to|3 +accozzando|ac=coz=zan=do|3 +accrebbe|ac=creb=be|3 +accrescevano|ac=cre=sce=va=no|3 +adattarsi|adat=tar=si|3 +addormentata|ad=dor=men=ta=ta|3 +addormentato|ad=dor=men=ta=to|3 +adempirlo|adem=pir=lo|3 +adolescenza|ado=le=scen=za|3 +adoprar|ado=prar|3 +affaccendata|af=fac=cen=da=ta|3 +affacciarsi|af=fac=ciar=si|3 +affacciato|af=fac=cia=to|3 +affannata|af=fan=na=ta|3 +affannato|af=fan=na=to|3 +affannoso|af=fan=no=so|3 +affermare|af=fer=ma=re|3 +afferrò|af=fer=rò|3 +affezionata|af=fe=zio=na=ta|3 +afflitti|af=flit=ti|3 +affrontar|af=fron=tar|3 +affronto|af=fron=to|3 +aggiungevano|ag=giun=ge=va=no|3 +aggiunto|ag=giun=to|3 +agitata|agi=ta=ta|3 +agosto|ago=sto|3 +aiutarlo|aiu=tar=lo|3 +aiutarsi|aiu=tar=si|3 +aiutarvi|aiu=tar=vi|3 +aiutasse|aiu=tas=se|3 +aiutatemi|aiu=ta=te=mi|3 +aiutati|aiu=ta=ti|3 +alemanne|ale=man=ne|3 +Alessio|Ales=sio|3 +allegramente|al=le=gra=men=te|3 +allontanato|al=lon=ta=na=to|3 +allorché|al=lor=ché|3 +allungò|al=lun=gò|3 +alterata|al=te=ra=ta|3 +altrettanta|al=tret=tan=ta|3 +alzano|al=za=no|3 +Alzatevi|Al=za=te=vi|3 +amareggiato|ama=reg=gia=to|3 +ambrosiana|am=bro=sia=na|3 +amichevole|ami=che=vo=le|3 +ammalata|am=ma=la=ta|3 +amorevolezza|amo=re=vo=lez=za|3 +anderete|an=de=re=te|3 +angelo|an=ge=lo|3 +angoli|an=go=li|3 +angustia|an=gu=stia|3 +animale|ani=ma=le|3 +animava|ani=ma=va|3 +animosità|ani=mo=si=tà|3 +annoiato|an=no=ia=to|3 +anziane|an=zia=ne|3 +apparir|ap=pa=rir|3 +appoggiate|ap=pog=gia=te|3 +approva|ap=pro=va|3 +aprile|apri=le|3 +Archimede|Ar=chi=me=de|3 +ardente|ar=den=te|3 +armadio|ar=ma=dio|3 +arnese|ar=ne=se|3 +arrivaron|ar=ri=va=ron|3 +Arrivati|Ar=ri=va=ti|3 +arrogante|ar=ro=gan=te|3 +arrolati|ar=ro=la=ti|3 +arrossendo|ar=ros=sen=do|3 +arruffate|ar=ruf=fa=te|3 +arsione|ar=sio=ne|3 +artifizi|ar=ti=fi=zi|3 +artigiani|ar=ti=gia=ni|3 +asciugandosi|asciu=gan=do=si|3 +asciutto|asciut=to|3 +ascoltare|ascol=ta=re|3 +ascoltava|ascol=ta=va|3 +aspettano|aspet=ta=no|3 +Aspettate|Aspet=ta=te|3 +assediava|as=se=dia=va|3 +assegnata|as=se=gna=ta|3 +assegnati|as=se=gna=ti|3 +assegnato|as=se=gna=to|3 +associata|as=so=cia=ta|3 +assorto|as=sor=to|3 +attaccati|at=tac=ca=ti|3 +attestare|at=te=sta=re|3 +attirati|at=ti=ra=ti|3 +attivi|at=ti=vi|3 +attraversava|at=tra=ver=sa=va|3 +attrezzi|at=trez=zi|3 +audacia|au=da=cia|3 +avanzata|avan=za=ta|3 +avanzato|avan=za=to|3 +avanzò|avan=zò|3 +avessimo|aves=si=mo|3 +Avevano|Ave=va=no|3 +avevate|ave=va=te|3 +avveda|av=ve=da|3 +avvede|av=ve=de|3 +avvedesse|av=ve=des=se|3 +avvenisse|av=ve=nis=se|3 +avversione|av=ver=sio=ne|3 +avvertimenti|av=ver=ti=men=ti|3 +avvertir|av=ver=tir|3 +avviamento|av=via=men=to|3 +avviata|av=via=ta|3 +avviava|av=via=va|3 +avviavano|av=via=va=no|3 +avvicinandosi|av=vi=ci=nan=do=si|3 +avvicinavano|av=vi=ci=na=va=no|3 +babilonia|ba=bi=lo=nia|3 +baccano|bac=ca=no|3 +balbettò|bal=bet=tò|3 +baldanza|bal=dan=za|3 +balenò|ba=le=nò|3 +baluardo|ba=luar=do|3 +bandito|ban=di=to|3 +bandolo|ban=do=lo|3 +baracche|ba=rac=che|3 +bastando|ba=stan=do|3 +bastantemente|ba=stan=te=men=te|3 +bastanti|ba=stan=ti|3 +bastare|ba=sta=re|3 +bastata|ba=sta=ta|3 +Bellano|Bel=la=no|3 +benedett|be=ne=dett|3 +benedette|be=ne=det=te|3 +benedice|be=ne=di=ce|3 +benedizioni|be=ne=di=zio=ni|3 +benefizio|be=ne=fi=zio|3 +benevoli|be=ne=vo=li|3 +bestemmia|be=stem=mia|3 +bestemmie|be=stem=mie|3 +bicchier|bic=chier|3 +bicchieri|bic=chie=ri|3 +bisbiglio|bi=sbi=glio|3 +bisognare|bi=so=gna=re|3 +bisognasse|bi=so=gnas=se|3 +bisogni|bi=so=gni|3 +bisognosi|bi=so=gno=si|3 +borbottava|bor=bot=ta=va|3 +botticina|bot=ti=ci=na|3 +bravaccio|bra=vac=cio|3 +brigate|bri=ga=te|3 +bruciar|bru=ciar|3 +bruciare|bru=cia=re|3 +bubboni|bub=bo=ni|3 +bugiarda|bu=giar=da|3 +bullette|bul=let=te|3 +burbero|bur=be=ro|3 +cabala|ca=ba=la|3 +cadente|ca=den=te|3 +caduto|ca=du=to|3 +calcio|cal=cio|3 +cambiando|cam=bian=do|3 +cambiare|cam=bia=re|3 +cambiata|cam=bia=ta|3 +cambiò|cam=biò|3 +Camera|Ca=me=ra|3 +cameriere|ca=me=rie=re|3 +camicia|ca=mi=cia|3 +camminò|cam=mi=nò|3 +campane|cam=pa=ne|3 +campanelli|cam=pa=nel=li|3 +candela|can=de=la|3 +canonici|ca=no=ni=ci|3 +cantando|can=tan=do|3 +canzonare|can=zo=na=re|3 +canzone|can=zo=ne|3 +capaci|ca=pa=ci|3 +caparbietà|ca=par=bie=tà|3 +capello|ca=pel=lo|3 +capezzale|ca=pez=za=le|3 +capita|ca=pi=ta|3 +capitasse|ca=pi=tas=se|3 +capite|ca=pi=te|3 +capitò|ca=pi=tò|3 +capiva|ca=pi=va|3 +capolino|ca=po=li=no|3 +cappelletti|cap=pel=let=ti|3 +cappone|cap=po=ne|3 +capponi|cap=po=ni|3 +cappuccio|cap=puc=cio|3 +carboni|car=bo=ni|3 +carceri|car=ce=ri|3 +carceriera|car=ce=rie=ra|3 +cardinali|car=di=na=li|3 +carichi|ca=ri=chi|3 +caritatevole|ca=ri=ta=te=vo=le|3 +Carneade|Car=nea=de|3 +carriera|car=rie=ra|3 +carteggio|car=teg=gio|3 +casacca|ca=sac=ca|3 +cascante|ca=scan=te|3 +casucce|ca=suc=ce|3 +cautela|cau=te=la|3 +cavando|ca=van=do|3 +cedere|ce=de=re|3 +celebrità|ce=le=bri=tà|3 +centro|cen=tro|3 +cercan|cer=can|3 +cercarvi|cer=car=vi|3 +Cercate|Cer=ca=te|3 +cerchi|cer=chi|3 +cervelli|cer=vel=li|3 +cessar|ces=sar|3 +cessasse|ces=sas=se|3 +chiamarli|chia=mar=li|3 +chiamate|chia=ma=te|3 +chiamavan|chia=ma=van|3 +chiavi|chia=vi|3 +chiedendo|chie=den=do|3 +chiedete|chie=de=te|3 +chiedevan|chie=de=van|3 +chiediate|chie=dia=te|3 +chinato|chi=na=to|3 +chiodi|chio=di|3 +Chiodo|Chio=do|3 +chiudendo|chiu=den=do|3 +Chiudete|Chiu=de=te|3 +ciglia|ci=glia|3 +cimitero|ci=mi=te=ro|3 +ciondoloni|cion=do=lo=ni|3 +ciottoli|ciot=to=li|3 +circondava|cir=con=da=va|3 +circospezione|cir=co=spe=zio=ne|3 +circostanti|cir=co=stan=ti|3 +citare|ci=ta=re|3 +claustrale|clau=stra=le|3 +cocche|coc=che|3 +cocuzzolo|co=cuz=zo=lo|3 +cognizioni|co=gni=zio=ni|3 +colleghi|col=le=ghi|3 +coltelli|col=tel=li|3 +coltivata|col=ti=va=ta|3 +coltura|col=tu=ra|3 +comandano|co=man=da=no|3 +comandante|co=man=dan=te|3 +comandò|co=man=dò|3 +combatteva|com=bat=te=va|3 +cominciata|co=min=cia=ta|3 +commesso|com=mes=so|3 +comodi|co=mo=di|3 +compagna|com=pa=gna|3 +compagnoni|com=pa=gno=ni|3 +compatisco|com=pa=ti=sco|3 +compenso|com=pen=so|3 +compimento|com=pi=men=to|3 +complesso|com=ples=so|3 +composti|com=po=sti|3 +compresa|com=pre=sa|3 +concedere|con=ce=de=re|3 +concederà|con=ce=de=rà|3 +concertata|con=cer=ta=ta|3 +concesso|con=ces=so|3 +concludere|con=clu=de=re|3 +condoglianze|con=do=glian=ze|3 +condurla|con=dur=la|3 +condurrò|con=dur=rò|3 +conferma|con=fer=ma|3 +confermava|con=fer=ma=va|3 +confessarmi|con=fes=sar=mi|3 +confidenti|con=fi=den=ti|3 +confondere|con=fon=de=re|3 +confratello|con=fra=tel=lo|3 +congiunti|con=giun=ti|3 +congiunzione|con=giun=zio=ne|3 +congratularsi|con=gra=tu=lar=si|3 +conoscente|co=no=scen=te|3 +conoscesse|co=no=sces=se|3 +conoscevan|co=no=sce=van|3 +conosciuti|co=no=sciu=ti|3 +consapevole|con=sa=pe=vo=le|3 +consegna|con=se=gna|3 +consegnare|con=se=gna=re|3 +conservar|con=ser=var|3 +considerabile|con=si=de=ra=bi=le|3 +considerato|con=si=de=ra=to|3 +considerazione|con=si=de=ra=zio=ne|3 +consigliata|con=si=glia=ta|3 +consisteva|con=si=ste=va|3 +consueta|con=sue=ta|3 +consueto|con=sue=to|3 +contan|con=tan|3 +contano|con=ta=no|3 +contanti|con=tan=ti|3 +contava|con=ta=va|3 +contemplato|con=tem=pla=to|3 +contendenti|con=ten=den=ti|3 +continuamente|con=ti=nua=men=te|3 +contraddette|con=trad=det=te|3 +contrapposto|con=trap=po=sto|3 +contraria|con=tra=ria|3 +contrasto|con=tra=sto|3 +contratti|con=trat=ti|3 +convenisse|con=ve=nis=se|3 +conversazioni|con=ver=sa=zio=ni|3 +coppia|cop=pia|3 +cordialità|cor=dia=li=tà|3 +Cordusio|Cor=du=sio|3 +correggere|cor=reg=ge=re|3 +corrente|cor=ren=te|3 +corrisposto|cor=ri=spo=sto|3 +corsero|cor=se=ro|3 +costiera|co=stie=ra|3 +costosa|co=sto=sa|3 +creato|crea=to|3 +credenza|cre=den=za|3 +creder|cre=der|3 +crediamo|cre=dia=mo|3 +cresta|cre=sta|3 +crocifero|cro=ci=fe=ro|3 +cuccagna|cuc=ca=gna|3 +culpable|cul=pa=ble|3 +cupidigia|cu=pi=di=gia|3 +cupola|cu=po=la|3 +curare|cu=ra=re|3 +curiosa|cu=rio=sa|3 +dandole|dan=do=le|3 +dappocaggine|dap=po=cag=gi=ne|3 +Davvero|Dav=ve=ro|3 +debiti|de=bi=ti|3 +decider|de=ci=der|3 +decidere|de=ci=de=re|3 +decifrare|de=ci=fra=re|3 +degnamente|de=gna=men=te|3 +delegati|de=le=ga=ti|3 +deporre|de=por=re|3 +descrizione|de=scri=zio=ne|3 +descrizioni|de=scri=zio=ni|3 +deserti|de=ser=ti|3 +desiderare|de=si=de=ra=re|3 +dialogo|dia=lo=go|3 +diavolerie|dia=vo=le=rie|3 +dibatteva|di=bat=te=va|3 +dicitore|di=ci=to=re|3 +dicitura|di=ci=tu=ra|3 +difendersi|di=fen=der=si|3 +diffidenza|dif=fi=den=za|3 +diffondeva|dif=fon=de=va|3 +diffusa|dif=fu=sa|3 +diligenze|di=li=gen=ze|3 +dimenava|di=me=na=va|3 +dimenticanza|di=men=ti=can=za|3 +Dimodoché|Di=mo=do=ché|3 +dimostrar|di=mo=strar|3 +dimostrava|di=mo=stra=va|3 +Dionigi|Dio=ni=gi|3 +dipendenza|di=pen=den=za|3 +dipendeva|di=pen=de=va|3 +diranno|di=ran=no|3 +dirotto|di=rot=to|3 +dirvelo|dir=ve=lo|3 +disabitata|di=sa=bi=ta=ta|3 +disarmato|di=sar=ma=to|3 +discreta|di=scre=ta|3 +disfare|di=sfa=re|3 +dispaccio|di=spac=cio|3 +disperati|di=spe=ra=ti|3 +disponeva|di=spo=ne=va|3 +disporre|di=spor=re|3 +dispose|di=spo=se|3 +disposizioni|di=spo=si=zio=ni|3 +dissimulare|dis=si=mu=la=re|3 +distingue|di=stin=gue|3 +distinguevano|di=stin=gue=va=no|3 +distinzioni|di=stin=zio=ni|3 +distratto|di=strat=to|3 +distribuivano|di=stri=bu=i=va=no|3 +ditele|di=te=le|3 +divenire|di=ve=ni=re|3 +divenivan|di=ve=ni=van|3 +diventata|di=ven=ta=ta|3 +diventavan|di=ven=ta=van|3 +divenute|di=ve=nu=te|3 +divenuti|di=ve=nu=ti|3 +divisa|di=vi=sa|3 +divozione|di=vo=zio=ne|3 +documenti|do=cu=men=ti|3 +domanderà|do=man=de=rà|3 +domestica|do=me=sti=ca|3 +dormiva|dor=mi=va|3 +dovendo|do=ven=do|3 +drappello|drap=pel=lo|3 +dubitate|du=bi=ta=te|3 +durante|du=ran=te|3 +ebbene|eb=be=ne|3 +ecclesiastico|ec=cle=sia=sti=co|3 +Eccome|Ec=co=me|3 +editto|edit=to|3 +educati|edu=ca=ti|3 +efficace|ef=fi=ca=ce|3 +elevato|ele=va=to|3 +eloquenza|elo=quen=za|3 +Emanuele|Ema=nue=le|3 +entrarvi|en=trar=vi|3 +entrate|en=tra=te|3 +entravan|en=tra=van|3 +entusiasmo|en=tu=sia=smo|3 +erbacce|er=bac=ce|3 +esclamando|escla=man=do|3 +escludere|esclu=de=re|3 +esclusivamente|esclu=si=va=men=te|3 +esecutori|ese=cu=to=ri|3 +eseguir|ese=guir|3 +eseguito|ese=gui=to|3 +esequie|ese=quie|3 +esercitare|eser=ci=ta=re|3 +esercitarsi|eser=ci=tar=si|3 +esibizione|esi=bi=zio=ne|3 +esperti|esper=ti|3 +esplorare|esplo=ra=re|3 +esploratori|esplo=ra=to=ri|3 +essendoci|es=sen=do=ci|3 +esterna|ester=na|3 +evidentemente|evi=den=te=men=te|3 +evitare|evi=ta=re|3 +facendolo|fa=cen=do=lo|3 +facili|fa=ci=li|3 +famigliarità|fa=mi=glia=ri=tà|3 +famosi|fa=mo=si|3 +fantasmi|fan=ta=smi|3 +farebbero|fa=reb=be=ro|3 +farsene|far=se=ne|3 +fastidiosi|fa=sti=dio=si|3 +Fatelo|Fa=te=lo|3 +fattibile|fat=ti=bi=le|3 +febbri|feb=bri|3 +fedele|fe=de=le|3 +fedeltà|fe=del=tà|3 +felicità|fe=li=ci=tà|3 +Ferdinando|Fer=di=nan=do|3 +fermarci|fer=mar=ci|3 +fermarono|fer=ma=ro=no|3 +fermasse|fer=mas=se|3 +fermate|fer=ma=te|3 +fermatina|fer=ma=ti=na|3 +fermezza|fer=mez=za|3 +ferocia|fe=ro=cia|3 +fiamma|fiam=ma|3 +ficcare|fic=ca=re|3 +fidarsi|fi=dar=si|3 +fidate|fi=da=te|3 +Figliuoli|Fi=gliuo=li|3 +figuratevi|fi=gu=ra=te=vi|3 +finestrina|fi=ne=stri=na|3 +finirò|fi=ni=rò|3 +finisse|fi=nis=se|3 +Finora|Fi=no=ra|3 +fissava|fis=sa=va|3 +focolare|fo=co=la=re|3 +foggia|fog=gia|3 +foglia|fo=glia|3 +fondaco|fon=da=co|3 +fondate|fon=da=te|3 +fondato|fon=da=to|3 +forestiera|fo=re=stie=ra|3 +formano|for=ma=no|3 +fornito|for=ni=to|3 +forzati|for=za=ti|3 +fossato|fos=sa=to|3 +fossimo|fos=si=mo|3 +francesi|fran=ce=si|3 +franco|fran=co|3 +frettoloso|fret=to=lo=so|3 +frugar|fru=gar|3 +fruttare|frut=ta=re|3 +frutte|frut=te|3 +frutto|frut=to|3 +fuggiaschi|fug=gia=schi|3 +fuggiti|fug=gi=ti|3 +funebre|fu=ne=bre|3 +funesta|fu=ne=sta|3 +funesti|fu=ne=sti|3 +furberie|fur=be=rie|3 +furiosi|fu=rio=si|3 +Furono|Fu=ro=no|3 +gabellieri|ga=bel=lie=ri|3 +gabellini|ga=bel=li=ni|3 +galoppo|ga=lop=po|3 +gangheri|gan=ghe=ri|3 +garbatamente|gar=ba=ta=men=te|3 +geloso|ge=lo=so|3 +gentilezza|gen=ti=lez=za|3 +gettarsi|get=tar=si|3 +giacevano|gia=ce=va=no|3 +gialli|gial=li|3 +ginocchia|gi=noc=chia|3 +gioviale|gio=via=le|3 +giovinezza|gio=vi=nez=za|3 +girando|gi=ran=do|3 +Girolamo|Gi=ro=la=mo|3 +giudicato|giu=di=ca=to|3 +giulivo|giu=li=vo|3 +giunse|giun=se|3 +giunto|giun=to|3 +Giunto|Giun=to|3 +giusti|giu=sti|3 +gliele|glie=le|3 +glieli|glie=li|3 +gloriosa|glo=rio=sa|3 +gocciolino|goc=cio=li=no|3 +godere|go=de=re|3 +Grazie|Gra=zie|3 +grazioso|gra=zio=so|3 +gregge|greg=ge|3 +grilli|gril=li|3 +grossi|gros=si|3 +grotta|grot=ta|3 +Guarda|Guar=da|3 +guardano|guar=da=no|3 +guardati|guar=da=ti|3 +guardavan|guar=da=van|3 +guardie|guar=die|3 +Guardò|Guar=dò|3 +Guarita|Gua=ri=ta|3 +guastava|gua=sta=va|3 +guidar|gui=dar|3 +guidare|gui=da=re|3 +hauuto|hauu=to|3 +ignoranti|igno=ran=ti|3 +illustre|il=lu=stre|3 +imboccatura|im=boc=ca=tu=ra|3 +imbrogliata|im=bro=glia=ta|3 +immaginata|im=ma=gi=na=ta|3 +immediata|im=me=dia=ta|3 +immensa|im=men=sa|3 +immenso|im=men=so|3 +immoto|im=mo=to|3 +impegnato|im=pe=gna=to|3 +impegnò|im=pe=gnò|3 +impero|im=pe=ro|3 +impeto|im=pe=to|3 +impiegar|im=pie=gar|3 +impiegato|im=pie=ga=to|3 +implorar|im=plo=rar|3 +implorava|im=plo=ra=va|3 +importava|im=por=ta=va|3 +impossibile|im=pos=si=bi=le|3 +impotenza|im=po=ten=za|3 +imprecazioni|im=pre=ca=zio=ni|3 +impressioni|im=pres=sio=ni|3 +impreveduto|im=pre=ve=du=to|3 +improvvisa|im=prov=vi=sa|3 +imprudente|im=pru=den=te|3 +incamminati|in=cam=mi=na=ti|3 +incendio|in=cen=dio|3 +incerta|in=cer=ta|3 +inchiodato|in=chio=da=to|3 +inciampo|in=ciam=po|3 +inclinati|in=cli=na=ti|3 +inclinazioni|in=cli=na=zio=ni|3 +incomoda|in=co=mo=da|3 +incontrar|in=con=trar|3 +incontri|in=con=tri|3 +indica|in=di=ca|3 +indicazioni|in=di=ca=zio=ni|3 +indicò|in=di=cò|3 +indifferenza|in=dif=fe=ren=za|3 +individui|in=di=vi=dui|3 +individuo|in=di=vi=duo|3 +indovinato|in=do=vi=na=to|3 +indovinava|in=do=vi=na=va|3 +industria|in=du=stria|3 +inedita|ine=di=ta|3 +inevitabile|ine=vi=ta=bi=le|3 +infallibilmente|in=fal=li=bil=men=te|3 +inferiori|in=fe=rio=ri|3 +infernale|in=fer=na=le|3 +infervorato|in=fer=vo=ra=to|3 +informar|in=for=mar|3 +informarlo|in=for=mar=lo|3 +ingannava|in=gan=na=va|3 +inganno|in=gan=no|3 +inginocchiò|in=gi=noc=chiò|3 +ingiuria|in=giu=ria|3 +ingrosso|in=gros=so|3 +inimicizia|ini=mi=ci=zia|3 +inoltrata|inol=tra=ta|3 +inoltrava|inol=tra=va|3 +inquietudini|in=quie=tu=di=ni|3 +insegnamento|in=se=gna=men=to|3 +insegnata|in=se=gna=ta|3 +inseguito|in=se=gui=to|3 +insensati|in=sen=sa=ti|3 +insistette|in=si=stet=te|3 +insolenza|in=so=len=za|3 +insomma|in=som=ma|3 +intelletto|in=tel=let=to|3 +intendete|in=ten=de=te|3 +Intendo|In=ten=do|3 +intenta|in=ten=ta|3 +interrompere|in=ter=rom=pe=re|3 +intimato|in=ti=ma=to|3 +intimò|in=ti=mò|3 +Intorno|In=tor=no|3 +intraprendere|in=tra=pren=de=re|3 +introdusse|in=tro=dus=se|3 +invasione|in=va=sio=ne|3 +invenzione|in=ven=zio=ne|3 +invigilare|in=vi=gi=la=re|3 +invitati|in=vi=ta=ti|3 +inviti|in=vi=ti|3 +involtino|in=vol=ti=no|3 +irrevocabile|ir=re=vo=ca=bi=le|3 +irritati|ir=ri=ta=ti|3 +iscoprire|isco=pri=re|3 +iscritto|iscrit=to|3 +isolata|iso=la=ta|3 +ispirato|ispi=ra=to|3 +istate|ista=te|3 +istato|ista=to|3 +istava|ista=va|3 +istette|istet=te|3 +itinerario|iti=ne=ra=rio|3 +languenti|lan=guen=ti|3 +languore|lan=guo=re|3 +Lasciamo|La=scia=mo|3 +lasciarle|la=sciar=le|3 +lasciarmi|la=sciar=mi|3 +lasciarvi|la=sciar=vi|3 +lasciassero|la=scias=se=ro|3 +lavoranti|la=vo=ran=ti|3 +lavorava|la=vo=ra=va|3 +legale|le=ga=le|3 +legati|le=ga=ti|3 +leggerezza|leg=ge=rez=za|3 +lentezza|len=tez=za|3 +lettura|let=tu=ra|3 +levargli|le=var=gli|3 +levarle|le=var=le|3 +levate|le=va=te|3 +levati|le=va=ti|3 +levava|le=va=va|3 +liberale|li=be=ra=le|3 +liberarla|li=be=rar=la|3 +liberata|li=be=ra=ta|3 +liberatori|li=be=ra=to=ri|3 +liberliber|li=ber=li=ber|3 +litiganti|li=ti=gan=ti|3 +lividi|li=vi=di|3 +lodarsi|lo=dar=si|3 +lontananza|lon=ta=nan=za|3 +luccicare|luc=ci=ca=re|3 +lucente|lu=cen=te|3 +lucido|lu=ci=do|3 +lusinghe|lu=sin=ghe|3 +lustri|lu=stri|3 +maestri|mae=stri|3 +magazzini|ma=gaz=zi=ni|3 +magistrato|ma=gi=stra=to|3 +magnifico|ma=gni=fi=co|3 +malanno|ma=lan=no|3 +malattie|ma=lat=tie|3 +maleficio|ma=le=fi=cio|3 +malinconia|ma=lin=co=nia|3 +mallevadore|mal=le=va=do=re|3 +mancamenti|man=ca=men=ti|3 +mancanza|man=can=za|3 +mancate|man=ca=te|3 +mancherà|man=che=rà|3 +mandate|man=da=te|3 +mangia|man=gia|3 +mangiar|man=giar|3 +mangiate|man=gia=te|3 +manifestamente|ma=ni=fe=sta=men=te|3 +manifestare|ma=ni=fe=sta=re|3 +manifestò|ma=ni=fe=stò|3 +mansuetudine|man=sue=tu=di=ne|3 +mantenuto|man=te=nu=to|3 +maritare|ma=ri=ta=re|3 +maritata|ma=ri=ta=ta|3 +mascalzoni|ma=scal=zo=ni|3 +masnada|ma=sna=da|3 +mattoni|mat=to=ni|3 +medicina|me=di=ci=na|3 +Mediolani|Me=dio=la=ni|3 +meditando|me=di=tan=do|3 +Meglio|Me=glio|3 +mentire|men=ti=re|3 +mercantessa|mer=can=tes=sa|3 +Mercanti|Mer=can=ti|3 +mescendo|me=scen=do|3 +mescolati|me=sco=la=ti|3 +messaggiero|mes=sag=gie=ro|3 +messosi|mes=so=si|3 +mestizia|me=sti=zia|3 +metterebbe|met=te=reb=be|3 +mettermi|met=ter=mi|3 +Michele|Mi=che=le|3 +minacciato|mi=nac=cia=to|3 +minacciosi|mi=nac=cio=si|3 +mischiarsi|mi=schiar=si|3 +misteriosa|mi=ste=rio=sa|3 +misurato|mi=su=ra=to|3 +misurava|mi=su=ra=va|3 +misure|mi=su=re|3 +mobile|mo=bi=le|3 +moggio|mog=gio|3 +momentaneo|mo=men=ta=neo|3 +monsignor|mon=si=gnor|3 +moribondo|mo=ri=bon=do|3 +mortali|mor=ta=li|3 +moschetti|mo=schet=ti|3 +mostri|mo=stri|3 +movimenti|mo=vi=men=ti|3 +mucchi|muc=chi|3 +mugolìo|mu=go=lìo|3 +munizione|mu=ni=zio=ne|3 +nasceva|na=sce=va|3 +nascondendo|na=scon=den=do|3 +nascondiglio|na=scon=di=glio|3 +nascosti|na=sco=sti|3 +nativo|na=ti=vo|3 +nebbia|neb=bia|3 +negata|ne=ga=ta|3 +nicchia|nic=chia|3 +nomina|no=mi=na|3 +nominare|no=mi=na=re|3 +nominarlo|no=mi=nar=lo|3 +nominato|no=mi=na=to|3 +noncuranza|non=cu=ran=za|3 +notate|no=ta=te|3 +noviziato|no=vi=zia=to|3 +nuvola|nu=vo=la|3 +obbliga|ob=bli=ga|3 +obbligata|ob=bli=ga=ta|3 +obblighi|ob=bli=ghi|3 +occhiacci|oc=chiac=ci|3 +occhiali|oc=chia=li|3 +occhiatina|oc=chia=ti=na|3 +occorso|oc=cor=so|3 +occupar|oc=cu=par|3 +occupati|oc=cu=pa=ti|3 +odiavo|odia=vo|3 +odiosa|odio=sa|3 +offerte|of=fer=te|3 +offesi|of=fe=si|3 +oltremodo|ol=tre=mo=do|3 +ombrosa|om=bro=sa|3 +ombroso|om=bro=so|3 +onorato|ono=ra=to|3 +opinioni|opi=nio=ni|3 +opportuni|op=por=tu=ni|3 +opposizione|op=po=si=zio=ne|3 +oppressi|op=pres=si|3 +ordigni|or=di=gni|3 +ordinare|or=di=na=re|3 +ordinariamente|or=di=na=ria=men=te|3 +ordinata|or=di=na=ta|3 +Orientale|Orien=ta=le|3 +orrendo|or=ren=do|3 +orrori|or=ro=ri|3 +ospitalità|ospi=ta=li=tà|3 +ossequio|os=se=quio|3 +osservando|os=ser=van=do|3 +osservata|os=ser=va=ta|3 +ostinati|osti=na=ti|3 +ostinazione|osti=na=zio=ne|3 +ottenne|ot=ten=ne|3 +ottenuta|ot=te=nu=ta|3 +pagarlo|pa=gar=lo|3 +pagato|pa=ga=to|3 +paggio|pag=gio|3 +paiono|pa=io=no|3 +pallore|pal=lo=re|3 +panchetto|pan=chet=to|3 +pancia|pan=cia|3 +paradiso|pa=ra=di=so|3 +paragonato|pa=ra=go=na=to|3 +parati|pa=ra=ti|3 +parecchi|pa=rec=chi|3 +parentela|pa=ren=te=la|3 +paressero|pa=res=se=ro|3 +parlargliene|par=lar=glie=ne|3 +Parlate|Par=la=te|3 +parlerà|par=le=rà|3 +parolacce|pa=ro=lac=ce|3 +parpagliole|par=pa=glio=le|3 +parrocchiale|par=roc=chia=le|3 +particolarmente|par=ti=co=lar=men=te|3 +partite|par=ti=te|3 +Partito|Par=ti=to|3 +parvero|par=ve=ro|3 +pascolo|pa=sco=lo|3 +Passato|Pas=sa=to|3 +pasticci|pa=stic=ci|3 +patrimonio|pa=tri=mo=nio|3 +paziente|pa=zien=te|3 +pazzia|paz=zia|3 +peccati|pec=ca=ti|3 +Peccato|Pec=ca=to|3 +pecore|pe=co=re|3 +pendevano|pen=de=va=no|3 +penosi|pe=no=si|3 +pensano|pen=sa=no|3 +pensata|pen=sa=ta|3 +pensavano|pen=sa=va=no|3 +penserebbe|pen=se=reb=be|3 +pentimenti|pen=ti=men=ti|3 +pentita|pen=ti=ta|3 +pentiva|pen=ti=va|3 +perdendosi|per=den=do=si|3 +perdonare|per=do=na=re|3 +perdonatemi|per=do=na=te=mi|3 +perdoni|per=do=ni|3 +perfino|per=fi=no|3 +permetteva|per=met=te=va|3 +perpetua|per=pe=tua|3 +persecuzione|per=se=cu=zio=ne|3 +persuader|per=sua=der|3 +pesanti|pe=san=ti|3 +piaghe|pia=ghe|3 +pianerottolo|pia=ne=rot=to=lo|3 +piantato|pian=ta=to|3 +piatti|piat=ti|3 +picchiava|pic=chia=va|3 +picchio|pic=chio|3 +piegando|pie=gan=do|3 +piegate|pie=ga=te|3 +pienezza|pie=nez=za|3 +pietosamente|pie=to=sa=men=te|3 +pigionali|pi=gio=na=li|3 +pilastri|pi=la=stri|3 +pistole|pi=sto=le|3 +politico|po=li=ti=co|3 +portan|por=tan|3 +portarne|por=tar=ne|3 +portatore|por=ta=to=re|3 +portatori|por=ta=to=ri|3 +portavan|por=ta=van|3 +portinaio|por=ti=na=io|3 +positura|po=si=tu=ra|3 +possedeva|pos=se=de=va|3 +possiam|pos=siam|3 +possiate|pos=sia=te|3 +possibilità|pos=si=bi=li=tà|3 +poterci|po=ter=ci|3 +potergli|po=ter=gli|3 +poterlo|po=ter=lo|3 +Potrebbe|Po=treb=be|3 +poverette|po=ve=ret=te|3 +pranzi|pran=zi|3 +pratiche|pra=ti=che|3 +precisamente|pre=ci=sa=men=te|3 +predizione|pre=di=zio=ne|3 +pregarla|pre=gar=la|3 +pregherete|pre=ghe=re=te|3 +preghi|pre=ghi|3 +prenda|pren=da|3 +prendendola|pren=den=do=la|3 +prenderla|pren=der=la|3 +prendersela|pren=der=se=la|3 +preparata|pre=pa=ra=ta|3 +preparativi|pre=pa=ra=ti=vi|3 +prescrisse|pre=scris=se|3 +prescrive|pre=scri=ve|3 +prescriveva|pre=scri=ve=va|3 +presenta|pre=sen=ta|3 +presentasse|pre=sen=tas=se|3 +presentata|pre=sen=ta=ta|3 +presentava|pre=sen=ta=va|3 +presolo|pre=so=lo|3 +pretende|pre=ten=de|3 +preveder|pre=ve=der|3 +preziosa|pre=zio=sa|3 +primaria|pri=ma=ria|3 +primogenito|pri=mo=ge=ni=to|3 +privilegiata|pri=vi=le=gia=ta|3 +privilegio|pri=vi=le=gio|3 +procacciarsi|pro=cac=ciar=si|3 +processo|pro=ces=so|3 +procurare|pro=cu=ra=re|3 +proditorio|pro=di=to=rio|3 +produrre|pro=dur=re|3 +proferiva|pro=fe=ri=va|3 +profitto|pro=fit=to|3 +progetti|pro=get=ti|3 +progresso|pro=gres=so|3 +promise|pro=mi=se|3 +pronunziato|pro=nun=zia=to|3 +propensione|pro=pen=sio=ne|3 +proposte|pro=po=ste|3 +Proprio|Pro=prio|3 +proteggere|pro=teg=ge=re|3 +protesta|pro=te=sta|3 +provasse|pro=vas=se|3 +Provava|Pro=va=va|3 +provocato|pro=vo=ca=to|3 +provocatori|pro=vo=ca=to=ri|3 +Provvisione|Prov=vi=sio=ne|3 +pubblicò|pub=bli=cò|3 +puerizia|pue=ri=zia|3 +pulcin|pul=cin|3 +pulizia|pu=li=zia|3 +pungenti|pun=gen=ti|3 +puntando|pun=tan=do|3 +puntino|pun=ti=no|3 +quaggiù|quag=giù|3 +Qualcheduno|Qual=che=du=no|3 +Quanti|Quan=ti|3 +Quantunque|Quan=tun=que|3 +quassù|quas=sù|3 +quietamente|quie=ta=men=te|3 +quieti|quie=ti|3 +rabbioso|rab=bio=so|3 +raccoglieva|rac=co=glie=va|3 +raccolte|rac=col=te|3 +raccomandazioni|rac=co=man=da=zio=ni|3 +raccontarsi|rac=con=tar=si|3 +racconti|rac=con=ti|3 +Racconto|Rac=con=to|3 +raddoppiar|rad=dop=piar|3 +raddoppiare|rad=dop=pia=re|3 +ragazzate|ra=gaz=za=te|3 +ragazzetto|ra=gaz=zet=to|3 +raggio|rag=gio|3 +Ragguaglio|Rag=gua=glio|3 +ragionevoli|ra=gio=ne=vo=li|3 +rammentandosi|ram=men=tan=do=si|3 +rammentarsi|ram=men=tar=si|3 +rammentate|ram=men=ta=te|3 +rannicchiata|ran=nic=chia=ta|3 +rappresentare|rap=pre=sen=ta=re|3 +rappresentava|rap=pre=sen=ta=va|3 +rasentando|ra=sen=tan=do|3 +ravviare|rav=via=re|3 +redenzione|re=den=zio=ne|3 +reggersi|reg=ger=si|3 +regione|re=gio=ne|3 +rendevan|ren=de=van|3 +rendite|ren=di=te|3 +Resegone|Re=se=go=ne|3 +resistere|re=si=ste=re|3 +respirò|re=spi=rò|3 +rettori|ret=to=ri|3 +reverendissima|re=ve=ren=dis=si=ma|3 +riceverli|ri=ce=ver=li|3 +richiedesse|ri=chie=des=se|3 +ricominciò|ri=co=min=ciò|3 +ricomparve|ri=com=par=ve|3 +riconosciuti|ri=co=no=sciu=ti|3 +Ricordatevi|Ri=cor=da=te=vi|3 +ricordò|ri=cor=dò|3 +ricusasse|ri=cu=sas=se|3 +ridendo|ri=den=do|3 +rideva|ri=de=va|3 +ridire|ri=di=re|3 +ridosso|ri=dos=so|3 +ridurre|ri=dur=re|3 +rifare|ri=fa=re|3 +riflessione|ri=fles=sio=ne|3 +rifugiata|ri=fu=gia=ta|3 +rifugiati|ri=fu=gia=ti|3 +rigiri|ri=gi=ri|3 +rigoroso|ri=go=ro=so|3 +riguardato|ri=guar=da=to|3 +rilevare|ri=le=va=re|3 +rilevate|ri=le=va=te|3 +rilievo|ri=lie=vo|3 +rimangon|ri=man=gon|3 +rimbombo|rim=bom=bo|3 +rimessa|ri=mes=sa|3 +rimestar|ri=me=star|3 +rimettendosi|ri=met=ten=do=si|3 +rimettere|ri=met=te=re|3 +rimpetto|rim=pet=to|3 +rinascere|ri=na=sce=re|3 +rinchiuse|rin=chiu=se|3 +rincorare|rin=co=ra=re|3 +rincorò|rin=co=rò|3 +ringraziarlo|rin=gra=ziar=lo|3 +ripararsi|ri=pa=rar=si|3 +ripetute|ri=pe=tu=te|3 +riportare|ri=por=ta=re|3 +riposare|ri=po=sa=re|3 +riposarsi|ri=po=sar=si|3 +riposta|ri=po=sta|3 +riprender|ri=pren=der|3 +riprendeva|ri=pren=de=va|3 +risapere|ri=sa=pe=re|3 +rischioso|ri=schio=so|3 +riscontro|ri=scon=tro|3 +risentimento|ri=sen=ti=men=to|3 +risentirsi|ri=sen=tir=si|3 +risentì|ri=sen=tì|3 +risolutezza|ri=so=lu=tez=za|3 +risoluzioni|ri=so=lu=zio=ni|3 +risonò|ri=so=nò|3 +risparmi|ri=spar=mi|3 +rispettabile|ri=spet=ta=bi=le|3 +rispetti|ri=spet=ti|3 +rispettoso|ri=spet=to=so|3 +rispondergli|ri=spon=der=gli|3 +ristoro|ri=sto=ro|3 +ristretta|ri=stret=ta|3 +ristretti|ri=stret=ti|3 +ristringeva|ri=strin=ge=va|3 +risvegliò|ri=sve=gliò|3 +ritira|ri=ti=ra|3 +ritirando|ri=ti=ran=do|3 +ritirano|ri=ti=ra=no|3 +ritiro|ri=ti=ro|3 +ritornare|ri=tor=na=re|3 +ritrovati|ri=tro=va=ti|3 +riuscir|riu=scir|3 +rivalità|ri=va=li=tà|3 +riveder|ri=ve=der|3 +Rivola|Ri=vo=la|3 +rivolge|ri=vol=ge|3 +Roccella|Roc=cel=la|3 +rodendosi|ro=den=do=si|3 +rodeva|ro=de=va|3 +Romani|Ro=ma=ni|3 +romper|rom=per|3 +rossore|ros=so=re|3 +rotolo|ro=to=lo|3 +rottami|rot=ta=mi|3 +rovinato|ro=vi=na=to|3 +rubato|ru=ba=to|3 +ruminando|ru=mi=nan=do|3 +sacerdote|sa=cer=do=te|3 +salario|sa=la=rio|3 +salottino|sa=lot=ti=no|3 +saltar|sal=tar|3 +salvare|sal=va=re|3 +salvatico|sal=va=ti=co|3 +sapevo|sa=pe=vo|3 +saprete|sa=pre=te|3 +sareste|sa=re=ste|3 +Saturno|Sa=tur=no|3 +sbaglio|sba=glio|3 +sbalordita|sba=lor=di=ta|3 +sbandati|sban=da=ti|3 +sbirraglia|sbir=ra=glia|3 +sbrattare|sbrat=ta=re|3 +scalzi|scal=zi|3 +scampo|scam=po|3 +scapestrati|sca=pe=stra=ti|3 +scappate|scap=pa=te|3 +scemato|sce=ma=to|3 +scende|scen=de|3 +scendendo|scen=den=do|3 +scendere|scen=de=re|3 +schegge|scheg=ge|3 +schermirsi|scher=mir=si|3 +schietta|schiet=ta|3 +schifo|schi=fo|3 +sciagure|scia=gu=re|3 +sciocca|scioc=ca|3 +sciolse|sciol=se|3 +scompagnati|scom=pa=gna=ti|3 +sconnesso|scon=nes=so|3 +scontare|scon=ta=re|3 +scoppio|scop=pio|3 +scorciatoia|scor=cia=to=ia|3 +scorse|scor=se|3 +scovar|sco=var|3 +scrive|scri=ve|3 +scriver|scri=ver|3 +scriveva|scri=ve=va|3 +scroscio|scro=scio|3 +scrupolo|scru=po=lo|3 +scusare|scu=sa=re|3 +secondare|se=con=da=re|3 +seduta|se=du=ta|3 +sedute|se=du=te|3 +segnale|se=gna=le|3 +segretamente|se=gre=ta=men=te|3 +seguano|se=gua=no|3 +seguendo|se=guen=do|3 +seguire|se=gui=re|3 +seguisse|se=guis=se|3 +seguita|se=gui=ta|3 +seguitavano|se=gui=ta=va=no|3 +seguiva|se=gui=va|3 +selvaggia|sel=vag=gia|3 +selvaggio|sel=vag=gio|3 +sensazioni|sen=sa=zio=ni|3 +sentirai|sen=ti=rai|3 +sentiremo|sen=ti=re=mo|3 +separarono|se=pa=ra=ro=no|3 +sequestrati|se=que=stra=ti|3 +serena|se=re=na|3 +seriamente|se=ria=men=te|3 +servile|ser=vi=le|3 +servirlo|ser=vir=lo|3 +servirsene|ser=vir=se=ne|3 +servirvi|ser=vir=vi|3 +servirò|ser=vi=rò|3 +servono|ser=vo=no|3 +severamente|se=ve=ra=men=te|3 +severissime|se=ve=ris=si=me|3 +sfogare|sfo=ga=re|3 +sfoghi|sfo=ghi|3 +sfrenatezza|sfre=na=tez=za|3 +sfuggire|sfug=gi=re|3 +sgambetto|sgam=bet=to|3 +sguaiato|sgua=ia=to|3 +significa|si=gni=fi=ca|3 +significare|si=gni=fi=ca=re|3 +significazione|si=gni=fi=ca=zio=ne|3 +sinistri|si=ni=stri|3 +situata|si=tua=ta|3 +situazione|si=tua=zio=ne|3 +smarrito|smar=ri=to|3 +smettere|smet=te=re|3 +smontò|smon=tò|3 +smossa|smos=sa|3 +smovere|smo=ve=re|3 +soccorrere|soc=cor=re=re|3 +soddisfatti|sod=di=sfat=ti|3 +soddisfazioni|sod=di=sfa=zio=ni|3 +sofferenza|sof=fe=ren=za|3 +sofferti|sof=fer=ti|3 +soffitta|sof=fit=ta|3 +soffiò|sof=fiò|3 +soffogato|sof=fo=ga=to|3 +sogghigno|sog=ghi=gno|3 +soggiungeva|sog=giun=ge=va|3 +sognare|so=gna=re|3 +soldatesca|sol=da=te=sca|3 +soldato|sol=da=to|3 +solitaria|so=li=ta=ria|3 +solleva|sol=le=va|3 +sollevar|sol=le=var|3 +sollevazione|sol=le=va=zio=ne|3 +somiglianza|so=mi=glian=za|3 +sonare|so=na=re|3 +sopraccigli|so=prac=ci=gli|3 +soprannome|so=pran=no=me|3 +sorridendo|sor=ri=den=do|3 +sospese|so=spe=se|3 +sospettare|so=spet=ta=re|3 +sospiri|so=spi=ri|3 +sostegno|so=ste=gno|3 +sostengono|so=sten=go=no|3 +sotterrarli|sot=ter=rar=li|3 +soverchieria|so=ver=chie=ria|3 +spalancando|spa=lan=can=do|3 +spandeva|span=de=va|3 +spaventevole|spa=ven=te=vo=le|3 +spedale|spe=da=le|3 +spediti|spe=di=ti|3 +spendere|spen=de=re|3 +sperasse|spe=ras=se|3 +Speriamo|Spe=ria=mo|3 +spiegato|spie=ga=to|3 +spieghi|spie=ghi|3 +spiegò|spie=gò|3 +Spinola|Spi=no=la|3 +spiriti|spi=ri=ti|3 +spirituali|spi=ri=tua=li|3 +splendido|splen=di=do|3 +splendore|splen=do=re|3 +spoglie|spo=glie|3 +sponda|spon=da|3 +squadra|squa=dra|3 +stabilire|sta=bi=li=re|3 +stabilita|sta=bi=li=ta|3 +staccata|stac=ca=ta|3 +stanca|stan=ca|3 +stanga|stan=ga|3 +stentato|sten=ta=to|3 +stettero|stet=te=ro|3 +stizzosamente|stiz=zo=sa=men=te|3 +storcendo|stor=cen=do|3 +storta|stor=ta|3 +storto|stor=to|3 +stracco|strac=co|3 +stradette|stra=det=te|3 +strage|stra=ge|3 +straordinarie|straor=di=na=rie|3 +strascicarsi|stra=sci=car=si|3 +stravolto|stra=vol=to|3 +stretti|stret=ti|3 +strida|stri=da|3 +strisciando|stri=scian=do|3 +studiando|stu=dian=do|3 +studiate|stu=dia=te|3 +studiava|stu=dia=va|3 +stufato|stu=fa=to|3 +stupefatta|stu=pe=fat=ta|3 +stupefatto|stu=pe=fat=to|3 +stupore|stu=po=re|3 +subordinati|su=bor=di=na=ti|3 +succinto|suc=cin=to|3 +superbi|su=per=bi|3 +superbo|su=per=bo|3 +superfluo|su=per=fluo|3 +supplicazione|sup=pli=ca=zio=ne|3 +supplizio|sup=pli=zio|3 +svanire|sva=ni=re|3 +sveglio|sve=glio|3 +taciuto|ta=ciu=to|3 +taglia|ta=glia|3 +talmente|tal=men=te|3 +talora|ta=lo=ra|3 +Talvolta|Tal=vol=ta|3 +tanghero|tan=ghe=ro|3 +tappeto|tap=pe=to|3 +temuta|te=mu=ta|3 +tendenza|ten=den=za|3 +tenendolo|te=nen=do=lo|3 +tenuti|te=nu=ti|3 +terminato|ter=mi=na=to|3 +terminò|ter=mi=nò|3 +tesoro|te=so=ro|3 +timido|ti=mi=do|3 +timori|ti=mo=ri|3 +tirarlo|ti=rar=lo|3 +tirarsi|ti=rar=si|3 +tiravan|ti=ra=van|3 +tiriamo|ti=ria=mo|3 +toccavano|toc=ca=va=no|3 +toccherebbe|toc=che=reb=be|3 +tonaca|to=na=ca|3 +torbida|tor=bi=da|3 +torbido|tor=bi=do|3 +tormentato|tor=men=ta=to|3 +tornata|tor=na=ta|3 +tornati|tor=na=ti|3 +Tornato|Tor=na=to|3 +torneremo|tor=ne=re=mo|3 +torrenti|tor=ren=ti|3 +tortura|tor=tu=ra|3 +tovaglia|to=va=glia|3 +tovagliolo|to=va=glio=lo|3 +tracce|trac=ce|3 +traditore|tra=di=to=re|3 +tradizioni|tra=di=zio=ni|3 +tranquillità|tran=quil=li=tà|3 +trascuranza|tra=scu=ran=za|3 +trascurato|tra=scu=ra=to|3 +trasportata|tra=spor=ta=ta|3 +trasportati|tra=spor=ta=ti|3 +Trattandosi|Trat=tan=do=si|3 +trattare|trat=ta=re|3 +trattarvi|trat=tar=vi|3 +trattasse|trat=tas=se|3 +trattenerla|trat=te=ner=la|3 +tratteneva|trat=te=ne=va|3 +travagli|tra=va=gli|3 +trecce|trec=ce|3 +tremare|tre=ma=re|3 +tremenda|tre=men=da|3 +tremendo|tre=men=do|3 +trenta|tren=ta|3 +tribolato|tri=bo=la=to|3 +trinciando|trin=cian=do|3 +tristezza|tri=stez=za|3 +trombe|trom=be|3 +troncar|tron=car|3 +troncata|tron=ca=ta|3 +tronche|tron=che|3 +tronco|tron=co|3 +troppi|trop=pi|3 +trovano|tro=va=no|3 From 64d161e88bef241a07364626448f618a261dc14b Mon Sep 17 00:00:00 2001 From: Istiak Tridip <13367189+istiak-tridip@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:19:34 +0600 Subject: [PATCH 17/29] feat: unify navigation handling with system-wide continuous navigation (#600) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR unifies navigation handling & adds system-wide support for continuous navigation. ## Summary Holding down a navigation button now continuously advances through items until the button is released. This removes the need for repeated press-and-release actions and makes navigation faster and smoother, especially in long menus or documents. When page-based navigation is available, it will navigate through pages. If not, it will progress through menu items or similar list-based UI elements. Additionally, this PR fixes inconsistencies in wrap-around behavior and navigation index calculations. Places where the navigation system was updated: - Home Page - Settings Pages - My Library Page - WiFi Selection Page - OPDS Browser Page - Keyboard - File Transfer Page - XTC Chapter Selector Page - EPUB Chapter Selector Page I’ve tested this on the device as much as possible and tried to match the existing behavior. Please let me know if I missed anything. Thanks 🙏 ![crosspoint](https://github.com/user-attachments/assets/6a3c7482-f45e-4a77-b156-721bb3b679e6) --- Following the request from @osteotek and @daveallie for system-wide support, the old PR (#379) has been closed in favor of this consolidated, system-wide implementation. --- ### AI Usage Did you use AI tools to help write this code? _**PARTIALLY**_ --------- Co-authored-by: Dave Allie --- .../browser/OpdsBookBrowserActivity.cpp | 44 ++++--- .../browser/OpdsBookBrowserActivity.h | 2 + src/activities/home/HomeActivity.cpp | 21 ++- src/activities/home/HomeActivity.h | 2 + src/activities/home/MyLibraryActivity.cpp | 41 +++--- src/activities/home/MyLibraryActivity.h | 3 + src/activities/home/RecentBooksActivity.cpp | 39 +++--- src/activities/home/RecentBooksActivity.h | 2 + .../network/NetworkModeSelectionActivity.cpp | 17 +-- .../network/NetworkModeSelectionActivity.h | 3 + .../network/WifiSelectionActivity.cpp | 24 ++-- .../network/WifiSelectionActivity.h | 2 + .../EpubReaderChapterSelectionActivity.cpp | 45 +++---- .../EpubReaderChapterSelectionActivity.h | 2 + .../reader/EpubReaderMenuActivity.cpp | 21 +-- .../reader/EpubReaderMenuActivity.h | 2 + .../EpubReaderPercentSelectionActivity.cpp | 22 +--- .../EpubReaderPercentSelectionActivity.h | 2 + .../XtcReaderChapterSelectionActivity.cpp | 53 +++----- .../XtcReaderChapterSelectionActivity.h | 2 + .../settings/CalibreSettingsActivity.cpp | 15 ++- .../settings/CalibreSettingsActivity.h | 2 + .../settings/KOReaderSettingsActivity.cpp | 15 ++- .../settings/KOReaderSettingsActivity.h | 2 + src/activities/settings/SettingsActivity.cpp | 38 +++--- src/activities/settings/SettingsActivity.h | 2 + src/activities/util/KeyboardEntryActivity.cpp | 70 ++++------ src/activities/util/KeyboardEntryActivity.h | 2 + src/main.cpp | 2 + src/util/ButtonNavigator.cpp | 124 ++++++++++++++++++ src/util/ButtonNavigator.h | 53 ++++++++ 31 files changed, 408 insertions(+), 266 deletions(-) create mode 100644 src/util/ButtonNavigator.cpp create mode 100644 src/util/ButtonNavigator.h diff --git a/src/activities/browser/OpdsBookBrowserActivity.cpp b/src/activities/browser/OpdsBookBrowserActivity.cpp index f33bda04..340b5444 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.cpp +++ b/src/activities/browser/OpdsBookBrowserActivity.cpp @@ -17,7 +17,6 @@ namespace { constexpr int PAGE_ITEMS = 23; -constexpr int SKIP_PAGE_MS = 700; } // namespace void OpdsBookBrowserActivity::taskTrampoline(void* param) { @@ -118,12 +117,6 @@ void OpdsBookBrowserActivity::loop() { // Handle browsing state if (state == BrowserState::BROWSING) { - const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) || - mappedInput.wasReleased(MappedInputManager::Button::Left); - const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) || - mappedInput.wasReleased(MappedInputManager::Button::Right); - const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; - if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (!entries.empty()) { const auto& entry = entries[selectorIndex]; @@ -135,20 +128,29 @@ void OpdsBookBrowserActivity::loop() { } } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { navigateBack(); - } else if (prevReleased && !entries.empty()) { - if (skipPage) { - selectorIndex = ((selectorIndex / PAGE_ITEMS - 1) * PAGE_ITEMS + entries.size()) % entries.size(); - } else { - selectorIndex = (selectorIndex + entries.size() - 1) % entries.size(); - } - updateRequired = true; - } else if (nextReleased && !entries.empty()) { - if (skipPage) { - selectorIndex = ((selectorIndex / PAGE_ITEMS + 1) * PAGE_ITEMS) % entries.size(); - } else { - selectorIndex = (selectorIndex + 1) % entries.size(); - } - updateRequired = true; + } + + // Handle navigation + if (!entries.empty()) { + buttonNavigator.onNextRelease([this] { + selectorIndex = ButtonNavigator::nextIndex(selectorIndex, entries.size()); + updateRequired = true; + }); + + buttonNavigator.onPreviousRelease([this] { + selectorIndex = ButtonNavigator::previousIndex(selectorIndex, entries.size()); + updateRequired = true; + }); + + buttonNavigator.onNextContinuous([this] { + selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, entries.size(), PAGE_ITEMS); + updateRequired = true; + }); + + buttonNavigator.onPreviousContinuous([this] { + selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, entries.size(), PAGE_ITEMS); + updateRequired = true; + }); } } } diff --git a/src/activities/browser/OpdsBookBrowserActivity.h b/src/activities/browser/OpdsBookBrowserActivity.h index b08d9c2a..e778f6b7 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.h +++ b/src/activities/browser/OpdsBookBrowserActivity.h @@ -9,6 +9,7 @@ #include #include "../ActivityWithSubactivity.h" +#include "util/ButtonNavigator.h" /** * Activity for browsing and downloading books from an OPDS server. @@ -37,6 +38,7 @@ class OpdsBookBrowserActivity final : public ActivityWithSubactivity { private: TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; bool updateRequired = false; BrowserState state = BrowserState::LOADING; diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 4155799c..5ae4ea5d 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -196,13 +196,18 @@ void HomeActivity::freeCoverBuffer() { } void HomeActivity::loop() { - const bool prevPressed = mappedInput.wasPressed(MappedInputManager::Button::Up) || - mappedInput.wasPressed(MappedInputManager::Button::Left); - const bool nextPressed = mappedInput.wasPressed(MappedInputManager::Button::Down) || - mappedInput.wasPressed(MappedInputManager::Button::Right); - const int menuCount = getMenuItemCount(); + buttonNavigator.onNext([this, menuCount] { + selectorIndex = ButtonNavigator::nextIndex(selectorIndex, menuCount); + updateRequired = true; + }); + + buttonNavigator.onPrevious([this, menuCount] { + selectorIndex = ButtonNavigator::previousIndex(selectorIndex, menuCount); + updateRequired = true; + }); + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { // Calculate dynamic indices based on which options are available int idx = 0; @@ -226,12 +231,6 @@ void HomeActivity::loop() { } else if (menuSelectedIndex == settingsIdx) { onSettingsOpen(); } - } else if (prevPressed) { - selectorIndex = (selectorIndex + menuCount - 1) % menuCount; - updateRequired = true; - } else if (nextPressed) { - selectorIndex = (selectorIndex + 1) % menuCount; - updateRequired = true; } } diff --git a/src/activities/home/HomeActivity.h b/src/activities/home/HomeActivity.h index 1f714217..8ec68777 100644 --- a/src/activities/home/HomeActivity.h +++ b/src/activities/home/HomeActivity.h @@ -8,6 +8,7 @@ #include "../Activity.h" #include "./MyLibraryActivity.h" +#include "util/ButtonNavigator.h" struct RecentBook; struct Rect; @@ -15,6 +16,7 @@ struct Rect; class HomeActivity final : public Activity { TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; int selectorIndex = 0; bool updateRequired = false; bool recentsLoading = false; diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index e0a36b97..52b2fe13 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -11,7 +11,6 @@ #include "util/StringUtils.h" namespace { -constexpr int SKIP_PAGE_MS = 700; constexpr unsigned long GO_HOME_MS = 1000; } // namespace @@ -109,13 +108,6 @@ void MyLibraryActivity::loop() { return; } - const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Left) || - mappedInput.wasReleased(MappedInputManager::Button::Up); - ; - const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Right) || - mappedInput.wasReleased(MappedInputManager::Button::Down); - - const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, false); if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { @@ -157,21 +149,26 @@ void MyLibraryActivity::loop() { } int listSize = static_cast(files.size()); - if (upReleased) { - if (skipPage) { - selectorIndex = std::max(static_cast((selectorIndex / pageItems - 1) * pageItems), 0); - } else { - selectorIndex = (selectorIndex + listSize - 1) % listSize; - } + + buttonNavigator.onNextRelease([this, listSize] { + selectorIndex = ButtonNavigator::nextIndex(static_cast(selectorIndex), listSize); updateRequired = true; - } else if (downReleased) { - if (skipPage) { - selectorIndex = std::min(static_cast((selectorIndex / pageItems + 1) * pageItems), listSize - 1); - } else { - selectorIndex = (selectorIndex + 1) % listSize; - } + }); + + buttonNavigator.onPreviousRelease([this, listSize] { + selectorIndex = ButtonNavigator::previousIndex(static_cast(selectorIndex), listSize); updateRequired = true; - } + }); + + buttonNavigator.onNextContinuous([this, listSize, pageItems] { + selectorIndex = ButtonNavigator::nextPageIndex(static_cast(selectorIndex), listSize, pageItems); + updateRequired = true; + }); + + buttonNavigator.onPreviousContinuous([this, listSize, pageItems] { + selectorIndex = ButtonNavigator::previousPageIndex(static_cast(selectorIndex), listSize, pageItems); + updateRequired = true; + }); } void MyLibraryActivity::displayTaskLoop() { @@ -217,4 +214,4 @@ size_t MyLibraryActivity::findEntry(const std::string& name) const { for (size_t i = 0; i < files.size(); i++) if (files[i] == name) return i; return 0; -} +} \ No newline at end of file diff --git a/src/activities/home/MyLibraryActivity.h b/src/activities/home/MyLibraryActivity.h index 70e9e29c..0713524d 100644 --- a/src/activities/home/MyLibraryActivity.h +++ b/src/activities/home/MyLibraryActivity.h @@ -8,11 +8,14 @@ #include #include "../Activity.h" +#include "RecentBooksStore.h" +#include "util/ButtonNavigator.h" class MyLibraryActivity final : public Activity { private: TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; size_t selectorIndex = 0; bool updateRequired = false; diff --git a/src/activities/home/RecentBooksActivity.cpp b/src/activities/home/RecentBooksActivity.cpp index 4add9b84..657d05c9 100644 --- a/src/activities/home/RecentBooksActivity.cpp +++ b/src/activities/home/RecentBooksActivity.cpp @@ -12,7 +12,6 @@ #include "util/StringUtils.h" namespace { -constexpr int SKIP_PAGE_MS = 700; constexpr unsigned long GO_HOME_MS = 1000; } // namespace @@ -70,13 +69,6 @@ void RecentBooksActivity::onExit() { } void RecentBooksActivity::loop() { - const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Left) || - mappedInput.wasReleased(MappedInputManager::Button::Up); - ; - const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Right) || - mappedInput.wasReleased(MappedInputManager::Button::Down); - - const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; const int pageItems = UITheme::getInstance().getNumberOfItemsPerPage(renderer, true, false, true, true); if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { @@ -92,21 +84,26 @@ void RecentBooksActivity::loop() { } int listSize = static_cast(recentBooks.size()); - if (upReleased) { - if (skipPage) { - selectorIndex = std::max(static_cast((selectorIndex / pageItems - 1) * pageItems), 0); - } else { - selectorIndex = (selectorIndex + listSize - 1) % listSize; - } + + buttonNavigator.onNextRelease([this, listSize] { + selectorIndex = ButtonNavigator::nextIndex(static_cast(selectorIndex), listSize); updateRequired = true; - } else if (downReleased) { - if (skipPage) { - selectorIndex = std::min(static_cast((selectorIndex / pageItems + 1) * pageItems), listSize - 1); - } else { - selectorIndex = (selectorIndex + 1) % listSize; - } + }); + + buttonNavigator.onPreviousRelease([this, listSize] { + selectorIndex = ButtonNavigator::previousIndex(static_cast(selectorIndex), listSize); updateRequired = true; - } + }); + + buttonNavigator.onNextContinuous([this, listSize, pageItems] { + selectorIndex = ButtonNavigator::nextPageIndex(static_cast(selectorIndex), listSize, pageItems); + updateRequired = true; + }); + + buttonNavigator.onPreviousContinuous([this, listSize, pageItems] { + selectorIndex = ButtonNavigator::previousPageIndex(static_cast(selectorIndex), listSize, pageItems); + updateRequired = true; + }); } void RecentBooksActivity::displayTaskLoop() { diff --git a/src/activities/home/RecentBooksActivity.h b/src/activities/home/RecentBooksActivity.h index 4490aeac..fee89981 100644 --- a/src/activities/home/RecentBooksActivity.h +++ b/src/activities/home/RecentBooksActivity.h @@ -9,11 +9,13 @@ #include "../Activity.h" #include "RecentBooksStore.h" +#include "util/ButtonNavigator.h" class RecentBooksActivity final : public Activity { private: TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; size_t selectorIndex = 0; bool updateRequired = false; diff --git a/src/activities/network/NetworkModeSelectionActivity.cpp b/src/activities/network/NetworkModeSelectionActivity.cpp index e6713ea0..bee13d8c 100644 --- a/src/activities/network/NetworkModeSelectionActivity.cpp +++ b/src/activities/network/NetworkModeSelectionActivity.cpp @@ -73,18 +73,15 @@ void NetworkModeSelectionActivity::loop() { } // Handle navigation - const bool prevPressed = mappedInput.wasPressed(MappedInputManager::Button::Up) || - mappedInput.wasPressed(MappedInputManager::Button::Left); - const bool nextPressed = mappedInput.wasPressed(MappedInputManager::Button::Down) || - mappedInput.wasPressed(MappedInputManager::Button::Right); + buttonNavigator.onNext([this] { + selectedIndex = ButtonNavigator::nextIndex(selectedIndex, MENU_ITEM_COUNT); + updateRequired = true; + }); - if (prevPressed) { - selectedIndex = (selectedIndex + MENU_ITEM_COUNT - 1) % MENU_ITEM_COUNT; + buttonNavigator.onPrevious([this] { + selectedIndex = ButtonNavigator::previousIndex(selectedIndex, MENU_ITEM_COUNT); updateRequired = true; - } else if (nextPressed) { - selectedIndex = (selectedIndex + 1) % MENU_ITEM_COUNT; - updateRequired = true; - } + }); } void NetworkModeSelectionActivity::displayTaskLoop() { diff --git a/src/activities/network/NetworkModeSelectionActivity.h b/src/activities/network/NetworkModeSelectionActivity.h index 1b93b825..5441e1af 100644 --- a/src/activities/network/NetworkModeSelectionActivity.h +++ b/src/activities/network/NetworkModeSelectionActivity.h @@ -6,6 +6,7 @@ #include #include "../Activity.h" +#include "util/ButtonNavigator.h" // Enum for network mode selection enum class NetworkMode { JOIN_NETWORK, CONNECT_CALIBRE, CREATE_HOTSPOT }; @@ -22,6 +23,8 @@ enum class NetworkMode { JOIN_NETWORK, CONNECT_CALIBRE, CREATE_HOTSPOT }; class NetworkModeSelectionActivity final : public Activity { TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; + int selectedIndex = 0; bool updateRequired = false; const std::function onModeSelected; diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index b80cf65d..5475251e 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -420,20 +420,16 @@ void WifiSelectionActivity::loop() { return; } - // Handle UP/DOWN navigation - if (mappedInput.wasPressed(MappedInputManager::Button::Up) || - mappedInput.wasPressed(MappedInputManager::Button::Left)) { - if (selectedNetworkIndex > 0) { - selectedNetworkIndex--; - updateRequired = true; - } - } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || - mappedInput.wasPressed(MappedInputManager::Button::Right)) { - if (!networks.empty() && selectedNetworkIndex < static_cast(networks.size()) - 1) { - selectedNetworkIndex++; - updateRequired = true; - } - } + // Handle navigation + buttonNavigator.onNext([this] { + selectedNetworkIndex = ButtonNavigator::nextIndex(selectedNetworkIndex, networks.size()); + updateRequired = true; + }); + + buttonNavigator.onPrevious([this] { + selectedNetworkIndex = ButtonNavigator::previousIndex(selectedNetworkIndex, networks.size()); + updateRequired = true; + }); } } diff --git a/src/activities/network/WifiSelectionActivity.h b/src/activities/network/WifiSelectionActivity.h index 0a7e7166..ae1702ea 100644 --- a/src/activities/network/WifiSelectionActivity.h +++ b/src/activities/network/WifiSelectionActivity.h @@ -10,6 +10,7 @@ #include #include "activities/ActivityWithSubactivity.h" +#include "util/ButtonNavigator.h" // Structure to hold WiFi network information struct WifiNetworkInfo { @@ -45,6 +46,7 @@ enum class WifiSelectionState { class WifiSelectionActivity final : public ActivityWithSubactivity { TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; bool updateRequired = false; WifiSelectionState state = WifiSelectionState::SCANNING; int selectedNetworkIndex = 0; diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index ae3a032c..9a11b1a3 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -6,11 +6,6 @@ #include "components/UITheme.h" #include "fontIds.h" -namespace { -// Time threshold for treating a long press as a page-up/page-down -constexpr int SKIP_PAGE_MS = 700; -} // namespace - int EpubReaderChapterSelectionActivity::getTotalItems() const { return epub->getTocItemsCount(); } int EpubReaderChapterSelectionActivity::getPageItems() const { @@ -77,12 +72,6 @@ void EpubReaderChapterSelectionActivity::loop() { return; } - const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) || - mappedInput.wasReleased(MappedInputManager::Button::Left); - const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) || - mappedInput.wasReleased(MappedInputManager::Button::Right); - - const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; const int pageItems = getPageItems(); const int totalItems = getTotalItems(); @@ -95,21 +84,27 @@ void EpubReaderChapterSelectionActivity::loop() { } } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { onGoBack(); - } else if (prevReleased) { - if (skipPage) { - selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + totalItems) % totalItems; - } else { - selectorIndex = (selectorIndex + totalItems - 1) % totalItems; - } - updateRequired = true; - } else if (nextReleased) { - if (skipPage) { - selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % totalItems; - } else { - selectorIndex = (selectorIndex + 1) % totalItems; - } - updateRequired = true; } + + buttonNavigator.onNextRelease([this, totalItems] { + selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems); + updateRequired = true; + }); + + buttonNavigator.onPreviousRelease([this, totalItems] { + selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems); + updateRequired = true; + }); + + buttonNavigator.onNextContinuous([this, totalItems, pageItems] { + selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems); + updateRequired = true; + }); + + buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] { + selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems); + updateRequired = true; + }); } void EpubReaderChapterSelectionActivity::displayTaskLoop() { diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.h b/src/activities/reader/EpubReaderChapterSelectionActivity.h index c43469d0..325d562a 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.h +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.h @@ -7,12 +7,14 @@ #include #include "../ActivityWithSubactivity.h" +#include "util/ButtonNavigator.h" class EpubReaderChapterSelectionActivity final : public ActivityWithSubactivity { std::shared_ptr epub; std::string epubPath; TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; int currentSpineIndex = 0; int currentPage = 0; int totalPagesInSpine = 0; diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp index 732cff5e..58ec6c4e 100644 --- a/src/activities/reader/EpubReaderMenuActivity.cpp +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -48,16 +48,19 @@ void EpubReaderMenuActivity::loop() { return; } + // Handle navigation + buttonNavigator.onNext([this] { + selectedIndex = ButtonNavigator::nextIndex(selectedIndex, static_cast(menuItems.size())); + updateRequired = true; + }); + + buttonNavigator.onPrevious([this] { + selectedIndex = ButtonNavigator::previousIndex(selectedIndex, static_cast(menuItems.size())); + updateRequired = true; + }); + // Use local variables for items we need to check after potential deletion - if (mappedInput.wasReleased(MappedInputManager::Button::Up) || - mappedInput.wasReleased(MappedInputManager::Button::Left)) { - selectedIndex = (selectedIndex + menuItems.size() - 1) % menuItems.size(); - updateRequired = true; - } else if (mappedInput.wasReleased(MappedInputManager::Button::Down) || - mappedInput.wasReleased(MappedInputManager::Button::Right)) { - selectedIndex = (selectedIndex + 1) % menuItems.size(); - updateRequired = true; - } else if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { + if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { const auto selectedAction = menuItems[selectedIndex].action; if (selectedAction == MenuAction::ROTATE_SCREEN) { // Cycle orientation preview locally; actual rotation happens on menu exit. diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h index c24a610e..1f34b208 100644 --- a/src/activities/reader/EpubReaderMenuActivity.h +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -9,6 +9,7 @@ #include #include "../ActivityWithSubactivity.h" +#include "util/ButtonNavigator.h" class EpubReaderMenuActivity final : public ActivityWithSubactivity { public: @@ -48,6 +49,7 @@ class EpubReaderMenuActivity final : public ActivityWithSubactivity { bool updateRequired = false; TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; std::string title = "Reader Menu"; uint8_t pendingOrientation = 0; const std::vector orientationLabels = {"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}; diff --git a/src/activities/reader/EpubReaderPercentSelectionActivity.cpp b/src/activities/reader/EpubReaderPercentSelectionActivity.cpp index 74dd5229..ec7293d8 100644 --- a/src/activities/reader/EpubReaderPercentSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderPercentSelectionActivity.cpp @@ -79,25 +79,11 @@ void EpubReaderPercentSelectionActivity::loop() { return; } - if (mappedInput.wasReleased(MappedInputManager::Button::Left)) { - adjustPercent(-kSmallStep); - return; - } + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { adjustPercent(-kSmallStep); }); + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { adjustPercent(kSmallStep); }); - if (mappedInput.wasReleased(MappedInputManager::Button::Right)) { - adjustPercent(kSmallStep); - return; - } - - if (mappedInput.wasReleased(MappedInputManager::Button::Up)) { - adjustPercent(kLargeStep); - return; - } - - if (mappedInput.wasReleased(MappedInputManager::Button::Down)) { - adjustPercent(-kLargeStep); - return; - } + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this] { adjustPercent(kLargeStep); }); + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] { adjustPercent(-kLargeStep); }); } void EpubReaderPercentSelectionActivity::renderScreen() { diff --git a/src/activities/reader/EpubReaderPercentSelectionActivity.h b/src/activities/reader/EpubReaderPercentSelectionActivity.h index 56238935..8d3ec96f 100644 --- a/src/activities/reader/EpubReaderPercentSelectionActivity.h +++ b/src/activities/reader/EpubReaderPercentSelectionActivity.h @@ -7,6 +7,7 @@ #include "MappedInputManager.h" #include "activities/ActivityWithSubactivity.h" +#include "util/ButtonNavigator.h" class EpubReaderPercentSelectionActivity final : public ActivityWithSubactivity { public: @@ -31,6 +32,7 @@ class EpubReaderPercentSelectionActivity final : public ActivityWithSubactivity // FreeRTOS task and mutex for rendering. TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; // Callback invoked when the user confirms a percent. const std::function onSelect; diff --git a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp index 51f4a6db..378924f0 100644 --- a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp @@ -8,10 +8,6 @@ #include "components/UITheme.h" #include "fontIds.h" -namespace { -constexpr int SKIP_PAGE_MS = 700; -} // namespace - int XtcReaderChapterSelectionActivity::getPageItems() const { constexpr int lineHeight = 30; @@ -78,13 +74,8 @@ void XtcReaderChapterSelectionActivity::onExit() { } void XtcReaderChapterSelectionActivity::loop() { - const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::Up) || - mappedInput.wasReleased(MappedInputManager::Button::Left); - const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::Down) || - mappedInput.wasReleased(MappedInputManager::Button::Right); - - const bool skipPage = mappedInput.getHeldTime() > SKIP_PAGE_MS; const int pageItems = getPageItems(); + const int totalItems = static_cast(xtc->getChapters().size()); if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { const auto& chapters = xtc->getChapters(); @@ -93,29 +84,27 @@ void XtcReaderChapterSelectionActivity::loop() { } } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { onGoBack(); - } else if (prevReleased) { - const int total = static_cast(xtc->getChapters().size()); - if (total == 0) { - return; - } - if (skipPage) { - selectorIndex = ((selectorIndex / pageItems - 1) * pageItems + total) % total; - } else { - selectorIndex = (selectorIndex + total - 1) % total; - } - updateRequired = true; - } else if (nextReleased) { - const int total = static_cast(xtc->getChapters().size()); - if (total == 0) { - return; - } - if (skipPage) { - selectorIndex = ((selectorIndex / pageItems + 1) * pageItems) % total; - } else { - selectorIndex = (selectorIndex + 1) % total; - } - updateRequired = true; } + + buttonNavigator.onNextRelease([this, totalItems] { + selectorIndex = ButtonNavigator::nextIndex(selectorIndex, totalItems); + updateRequired = true; + }); + + buttonNavigator.onPreviousRelease([this, totalItems] { + selectorIndex = ButtonNavigator::previousIndex(selectorIndex, totalItems); + updateRequired = true; + }); + + buttonNavigator.onNextContinuous([this, totalItems, pageItems] { + selectorIndex = ButtonNavigator::nextPageIndex(selectorIndex, totalItems, pageItems); + updateRequired = true; + }); + + buttonNavigator.onPreviousContinuous([this, totalItems, pageItems] { + selectorIndex = ButtonNavigator::previousPageIndex(selectorIndex, totalItems, pageItems); + updateRequired = true; + }); } void XtcReaderChapterSelectionActivity::displayTaskLoop() { diff --git a/src/activities/reader/XtcReaderChapterSelectionActivity.h b/src/activities/reader/XtcReaderChapterSelectionActivity.h index f0fe06bb..c4de4f0b 100644 --- a/src/activities/reader/XtcReaderChapterSelectionActivity.h +++ b/src/activities/reader/XtcReaderChapterSelectionActivity.h @@ -7,11 +7,13 @@ #include #include "../Activity.h" +#include "util/ButtonNavigator.h" class XtcReaderChapterSelectionActivity final : public Activity { std::shared_ptr xtc; TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; uint32_t currentPage = 0; int selectorIndex = 0; bool updateRequired = false; diff --git a/src/activities/settings/CalibreSettingsActivity.cpp b/src/activities/settings/CalibreSettingsActivity.cpp index 86a1a070..7b7a0ed4 100644 --- a/src/activities/settings/CalibreSettingsActivity.cpp +++ b/src/activities/settings/CalibreSettingsActivity.cpp @@ -63,15 +63,16 @@ void CalibreSettingsActivity::loop() { return; } - if (mappedInput.wasPressed(MappedInputManager::Button::Up) || - mappedInput.wasPressed(MappedInputManager::Button::Left)) { - selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS; - updateRequired = true; - } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || - mappedInput.wasPressed(MappedInputManager::Button::Right)) { + // Handle navigation + buttonNavigator.onNext([this] { selectedIndex = (selectedIndex + 1) % MENU_ITEMS; updateRequired = true; - } + }); + + buttonNavigator.onPrevious([this] { + selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS; + updateRequired = true; + }); } void CalibreSettingsActivity::handleSelection() { diff --git a/src/activities/settings/CalibreSettingsActivity.h b/src/activities/settings/CalibreSettingsActivity.h index 49695c62..53de46bc 100644 --- a/src/activities/settings/CalibreSettingsActivity.h +++ b/src/activities/settings/CalibreSettingsActivity.h @@ -6,6 +6,7 @@ #include #include "activities/ActivityWithSubactivity.h" +#include "util/ButtonNavigator.h" /** * Submenu for OPDS Browser settings. @@ -24,6 +25,7 @@ class CalibreSettingsActivity final : public ActivityWithSubactivity { private: TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; bool updateRequired = false; int selectedIndex = 0; diff --git a/src/activities/settings/KOReaderSettingsActivity.cpp b/src/activities/settings/KOReaderSettingsActivity.cpp index 278ce7cd..a72151d6 100644 --- a/src/activities/settings/KOReaderSettingsActivity.cpp +++ b/src/activities/settings/KOReaderSettingsActivity.cpp @@ -64,15 +64,16 @@ void KOReaderSettingsActivity::loop() { return; } - if (mappedInput.wasPressed(MappedInputManager::Button::Up) || - mappedInput.wasPressed(MappedInputManager::Button::Left)) { - selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS; - updateRequired = true; - } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || - mappedInput.wasPressed(MappedInputManager::Button::Right)) { + // Handle navigation + buttonNavigator.onNext([this] { selectedIndex = (selectedIndex + 1) % MENU_ITEMS; updateRequired = true; - } + }); + + buttonNavigator.onPrevious([this] { + selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS; + updateRequired = true; + }); } void KOReaderSettingsActivity::handleSelection() { diff --git a/src/activities/settings/KOReaderSettingsActivity.h b/src/activities/settings/KOReaderSettingsActivity.h index 2bedf034..24f2f820 100644 --- a/src/activities/settings/KOReaderSettingsActivity.h +++ b/src/activities/settings/KOReaderSettingsActivity.h @@ -6,6 +6,7 @@ #include #include "activities/ActivityWithSubactivity.h" +#include "util/ButtonNavigator.h" /** * Submenu for KOReader Sync settings. @@ -24,6 +25,7 @@ class KOReaderSettingsActivity final : public ActivityWithSubactivity { private: TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; bool updateRequired = false; int selectedIndex = 0; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 967a8342..14a2f8a0 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -16,10 +16,6 @@ const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"}; -namespace { -constexpr int changeTabsMs = 700; -} // namespace - void SettingsActivity::taskTrampoline(void* param) { auto* self = static_cast(param); self->displayTaskLoop(); @@ -116,28 +112,28 @@ void SettingsActivity::loop() { return; } - const bool upReleased = mappedInput.wasReleased(MappedInputManager::Button::Up); - const bool downReleased = mappedInput.wasReleased(MappedInputManager::Button::Down); - const bool leftReleased = mappedInput.wasReleased(MappedInputManager::Button::Left); - const bool rightReleased = mappedInput.wasReleased(MappedInputManager::Button::Right); - const bool changeTab = mappedInput.getHeldTime() > changeTabsMs; - // Handle navigation - if (upReleased && changeTab) { + buttonNavigator.onNextRelease([this] { + selectedSettingIndex = ButtonNavigator::nextIndex(selectedSettingIndex, settingsCount + 1); + updateRequired = true; + }); + + buttonNavigator.onPreviousRelease([this] { + selectedSettingIndex = ButtonNavigator::previousIndex(selectedSettingIndex, settingsCount + 1); + updateRequired = true; + }); + + buttonNavigator.onNextContinuous([this, &hasChangedCategory] { hasChangedCategory = true; - selectedCategoryIndex = (selectedCategoryIndex > 0) ? (selectedCategoryIndex - 1) : (categoryCount - 1); + selectedCategoryIndex = ButtonNavigator::nextIndex(selectedCategoryIndex, categoryCount); updateRequired = true; - } else if (downReleased && changeTab) { + }); + + buttonNavigator.onPreviousContinuous([this, &hasChangedCategory] { hasChangedCategory = true; - selectedCategoryIndex = (selectedCategoryIndex < categoryCount - 1) ? (selectedCategoryIndex + 1) : 0; + selectedCategoryIndex = ButtonNavigator::previousIndex(selectedCategoryIndex, categoryCount); updateRequired = true; - } else if (upReleased || leftReleased) { - selectedSettingIndex = (selectedSettingIndex > 0) ? (selectedSettingIndex - 1) : (settingsCount); - updateRequired = true; - } else if (rightReleased || downReleased) { - selectedSettingIndex = (selectedSettingIndex < settingsCount) ? (selectedSettingIndex + 1) : 0; - updateRequired = true; - } + }); if (hasChangedCategory) { selectedSettingIndex = (selectedSettingIndex == 0) ? 0 : 1; diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 70248bb0..04ead1e0 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -8,6 +8,7 @@ #include #include "activities/ActivityWithSubactivity.h" +#include "util/ButtonNavigator.h" class CrossPointSettings; @@ -124,6 +125,7 @@ struct SettingInfo { class SettingsActivity final : public ActivityWithSubactivity { TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; bool updateRequired = false; int selectedCategoryIndex = 0; // Currently selected category int selectedSettingIndex = 0; diff --git a/src/activities/util/KeyboardEntryActivity.cpp b/src/activities/util/KeyboardEntryActivity.cpp index bc0bdff8..40f2eaa6 100644 --- a/src/activities/util/KeyboardEntryActivity.cpp +++ b/src/activities/util/KeyboardEntryActivity.cpp @@ -142,37 +142,24 @@ void KeyboardEntryActivity::handleKeyPress() { } void KeyboardEntryActivity::loop() { - // Navigation - if (mappedInput.wasPressed(MappedInputManager::Button::Up)) { - if (selectedRow > 0) { - selectedRow--; - // Clamp column to valid range for new row - const int maxCol = getRowLength(selectedRow) - 1; - if (selectedCol > maxCol) selectedCol = maxCol; - } else { - // Wrap to bottom row - selectedRow = NUM_ROWS - 1; - const int maxCol = getRowLength(selectedRow) - 1; - if (selectedCol > maxCol) selectedCol = maxCol; - } - updateRequired = true; - } + // Handle navigation + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Up}, [this] { + selectedRow = ButtonNavigator::previousIndex(selectedRow, NUM_ROWS); - if (mappedInput.wasPressed(MappedInputManager::Button::Down)) { - if (selectedRow < NUM_ROWS - 1) { - selectedRow++; - const int maxCol = getRowLength(selectedRow) - 1; - if (selectedCol > maxCol) selectedCol = maxCol; - } else { - // Wrap to top row - selectedRow = 0; - const int maxCol = getRowLength(selectedRow) - 1; - if (selectedCol > maxCol) selectedCol = maxCol; - } + const int maxCol = getRowLength(selectedRow) - 1; + if (selectedCol > maxCol) selectedCol = maxCol; updateRequired = true; - } + }); - if (mappedInput.wasPressed(MappedInputManager::Button::Left)) { + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Down}, [this] { + selectedRow = ButtonNavigator::nextIndex(selectedRow, NUM_ROWS); + + const int maxCol = getRowLength(selectedRow) - 1; + if (selectedCol > maxCol) selectedCol = maxCol; + updateRequired = true; + }); + + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Left}, [this] { const int maxCol = getRowLength(selectedRow) - 1; // Special bottom row case @@ -191,20 +178,14 @@ void KeyboardEntryActivity::loop() { // At done button, move to backspace selectedCol = BACKSPACE_COL; } - updateRequired = true; - return; - } - - if (selectedCol > 0) { - selectedCol--; } else { - // Wrap to end of current row - selectedCol = maxCol; + selectedCol = ButtonNavigator::previousIndex(selectedCol, maxCol + 1); } - updateRequired = true; - } - if (mappedInput.wasPressed(MappedInputManager::Button::Right)) { + updateRequired = true; + }); + + buttonNavigator.onPressAndContinuous({MappedInputManager::Button::Right}, [this] { const int maxCol = getRowLength(selectedRow) - 1; // Special bottom row case @@ -223,18 +204,11 @@ void KeyboardEntryActivity::loop() { // At done button, wrap to beginning of row selectedCol = SHIFT_COL; } - updateRequired = true; - return; - } - - if (selectedCol < maxCol) { - selectedCol++; } else { - // Wrap to beginning of current row - selectedCol = 0; + selectedCol = ButtonNavigator::nextIndex(selectedCol, maxCol + 1); } updateRequired = true; - } + }); // Selection if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { diff --git a/src/activities/util/KeyboardEntryActivity.h b/src/activities/util/KeyboardEntryActivity.h index 0f05bae5..8e94fd3c 100644 --- a/src/activities/util/KeyboardEntryActivity.h +++ b/src/activities/util/KeyboardEntryActivity.h @@ -9,6 +9,7 @@ #include #include "../Activity.h" +#include "util/ButtonNavigator.h" /** * Reusable keyboard entry activity for text input. @@ -65,6 +66,7 @@ class KeyboardEntryActivity : public Activity { bool isPassword; TaskHandle_t displayTaskHandle = nullptr; SemaphoreHandle_t renderingMutex = nullptr; + ButtonNavigator buttonNavigator; bool updateRequired = false; // Keyboard state diff --git a/src/main.cpp b/src/main.cpp index 33515bce..80dd36ac 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -27,6 +27,7 @@ #include "activities/util/FullScreenMessageActivity.h" #include "components/UITheme.h" #include "fontIds.h" +#include "util/ButtonNavigator.h" HalDisplay display; HalGPIO gpio; @@ -304,6 +305,7 @@ void setup() { SETTINGS.loadFromFile(); KOREADER_STORE.loadFromFile(); UITheme::getInstance().reload(); + ButtonNavigator::setMappedInputManager(mappedInputManager); switch (gpio.getWakeupReason()) { case HalGPIO::WakeupReason::PowerButton: diff --git a/src/util/ButtonNavigator.cpp b/src/util/ButtonNavigator.cpp new file mode 100644 index 00000000..d9844138 --- /dev/null +++ b/src/util/ButtonNavigator.cpp @@ -0,0 +1,124 @@ +#include "ButtonNavigator.h" + +const MappedInputManager* ButtonNavigator::mappedInput = nullptr; + +void ButtonNavigator::onNext(const Callback& callback) { + onNextPress(callback); + onNextContinuous(callback); +} + +void ButtonNavigator::onPrevious(const Callback& callback) { + onPreviousPress(callback); + onPreviousContinuous(callback); +} + +void ButtonNavigator::onPressAndContinuous(const Buttons& buttons, const Callback& callback) { + onPress(buttons, callback); + onContinuous(buttons, callback); +} + +void ButtonNavigator::onNextPress(const Callback& callback) { onPress(getNextButtons(), callback); } + +void ButtonNavigator::onPreviousPress(const Callback& callback) { onPress(getPreviousButtons(), callback); } + +void ButtonNavigator::onNextRelease(const Callback& callback) { onRelease(getNextButtons(), callback); } + +void ButtonNavigator::onPreviousRelease(const Callback& callback) { onRelease(getPreviousButtons(), callback); } + +void ButtonNavigator::onNextContinuous(const Callback& callback) { onContinuous(getNextButtons(), callback); } + +void ButtonNavigator::onPreviousContinuous(const Callback& callback) { onContinuous(getPreviousButtons(), callback); } + +void ButtonNavigator::onPress(const Buttons& buttons, const Callback& callback) { + const bool wasPressed = std::any_of(buttons.begin(), buttons.end(), [](const MappedInputManager::Button button) { + return mappedInput != nullptr && mappedInput->wasPressed(button); + }); + + if (wasPressed) { + callback(); + } +} + +void ButtonNavigator::onRelease(const Buttons& buttons, const Callback& callback) { + const bool wasReleased = std::any_of(buttons.begin(), buttons.end(), [](const MappedInputManager::Button button) { + return mappedInput != nullptr && mappedInput->wasReleased(button); + }); + + if (wasReleased) { + if (lastContinuousNavTime == 0) { + callback(); + } + + lastContinuousNavTime = 0; + } +} + +void ButtonNavigator::onContinuous(const Buttons& buttons, const Callback& callback) { + const bool isPressed = std::any_of(buttons.begin(), buttons.end(), [this](const MappedInputManager::Button button) { + return mappedInput != nullptr && mappedInput->isPressed(button) && shouldNavigateContinuously(); + }); + + if (isPressed) { + callback(); + lastContinuousNavTime = millis(); + } +} + +bool ButtonNavigator::shouldNavigateContinuously() const { + if (!mappedInput) return false; + + const bool buttonHeldLongEnough = mappedInput->getHeldTime() > continuousStartMs; + const bool navigationIntervalElapsed = (millis() - lastContinuousNavTime) > continuousIntervalMs; + + return buttonHeldLongEnough && navigationIntervalElapsed; +} + +int ButtonNavigator::nextIndex(const int currentIndex, const int totalItems) { + if (totalItems <= 0) return 0; + + // Calculate the next index with wrap-around + return (currentIndex + 1) % totalItems; +} + +int ButtonNavigator::previousIndex(const int currentIndex, const int totalItems) { + if (totalItems <= 0) return 0; + + // Calculate the previous index with wrap-around + return (currentIndex + totalItems - 1) % totalItems; +} + +int ButtonNavigator::nextPageIndex(const int currentIndex, const int totalItems, const int itemsPerPage) { + if (totalItems <= 0 || itemsPerPage <= 0) return 0; + + // When items fit on one page, use index navigation instead + if (totalItems <= itemsPerPage) { + return nextIndex(currentIndex, totalItems); + } + + const int lastPageIndex = (totalItems - 1) / itemsPerPage; + const int currentPageIndex = currentIndex / itemsPerPage; + + if (currentPageIndex < lastPageIndex) { + return (currentPageIndex + 1) * itemsPerPage; + } + + return 0; +} + +int ButtonNavigator::previousPageIndex(const int currentIndex, const int totalItems, const int itemsPerPage) { + if (totalItems <= 0 || itemsPerPage <= 0) return 0; + + // When items fit on one page, use index navigation instead + if (totalItems <= itemsPerPage) { + return previousIndex(currentIndex, totalItems); + } + + const int lastPageIndex = (totalItems - 1) / itemsPerPage; + const int currentPageIndex = currentIndex / itemsPerPage; + + if (currentPageIndex > 0) { + return (currentPageIndex - 1) * itemsPerPage; + } + + return lastPageIndex * itemsPerPage; +} diff --git a/src/util/ButtonNavigator.h b/src/util/ButtonNavigator.h new file mode 100644 index 00000000..2f9afbc1 --- /dev/null +++ b/src/util/ButtonNavigator.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include + +#include "MappedInputManager.h" + +class ButtonNavigator final { + using Callback = std::function; + using Buttons = std::vector; + + const uint16_t continuousStartMs; + const uint16_t continuousIntervalMs; + uint32_t lastContinuousNavTime = 0; + static const MappedInputManager* mappedInput; + + [[nodiscard]] bool shouldNavigateContinuously() const; + + public: + explicit ButtonNavigator(const uint16_t continuousIntervalMs = 500, const uint16_t continuousStartMs = 500) + : continuousStartMs(continuousStartMs), continuousIntervalMs(continuousIntervalMs) {} + + static void setMappedInputManager(const MappedInputManager& mappedInputManager) { mappedInput = &mappedInputManager; } + + void onNext(const Callback& callback); + void onPrevious(const Callback& callback); + void onPressAndContinuous(const Buttons& buttons, const Callback& callback); + + void onNextPress(const Callback& callback); + void onPreviousPress(const Callback& callback); + void onPress(const Buttons& buttons, const Callback& callback); + + void onNextRelease(const Callback& callback); + void onPreviousRelease(const Callback& callback); + void onRelease(const Buttons& buttons, const Callback& callback); + + void onNextContinuous(const Callback& callback); + void onPreviousContinuous(const Callback& callback); + void onContinuous(const Buttons& buttons, const Callback& callback); + + [[nodiscard]] static int nextIndex(int currentIndex, int totalItems); + [[nodiscard]] static int previousIndex(int currentIndex, int totalItems); + + [[nodiscard]] static int nextPageIndex(int currentIndex, int totalItems, int itemsPerPage); + [[nodiscard]] static int previousPageIndex(int currentIndex, int totalItems, int itemsPerPage); + + [[nodiscard]] static Buttons getNextButtons() { + return {MappedInputManager::Button::Down, MappedInputManager::Button::Right}; + } + [[nodiscard]] static Buttons getPreviousButtons() { + return {MappedInputManager::Button::Up, MappedInputManager::Button::Left}; + } +}; \ No newline at end of file From 14ef625679dbd5bbc326e99ef9be3b95c43f8066 Mon Sep 17 00:00:00 2001 From: harshit181 Date: Mon, 9 Feb 2026 16:42:21 +0530 Subject: [PATCH 18/29] fix: issue if book href are absolute url and not relative to server (#741) ## Summary fixing issue if book href are absolute url and not relative to the server ## Additional Context * Fixes https://github.com/crosspoint-reader/crosspoint-reader/issues/632 * https://github.com/harshit181/RSSPub/issues/43 --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**< PARTIALLY>**_ --- src/util/UrlUtils.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/util/UrlUtils.cpp b/src/util/UrlUtils.cpp index bbf5ac15..a2156552 100644 --- a/src/util/UrlUtils.cpp +++ b/src/util/UrlUtils.cpp @@ -25,6 +25,10 @@ std::string extractHost(const std::string& url) { } std::string buildUrl(const std::string& serverUrl, const std::string& path) { + // If path is already an absolute URL (has protocol), use it directly + if (path.find("://") != std::string::npos) { + return path; + } const std::string urlWithProtocol = ensureProtocol(serverUrl); if (path.empty()) { return urlWithProtocol; From b5d28a3a9c2c09e74a6c4e1c4f93bc8f3faae333 Mon Sep 17 00:00:00 2001 From: ThatCrispyToast <61917048+ThatCrispyToast@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:09:24 -0500 Subject: [PATCH 19/29] feat: use natural sort in file browser (#722) ## Summary * **What is the goal of this PR?** (e.g., Implements the new feature for file uploading.) Implement natural sort (e.g. "file1.txt, file2.txt, file10.txt" instead of "file1.txt, file10.txt, file2.txt") for files in the MyLibraryActivity menu * **What changes are included?** Modifies the `sortFileList` function under `src/activities/home/MyLibraryActivity.cpp` to use natural sort as opposed to lexicographical sort ## Additional Context I wasn't entirely sure whether or not i should make this a configurable option, but most file browsers and directory listing tools have this set as an immutable default, so I opted against it. * Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks, specific areas to focus on). --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**NO**_ --- src/activities/home/MyLibraryActivity.cpp | 52 ++++++++++++++++++++--- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index 52b2fe13..3d5dbf1a 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -16,11 +16,53 @@ constexpr unsigned long GO_HOME_MS = 1000; void sortFileList(std::vector& strs) { std::sort(begin(strs), end(strs), [](const std::string& str1, const std::string& str2) { - if (str1.back() == '/' && str2.back() != '/') return true; - if (str1.back() != '/' && str2.back() == '/') return false; - return lexicographical_compare( - begin(str1), end(str1), begin(str2), end(str2), - [](const char& char1, const char& char2) { return tolower(char1) < tolower(char2); }); + // Directories first + bool isDir1 = str1.back() == '/'; + bool isDir2 = str2.back() == '/'; + if (isDir1 != isDir2) return isDir1; + + // Start naive natural sort + const char* s1 = str1.c_str(); + const char* s2 = str2.c_str(); + + // Iterate while both strings have characters + while (*s1 && *s2) { + // Check if both are at the start of a number + if (isdigit(*s1) && isdigit(*s2)) { + // Skip leading zeros and track them + const char* start1 = s1; + const char* start2 = s2; + while (*s1 == '0') s1++; + while (*s2 == '0') s2++; + + // Count digits to compare lengths first + int len1 = 0, len2 = 0; + while (isdigit(s1[len1])) len1++; + while (isdigit(s2[len2])) len2++; + + // Different length so return smaller integer value + if (len1 != len2) return len1 < len2; + + // Same length so compare digit by digit + for (int i = 0; i < len1; i++) { + if (s1[i] != s2[i]) return s1[i] < s2[i]; + } + + // Numbers equal so advance pointers + s1 += len1; + s2 += len2; + } else { + // Regular case-insensitive character comparison + char c1 = tolower(*s1); + char c2 = tolower(*s2); + if (c1 != c2) return c1 < c2; + s1++; + s2++; + } + } + + // One string is prefix of other + return *s1 == '\0' && *s2 != '\0'; }); } From 98e67896265c56bdf1cbc8a33ebef450ccf151f6 Mon Sep 17 00:00:00 2001 From: Eliz Date: Tue, 10 Feb 2026 09:41:44 +0000 Subject: [PATCH 20/29] feat: Connect to last wifi by default (#752) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary * **What is the goal of this PR?** Use last connected network as default * **What changes are included?** - Refactor how an action type of Settings are handled - Add a new System Settings option → Network - Add the ability to forget a network in the Network Selection Screen - Add the ability to Refresh network list - Save the last connected network SSID - Use the last connection whenever network is needed (OPDS, Koreader sync, update etc) ## Additional Context * Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks, specific areas to focus on). ![IMG_6504](https://github.com/user-attachments/assets/e48fb013-b5c3-45c0-b284-e183e6fd5a68) ![IMG_6503](https://github.com/user-attachments/assets/78c4b6b6-4e7b-4656-b356-19d65ff6aa12) https://github.com/user-attachments/assets/95bf34a8-44ce-4279-8cd8-f78524ce745b --- ### AI Usage Did you use AI tools to help write this code? _** PARTIALLY: I wrote most of it but I also used Gemini as assist. --------- Co-authored-by: Eliz Kilic Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/WifiCredentialStore.cpp | 31 +++++- src/WifiCredentialStore.h | 6 + .../network/WifiSelectionActivity.cpp | 103 ++++++++++++++---- .../network/WifiSelectionActivity.h | 13 ++- src/activities/settings/SettingsActivity.cpp | 84 +++++++------- src/activities/settings/SettingsActivity.h | 16 ++- 6 files changed, 183 insertions(+), 70 deletions(-) diff --git a/src/WifiCredentialStore.cpp b/src/WifiCredentialStore.cpp index 59c3a7d3..4ac6629b 100644 --- a/src/WifiCredentialStore.cpp +++ b/src/WifiCredentialStore.cpp @@ -9,7 +9,7 @@ WifiCredentialStore WifiCredentialStore::instance; namespace { // File format version -constexpr uint8_t WIFI_FILE_VERSION = 1; +constexpr uint8_t WIFI_FILE_VERSION = 2; // Increased version // WiFi credentials file path constexpr char WIFI_FILE[] = "/.crosspoint/wifi.bin"; @@ -38,6 +38,7 @@ bool WifiCredentialStore::saveToFile() const { // Write header serialization::writePod(file, WIFI_FILE_VERSION); + serialization::writeString(file, lastConnectedSsid); // Save last connected SSID serialization::writePod(file, static_cast(credentials.size())); // Write each credential @@ -67,12 +68,18 @@ bool WifiCredentialStore::loadFromFile() { // Read and verify version uint8_t version; serialization::readPod(file, version); - if (version != WIFI_FILE_VERSION) { + if (version > WIFI_FILE_VERSION) { Serial.printf("[%lu] [WCS] Unknown file version: %u\n", millis(), version); file.close(); return false; } + if (version >= 2) { + serialization::readString(file, lastConnectedSsid); + } else { + lastConnectedSsid.clear(); + } + // Read credential count uint8_t count; serialization::readPod(file, count); @@ -128,6 +135,9 @@ bool WifiCredentialStore::removeCredential(const std::string& ssid) { if (cred != credentials.end()) { credentials.erase(cred); Serial.printf("[%lu] [WCS] Removed credentials for: %s\n", millis(), ssid.c_str()); + if (ssid == lastConnectedSsid) { + clearLastConnectedSsid(); + } return saveToFile(); } return false; // Not found @@ -146,8 +156,25 @@ const WifiCredential* WifiCredentialStore::findCredential(const std::string& ssi bool WifiCredentialStore::hasSavedCredential(const std::string& ssid) const { return findCredential(ssid) != nullptr; } +void WifiCredentialStore::setLastConnectedSsid(const std::string& ssid) { + if (lastConnectedSsid != ssid) { + lastConnectedSsid = ssid; + saveToFile(); + } +} + +const std::string& WifiCredentialStore::getLastConnectedSsid() const { return lastConnectedSsid; } + +void WifiCredentialStore::clearLastConnectedSsid() { + if (!lastConnectedSsid.empty()) { + lastConnectedSsid.clear(); + saveToFile(); + } +} + void WifiCredentialStore::clearAll() { credentials.clear(); + lastConnectedSsid.clear(); saveToFile(); Serial.printf("[%lu] [WCS] Cleared all WiFi credentials\n", millis()); } diff --git a/src/WifiCredentialStore.h b/src/WifiCredentialStore.h index 0004dc9b..2e2fd6ba 100644 --- a/src/WifiCredentialStore.h +++ b/src/WifiCredentialStore.h @@ -16,6 +16,7 @@ class WifiCredentialStore { private: static WifiCredentialStore instance; std::vector credentials; + std::string lastConnectedSsid; static constexpr size_t MAX_NETWORKS = 8; @@ -48,6 +49,11 @@ class WifiCredentialStore { // Check if a network is saved bool hasSavedCredential(const std::string& ssid) const; + // Last connected network + void setLastConnectedSsid(const std::string& ssid); + const std::string& getLastConnectedSsid() const; + void clearLastConnectedSsid(); + // Clear all credentials void clearAll(); }; diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index 5475251e..83af4e07 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -21,7 +21,8 @@ void WifiSelectionActivity::onEnter() { renderingMutex = xSemaphoreCreateMutex(); - // Load saved WiFi credentials - SD card operations need lock as we use SPI for both + // Load saved WiFi credentials - SD card operations need lock as we use SPI + // for both xSemaphoreTake(renderingMutex, portMAX_DELAY); WIFI_STORE.loadFromFile(); xSemaphoreGive(renderingMutex); @@ -37,6 +38,7 @@ void WifiSelectionActivity::onEnter() { usedSavedPassword = false; savePromptSelection = 0; forgetPromptSelection = 0; + autoConnecting = false; // Cache MAC address for display uint8_t mac[6]; @@ -46,9 +48,7 @@ void WifiSelectionActivity::onEnter() { mac[5]); cachedMacAddress = std::string(macStr); - // Trigger first update to show scanning message - updateRequired = true; - + // Task creation xTaskCreate(&WifiSelectionActivity::taskTrampoline, "WifiSelectionTask", 4096, // Stack size (larger for WiFi operations) this, // Parameters @@ -56,7 +56,26 @@ void WifiSelectionActivity::onEnter() { &displayTaskHandle // Task handle ); - // Start WiFi scan + // Attempt to auto-connect to the last network + if (allowAutoConnect) { + const std::string lastSsid = WIFI_STORE.getLastConnectedSsid(); + if (!lastSsid.empty()) { + const auto* cred = WIFI_STORE.findCredential(lastSsid); + if (cred) { + Serial.printf("[%lu] [WIFI] Attempting to auto-connect to %s\n", millis(), lastSsid.c_str()); + selectedSSID = cred->ssid; + enteredPassword = cred->password; + selectedRequiresPassword = !cred->password.empty(); + usedSavedPassword = true; + autoConnecting = true; + attemptConnection(); + updateRequired = true; + return; + } + } + } + + // Fallback to scanning startWifiScan(); } @@ -70,15 +89,17 @@ void WifiSelectionActivity::onExit() { WiFi.scanDelete(); Serial.printf("[%lu] [WIFI] [MEM] Free heap after scanDelete: %d bytes\n", millis(), ESP.getFreeHeap()); - // Note: We do NOT disconnect WiFi here - the parent activity (CrossPointWebServerActivity) - // manages WiFi connection state. We just clean up the scan and task. + // Note: We do NOT disconnect WiFi here - the parent activity + // (CrossPointWebServerActivity) manages WiFi connection state. We just clean + // up the scan and task. // Acquire mutex before deleting task to ensure task isn't using it // This prevents hangs/crashes if the task holds the mutex when deleted Serial.printf("[%lu] [WIFI] Acquiring rendering mutex before task deletion...\n", millis()); xSemaphoreTake(renderingMutex, portMAX_DELAY); - // Delete the display task (we now hold the mutex, so task is blocked if it needs it) + // Delete the display task (we now hold the mutex, so task is blocked if it + // needs it) Serial.printf("[%lu] [WIFI] Deleting display task...\n", millis()); if (displayTaskHandle) { vTaskDelete(displayTaskHandle); @@ -96,6 +117,7 @@ void WifiSelectionActivity::onExit() { } void WifiSelectionActivity::startWifiScan() { + autoConnecting = false; state = WifiSelectionState::SCANNING; networks.clear(); updateRequired = true; @@ -181,6 +203,7 @@ void WifiSelectionActivity::selectNetwork(const int index) { selectedRequiresPassword = network.isEncrypted; usedSavedPassword = false; enteredPassword.clear(); + autoConnecting = false; // Check if we have saved credentials for this network const auto* savedCred = WIFI_STORE.findCredential(selectedSSID); @@ -223,7 +246,7 @@ void WifiSelectionActivity::selectNetwork(const int index) { } void WifiSelectionActivity::attemptConnection() { - state = WifiSelectionState::CONNECTING; + state = autoConnecting ? WifiSelectionState::AUTO_CONNECTING : WifiSelectionState::CONNECTING; connectionStartTime = millis(); connectedIP.clear(); connectionError.clear(); @@ -239,7 +262,7 @@ void WifiSelectionActivity::attemptConnection() { } void WifiSelectionActivity::checkConnectionStatus() { - if (state != WifiSelectionState::CONNECTING) { + if (state != WifiSelectionState::CONNECTING && state != WifiSelectionState::AUTO_CONNECTING) { return; } @@ -251,6 +274,13 @@ void WifiSelectionActivity::checkConnectionStatus() { char ipStr[16]; snprintf(ipStr, sizeof(ipStr), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]); connectedIP = ipStr; + autoConnecting = false; + + // Save this as the last connected network - SD card operations need lock as + // we use SPI for both + xSemaphoreTake(renderingMutex, portMAX_DELAY); + WIFI_STORE.setLastConnectedSsid(selectedSSID); + xSemaphoreGive(renderingMutex); // If we entered a new password, ask if user wants to save it // Otherwise, immediately complete so parent can start web server @@ -260,7 +290,10 @@ void WifiSelectionActivity::checkConnectionStatus() { updateRequired = true; } else { // Using saved password or open network - complete immediately - Serial.printf("[%lu] [WIFI] Connected with saved/open credentials, completing immediately\n", millis()); + Serial.printf( + "[%lu] [WIFI] Connected with saved/open credentials, " + "completing immediately\n", + millis()); onComplete(true); } return; @@ -299,7 +332,7 @@ void WifiSelectionActivity::loop() { } // Check connection progress - if (state == WifiSelectionState::CONNECTING) { + if (state == WifiSelectionState::CONNECTING || state == WifiSelectionState::AUTO_CONNECTING) { checkConnectionStatus(); return; } @@ -368,17 +401,16 @@ void WifiSelectionActivity::loop() { } } // Go back to network list (whether Cancel or Forget network was selected) - state = WifiSelectionState::NETWORK_LIST; - updateRequired = true; + startWifiScan(); } else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { // Skip forgetting, go back to network list - state = WifiSelectionState::NETWORK_LIST; - updateRequired = true; + startWifiScan(); } return; } - // Handle connected state (should not normally be reached - connection completes immediately) + // Handle connected state (should not normally be reached - connection + // completes immediately) if (state == WifiSelectionState::CONNECTED) { // Safety fallback - immediately complete onComplete(true); @@ -389,12 +421,14 @@ void WifiSelectionActivity::loop() { if (state == WifiSelectionState::CONNECTION_FAILED) { if (mappedInput.wasPressed(MappedInputManager::Button::Back) || mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { - // If we used saved credentials, offer to forget the network - if (usedSavedPassword) { + // If we were auto-connecting or using a saved credential, offer to forget + // the network + if (autoConnecting || usedSavedPassword) { + autoConnecting = false; state = WifiSelectionState::FORGET_PROMPT; forgetPromptSelection = 0; // Default to "Cancel" } else { - // Go back to network list on failure + // Go back to network list on failure for non-saved credentials state = WifiSelectionState::NETWORK_LIST; } updateRequired = true; @@ -420,6 +454,23 @@ void WifiSelectionActivity::loop() { return; } + if (mappedInput.wasPressed(MappedInputManager::Button::Right)) { + startWifiScan(); + return; + } + + const bool leftPressed = mappedInput.wasPressed(MappedInputManager::Button::Left); + if (leftPressed) { + const bool hasSavedPassword = !networks.empty() && networks[selectedNetworkIndex].hasSavedPassword; + if (hasSavedPassword) { + selectedSSID = networks[selectedNetworkIndex].ssid; + state = WifiSelectionState::FORGET_PROMPT; + forgetPromptSelection = 0; // Default to "Cancel" + updateRequired = true; + return; + } + } + // Handle navigation buttonNavigator.onNext([this] { selectedNetworkIndex = ButtonNavigator::nextIndex(selectedNetworkIndex, networks.size()); @@ -479,6 +530,9 @@ void WifiSelectionActivity::render() const { renderer.clearScreen(); switch (state) { + case WifiSelectionState::AUTO_CONNECTING: + renderConnecting(); + break; case WifiSelectionState::SCANNING: renderConnecting(); // Reuse connecting screen with different message break; @@ -582,7 +636,11 @@ void WifiSelectionActivity::renderNetworkList() const { // Draw help text renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 75, "* = Encrypted | + = Saved"); - const auto labels = mappedInput.mapLabels("« Back", "Connect", "", ""); + + const bool hasSavedPassword = !networks.empty() && networks[selectedNetworkIndex].hasSavedPassword; + const char* forgetLabel = hasSavedPassword ? "Forget" : ""; + + const auto labels = mappedInput.mapLabels("« Back", "Connect", forgetLabel, "Refresh"); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } @@ -686,8 +744,7 @@ void WifiSelectionActivity::renderForgetPrompt() const { const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto top = (pageHeight - height * 3) / 2; - renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Connection Failed", true, EpdFontFamily::BOLD); - + renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Forget Network", true, EpdFontFamily::BOLD); std::string ssidInfo = "Network: " + selectedSSID; if (ssidInfo.length() > 28) { ssidInfo.replace(25, ssidInfo.length() - 25, "..."); diff --git a/src/activities/network/WifiSelectionActivity.h b/src/activities/network/WifiSelectionActivity.h index ae1702ea..32eb36db 100644 --- a/src/activities/network/WifiSelectionActivity.h +++ b/src/activities/network/WifiSelectionActivity.h @@ -22,6 +22,7 @@ struct WifiNetworkInfo { // WiFi selection states enum class WifiSelectionState { + AUTO_CONNECTING, // Trying to connect to the last known network SCANNING, // Scanning for networks NETWORK_LIST, // Displaying available networks PASSWORD_ENTRY, // Entering password for selected network @@ -70,6 +71,12 @@ class WifiSelectionActivity final : public ActivityWithSubactivity { // Whether network was connected using a saved password (skip save prompt) bool usedSavedPassword = false; + // Whether to attempt auto-connect on entry + const bool allowAutoConnect; + + // Whether we are attempting to auto-connect + bool autoConnecting = false; + // Save/forget prompt selection (0 = Yes, 1 = No) int savePromptSelection = 0; int forgetPromptSelection = 0; @@ -98,8 +105,10 @@ class WifiSelectionActivity final : public ActivityWithSubactivity { public: explicit WifiSelectionActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onComplete) - : ActivityWithSubactivity("WifiSelection", renderer, mappedInput), onComplete(onComplete) {} + const std::function& onComplete, bool autoConnect = true) + : ActivityWithSubactivity("WifiSelection", renderer, mappedInput), + onComplete(onComplete), + allowAutoConnect(autoConnect) {} void onEnter() override; void onExit() override; void loop() override; diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 14a2f8a0..7d3a6016 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -11,6 +11,7 @@ #include "MappedInputManager.h" #include "OtaUpdateActivity.h" #include "SettingsList.h" +#include "activities/network/WifiSelectionActivity.h" #include "components/UITheme.h" #include "fontIds.h" @@ -46,11 +47,13 @@ void SettingsActivity::onEnter() { } // Append device-only ACTION items - controlsSettings.insert(controlsSettings.begin(), SettingInfo::Action("Remap Front Buttons")); - systemSettings.push_back(SettingInfo::Action("KOReader Sync")); - systemSettings.push_back(SettingInfo::Action("OPDS Browser")); - systemSettings.push_back(SettingInfo::Action("Clear Cache")); - systemSettings.push_back(SettingInfo::Action("Check for updates")); + controlsSettings.insert(controlsSettings.begin(), + SettingInfo::Action("Remap Front Buttons", SettingAction::RemapFrontButtons)); + systemSettings.push_back(SettingInfo::Action("Network", SettingAction::Network)); + systemSettings.push_back(SettingInfo::Action("KOReader Sync", SettingAction::KOReaderSync)); + systemSettings.push_back(SettingInfo::Action("OPDS Browser", SettingAction::OPDSBrowser)); + systemSettings.push_back(SettingInfo::Action("Clear Cache", SettingAction::ClearCache)); + systemSettings.push_back(SettingInfo::Action("Check for updates", SettingAction::CheckForUpdates)); // Reset selection to first category selectedCategoryIndex = 0; @@ -178,46 +181,45 @@ void SettingsActivity::toggleCurrentSetting() { SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step; } } else if (setting.type == SettingType::ACTION) { - if (strcmp(setting.name, "Remap Front Buttons") == 0) { + auto enterSubActivity = [this](Activity* activity) { xSemaphoreTake(renderingMutex, portMAX_DELAY); exitActivity(); - enterNewActivity(new ButtonRemapActivity(renderer, mappedInput, [this] { - exitActivity(); - updateRequired = true; - })); + enterNewActivity(activity); xSemaphoreGive(renderingMutex); - } else if (strcmp(setting.name, "KOReader Sync") == 0) { - xSemaphoreTake(renderingMutex, portMAX_DELAY); + }; + + auto onComplete = [this] { exitActivity(); - enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] { - exitActivity(); - updateRequired = true; - })); - xSemaphoreGive(renderingMutex); - } else if (strcmp(setting.name, "OPDS Browser") == 0) { - xSemaphoreTake(renderingMutex, portMAX_DELAY); + updateRequired = true; + }; + + auto onCompleteBool = [this](bool) { exitActivity(); - enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] { - exitActivity(); - updateRequired = true; - })); - xSemaphoreGive(renderingMutex); - } else if (strcmp(setting.name, "Clear Cache") == 0) { - xSemaphoreTake(renderingMutex, portMAX_DELAY); - exitActivity(); - enterNewActivity(new ClearCacheActivity(renderer, mappedInput, [this] { - exitActivity(); - updateRequired = true; - })); - xSemaphoreGive(renderingMutex); - } else if (strcmp(setting.name, "Check for updates") == 0) { - xSemaphoreTake(renderingMutex, portMAX_DELAY); - exitActivity(); - enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] { - exitActivity(); - updateRequired = true; - })); - xSemaphoreGive(renderingMutex); + updateRequired = true; + }; + + switch (setting.action) { + case SettingAction::RemapFrontButtons: + enterSubActivity(new ButtonRemapActivity(renderer, mappedInput, onComplete)); + break; + case SettingAction::KOReaderSync: + enterSubActivity(new KOReaderSettingsActivity(renderer, mappedInput, onComplete)); + break; + case SettingAction::OPDSBrowser: + enterSubActivity(new CalibreSettingsActivity(renderer, mappedInput, onComplete)); + break; + case SettingAction::Network: + enterSubActivity(new WifiSelectionActivity(renderer, mappedInput, onCompleteBool, false)); + break; + case SettingAction::ClearCache: + enterSubActivity(new ClearCacheActivity(renderer, mappedInput, onComplete)); + break; + case SettingAction::CheckForUpdates: + enterSubActivity(new OtaUpdateActivity(renderer, mappedInput, onComplete)); + break; + case SettingAction::None: + // Do nothing + break; } } else { return; @@ -289,4 +291,4 @@ void SettingsActivity::render() const { // Always use standard refresh for settings screen renderer.displayBuffer(); -} +} \ No newline at end of file diff --git a/src/activities/settings/SettingsActivity.h b/src/activities/settings/SettingsActivity.h index 04ead1e0..1417c17d 100644 --- a/src/activities/settings/SettingsActivity.h +++ b/src/activities/settings/SettingsActivity.h @@ -14,11 +14,22 @@ class CrossPointSettings; enum class SettingType { TOGGLE, ENUM, ACTION, VALUE, STRING }; +enum class SettingAction { + None, + RemapFrontButtons, + KOReaderSync, + OPDSBrowser, + Network, + ClearCache, + CheckForUpdates, +}; + struct SettingInfo { const char* name; SettingType type; uint8_t CrossPointSettings::* valuePtr = nullptr; std::vector enumValues; + SettingAction action = SettingAction::None; struct ValueRange { uint8_t min; @@ -63,10 +74,11 @@ struct SettingInfo { return s; } - static SettingInfo Action(const char* name) { + static SettingInfo Action(const char* name, SettingAction action) { SettingInfo s; s.name = name; s.type = SettingType::ACTION; + s.action = action; return s; } @@ -156,4 +168,4 @@ class SettingsActivity final : public ActivityWithSubactivity { void onEnter() override; void onExit() override; void loop() override; -}; +}; \ No newline at end of file From 3a12ca27258f8cb1f275fadff051e659e8c29b00 Mon Sep 17 00:00:00 2001 From: Jonas Diemer Date: Tue, 10 Feb 2026 12:04:32 +0100 Subject: [PATCH 21/29] docs: Update USER_GUIDE.md (#808) Added info about optimizing EPUB. ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? no --- USER_GUIDE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 6975e944..f3139521 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -230,6 +230,7 @@ Accessible by pressing **Confirm** while inside a book. Please note that this firmware is currently in active development. The following features are **not yet supported** but are planned for future updates: * **Images:** Embedded images in e-books will not render. +* **Cover Images:** Large cover images embedded into EPUB require several seconds (~10s for ~2000 pixel tall image) to convert for sleep screen and home screen thumbnail. Consider optimizing the EPUB with e.g. https://github.com/bigbag/epub-to-xtc-converter to speed this up. --- From 0c2df24f5c9939b65a751f31cc46001e64dbb64a Mon Sep 17 00:00:00 2001 From: jpirnay Date: Tue, 10 Feb 2026 12:07:56 +0100 Subject: [PATCH 22/29] feat: Extend python debugging monitor functionality (keyword filter / suppress) (#810) ## Summary * I needed the ability to filter and or suppress debug messages containig certain keywords (eg [GFX] for render related stuff) * Update of debugging_monitor.py script for development work ## Additional Context ``` usage: debugging_monitor.py [-h] [--baud BAUD] [--filter FILTER] [--suppress SUPPRESS] [port] ESP32 Monitor with Graph positional arguments: port Serial port options: -h, --help show this help message and exit --baud BAUD Baud rate --filter FILTER Only display lines containing this keyword (case-insensitive) --suppress SUPPRESS Suppress lines containing this keyword (case-insensitive) ``` * plus a couple of platform specific defaults (port, pip style) --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? NO --- scripts/debugging_monitor.py | 281 +++++++++++++++++++++++++---------- 1 file changed, 206 insertions(+), 75 deletions(-) diff --git a/scripts/debugging_monitor.py b/scripts/debugging_monitor.py index 57695e2b..f03ffa5a 100755 --- a/scripts/debugging_monitor.py +++ b/scripts/debugging_monitor.py @@ -1,32 +1,46 @@ +#!/usr/bin/env python3 +""" +ESP32 Serial Monitor with Memory Graph + +This script provides a real-time serial monitor for ESP32 devices with +integrated memory usage graphing capabilities. It reads serial output, +parses memory information, and displays it in both console and graphical form. +""" + import sys import argparse import re import threading from datetime import datetime from collections import deque -import time # Try to import potentially missing packages +PACKAGE_MAPPING: dict[str, str] = { + "serial": "pyserial", + "colorama": "colorama", + "matplotlib": "matplotlib", +} + try: import serial from colorama import init, Fore, Style import matplotlib.pyplot as plt - import matplotlib.animation as animation + from matplotlib import animation except ImportError as e: - missing_package = e.name + ERROR_MSG = str(e).lower() + missing_packages = [pkg for mod, pkg in PACKAGE_MAPPING.items() if mod in ERROR_MSG] + + if not missing_packages: + # Fallback if mapping doesn't cover + missing_packages = ["pyserial", "colorama", "matplotlib"] + print("\n" + "!" * 50) - print(f" Error: The required package '{missing_package}' is not installed.") + print(f" Error: Required package(s) not installed: {', '.join(missing_packages)}") print("!" * 50) - print(f"\nTo fix this, please run the following command in your terminal:\n") - - install_cmd = "pip install " - packages = [] - if 'serial' in str(e): packages.append("pyserial") - if 'colorama' in str(e): packages.append("colorama") - if 'matplotlib' in str(e): packages.append("matplotlib") - - print(f" {install_cmd}{' '.join(packages)}") + print("\nTo fix this, please run the following command in your terminal:\n") + INSTALL_CMD = "pip install " if sys.platform.startswith("win") else "pip3 install " + print(f" {INSTALL_CMD}{' '.join(missing_packages)}") print("\nExiting...") sys.exit(1) @@ -34,50 +48,92 @@ except ImportError as e: # --- Global Variables for Data Sharing --- # Store last 50 data points MAX_POINTS = 50 -time_data = deque(maxlen=MAX_POINTS) -free_mem_data = deque(maxlen=MAX_POINTS) -total_mem_data = deque(maxlen=MAX_POINTS) -data_lock = threading.Lock() # Prevent reading while writing +time_data: deque[str] = deque(maxlen=MAX_POINTS) +free_mem_data: deque[float] = deque(maxlen=MAX_POINTS) +total_mem_data: deque[float] = deque(maxlen=MAX_POINTS) +data_lock: threading.Lock = threading.Lock() # Prevent reading while writing # Initialize colors init(autoreset=True) -def get_color_for_line(line): +# Color mapping for log lines +COLOR_KEYWORDS: dict[str, list[str]] = { + Fore.RED: ["ERROR", "[ERR]", "[SCT]", "FAILED", "WARNING"], + Fore.CYAN: ["[MEM]", "FREE:"], + Fore.MAGENTA: [ + "[GFX]", + "[ERS]", + "DISPLAY", + "RAM WRITE", + "RAM COMPLETE", + "REFRESH", + "POWERING ON", + "FRAME BUFFER", + "LUT", + ], + Fore.GREEN: [ + "[EBP]", + "[BMC]", + "[ZIP]", + "[PARSER]", + "[EHP]", + "LOADING EPUB", + "CACHE", + "DECOMPRESSED", + "PARSING", + ], + Fore.YELLOW: ["[ACT]", "ENTERING ACTIVITY", "EXITING ACTIVITY"], + Fore.BLUE: ["RENDERED PAGE", "[LOOP]", "DURATION", "WAIT COMPLETE"], + Fore.LIGHTYELLOW_EX: [ + "[CPS]", + "SETTINGS", + "[CLEAR_CACHE]", + "[CHAP]", + "[OPDS]", + "[COF]", + ], + Fore.LIGHTBLACK_EX: [ + "ESP-ROM", + "BUILD:", + "RST:", + "BOOT:", + "SPIWP:", + "MODE:", + "LOAD:", + "ENTRY", + "[SD]", + "STARTING CROSSPOINT", + "VERSION", + ], + Fore.LIGHTCYAN_EX: ["[RBS]"], + Fore.LIGHTMAGENTA_EX: [ + "[KRS]", + "EINKDISPLAY:", + "STATIC FRAME", + "INITIALIZING", + "SPI INITIALIZED", + "GPIO PINS", + "RESETTING", + "SSD1677", + "E-INK", + ], + Fore.LIGHTGREEN_EX: ["[FNS]", "FOOTNOTE"], +} + + +# pylint: disable=R0912 +def get_color_for_line(line: str) -> str: """ Classify log lines by type and assign appropriate colors. """ line_upper = line.upper() - - if any(keyword in line_upper for keyword in ["ERROR", "[ERR]", "[SCT]", "FAILED", "WARNING"]): - return Fore.RED - if "[MEM]" in line_upper or "FREE:" in line_upper: - return Fore.CYAN - if any(keyword in line_upper for keyword in ["[GFX]", "[ERS]", "DISPLAY", "RAM WRITE", "RAM COMPLETE", "REFRESH", "POWERING ON", "FRAME BUFFER", "LUT"]): - return Fore.MAGENTA - if any(keyword in line_upper for keyword in ["[EBP]", "[BMC]", "[ZIP]", "[PARSER]", "[EHP]", "LOADING EPUB", "CACHE", "DECOMPRESSED", "PARSING"]): - return Fore.GREEN - if "[ACT]" in line_upper or "ENTERING ACTIVITY" in line_upper or "EXITING ACTIVITY" in line_upper: - return Fore.YELLOW - if any(keyword in line_upper for keyword in ["RENDERED PAGE", "[LOOP]", "DURATION", "WAIT COMPLETE"]): - return Fore.BLUE - if any(keyword in line_upper for keyword in ["[CPS]", "SETTINGS", "[CLEAR_CACHE]"]): - return Fore.LIGHTYELLOW_EX - if any(keyword in line_upper for keyword in ["ESP-ROM", "BUILD:", "RST:", "BOOT:", "SPIWP:", "MODE:", "LOAD:", "ENTRY", "[SD]", "STARTING CROSSPOINT", "VERSION"]): - return Fore.LIGHTBLACK_EX - if "[RBS]" in line_upper: - return Fore.LIGHTCYAN_EX - if "[KRS]" in line_upper: - return Fore.LIGHTMAGENTA_EX - if any(keyword in line_upper for keyword in ["EINKDISPLAY:", "STATIC FRAME", "INITIALIZING", "SPI INITIALIZED", "GPIO PINS", "RESETTING", "SSD1677", "E-INK"]): - return Fore.LIGHTMAGENTA_EX - if any(keyword in line_upper for keyword in ["[FNS]", "FOOTNOTE"]): - return Fore.LIGHTGREEN_EX - if any(keyword in line_upper for keyword in ["[CHAP]", "[OPDS]", "[COF]"]): - return Fore.LIGHTYELLOW_EX - + for color, keywords in COLOR_KEYWORDS.items(): + if any(keyword in line_upper for keyword in keywords): + return color return Fore.WHITE -def parse_memory_line(line): + +def parse_memory_line(line: str) -> tuple[int | None, int | None]: """ Extracts Free and Total bytes from the specific log line. Format: [MEM] Free: 196344 bytes, Total: 226412 bytes, Min Free: 112620 bytes @@ -93,12 +149,29 @@ def parse_memory_line(line): return None, None return None, None -def serial_worker(port, baud): + +def serial_worker(port: str, baud: int, kwargs: dict[str, str]) -> None: """ Runs in a background thread. Handles reading serial, printing to console, and updating the data lists. """ print(f"{Fore.CYAN}--- Opening {port} at {baud} baud ---{Style.RESET_ALL}") + filter_keyword = kwargs.get("filter", "").lower() + suppress = kwargs.get("suppress", "").lower() + if filter_keyword and suppress and filter_keyword == suppress: + print( + f"{Fore.YELLOW}Warning: Filter and Suppress keywords are the same. " + f"This may result in no output.{Style.RESET_ALL}" + ) + if filter_keyword: + print( + f"{Fore.YELLOW}Filtering lines to only show those containing: " + f"'{filter_keyword}'{Style.RESET_ALL}" + ) + if suppress: + print( + f"{Fore.YELLOW}Suppressing lines containing: '{suppress}'{Style.RESET_ALL}" + ) try: ser = serial.Serial(port, baud, timeout=0.1) @@ -111,7 +184,7 @@ def serial_worker(port, baud): try: while True: try: - raw_data = ser.readline().decode('utf-8', errors='replace') + raw_data = ser.readline().decode("utf-8", errors="replace") if not raw_data: continue @@ -127,88 +200,146 @@ def serial_worker(port, baud): # Check for Memory Line if "[MEM]" in formatted_line: free_val, total_val = parse_memory_line(formatted_line) - if free_val is not None: + if free_val is not None and total_val is not None: with data_lock: time_data.append(pc_time) - free_mem_data.append(free_val / 1024) # Convert to KB - total_mem_data.append(total_val / 1024) # Convert to KB - + free_mem_data.append(free_val / 1024) # Convert to KB + total_mem_data.append(total_val / 1024) # Convert to KB + # Apply filters + if filter_keyword and filter_keyword not in formatted_line.lower(): + continue + if suppress and suppress in formatted_line.lower(): + continue # Print to console line_color = get_color_for_line(formatted_line) print(f"{line_color}{formatted_line}") - except OSError: - print(f"{Fore.RED}Device disconnected.{Style.RESET_ALL}") + except (OSError, UnicodeDecodeError): + print(f"{Fore.RED}Device disconnected or data error.{Style.RESET_ALL}") break - except Exception as e: + except KeyboardInterrupt: # If thread is killed violently (e.g. main exit), silence errors pass finally: - if 'ser' in locals() and ser.is_open: + if "ser" in locals() and ser.is_open: ser.close() -def update_graph(frame): + +def update_graph(frame) -> list: # pylint: disable=unused-argument """ Called by Matplotlib animation to redraw the chart. """ with data_lock: if not time_data: - return + return [] # Convert deques to lists for plotting x = list(time_data) y_free = list(free_mem_data) y_total = list(total_mem_data) - plt.cla() # Clear axis + plt.cla() # Clear axis # Plot Total RAM - plt.plot(x, y_total, label='Total RAM (KB)', color='red', linestyle='--') + plt.plot(x, y_total, label="Total RAM (KB)", color="red", linestyle="--") # Plot Free RAM - plt.plot(x, y_free, label='Free RAM (KB)', color='green', marker='o') + plt.plot(x, y_free, label="Free RAM (KB)", color="green", marker="o") # Fill area under Free RAM - plt.fill_between(x, y_free, color='green', alpha=0.1) + plt.fill_between(x, y_free, color="green", alpha=0.1) plt.title("ESP32 Memory Monitor") plt.ylabel("Memory (KB)") plt.xlabel("Time") - plt.legend(loc='upper left') - plt.grid(True, linestyle=':', alpha=0.6) + plt.legend(loc="upper left") + plt.grid(True, linestyle=":", alpha=0.6) # Rotate date labels - plt.xticks(rotation=45, ha='right') + plt.xticks(rotation=45, ha="right") plt.tight_layout() -def main(): + return [] + + +def main() -> None: + """ + Main entry point for the ESP32 monitor application. + Sets up argument parsing, starts serial monitoring thread, and initializes the memory graph. + """ parser = argparse.ArgumentParser(description="ESP32 Monitor with Graph") - parser.add_argument("port", nargs="?", default="/dev/ttyACM0", help="Serial port") - parser.add_argument("--baud", type=int, default=115200, help="Baud rate") + if sys.platform.startswith("win"): + default_port = "COM8" + elif sys.platform.startswith("darwin"): + default_port = "/dev/cu.usbmodem101" + else: + default_port = "/dev/ttyACM0" + default_baudrate = 115200 + parser.add_argument( + "port", + nargs="?", + default=default_port, + help=f"Serial port (default: {default_port})", + ) + parser.add_argument( + "--baud", + type=int, + default=default_baudrate, + help=f"Baud rate (default: {default_baudrate})", + ) + parser.add_argument( + "--filter", + type=str, + default="", + help="Only display lines containing this keyword (case-insensitive)", + ) + parser.add_argument( + "--suppress", + type=str, + default="", + help="Suppress lines containing this keyword (case-insensitive)", + ) args = parser.parse_args() # 1. Start the Serial Reader in a separate thread # Daemon=True means this thread dies when the main program closes - t = threading.Thread(target=serial_worker, args=(args.port, args.baud), daemon=True) + myargs = vars(args) # Convert Namespace to dict for easier passing + t = threading.Thread( + target=serial_worker, args=(args.port, args.baud, myargs), daemon=True + ) t.start() # 2. Set up the Graph (Main Thread) try: - plt.style.use('light_background') - except: + import matplotlib.style as mplstyle # pylint: disable=import-outside-toplevel + default_styles = ("light_background", "ggplot", "seaborn", "dark_background", ) + styles = list(mplstyle.available) + for default_style in default_styles: + if default_style in styles: + print( + f"\n{Fore.CYAN}--- Using Matplotlib style: {default_style} ---{Style.RESET_ALL}" + ) + mplstyle.use(default_style) + break + except (AttributeError, ValueError): pass fig = plt.figure(figsize=(10, 6)) # Update graph every 1000ms - ani = animation.FuncAnimation(fig, update_graph, interval=1000) + _ = animation.FuncAnimation( + fig, update_graph, interval=1000, cache_frame_data=False + ) try: - print(f"{Fore.YELLOW}Starting Graph Window... (Close window to exit){Style.RESET_ALL}") + print( + f"{Fore.YELLOW}Starting Graph Window... (Close window to exit){Style.RESET_ALL}" + ) plt.show() except KeyboardInterrupt: print(f"\n{Fore.YELLOW}Exiting...{Style.RESET_ALL}") - plt.close('all') # Force close any lingering plot windows + plt.close("all") # Force close any lingering plot windows + if __name__ == "__main__": main() From 44452a42e9e89b796e2588c20bbd80daf86ca7a1 Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Tue, 10 Feb 2026 22:56:22 +1100 Subject: [PATCH 23/29] fix: Prevent sleeping when in OPDS browser / downloading books (#818) ## Summary * Prevent sleeping when in OPDS browser / downloading books ## Additional Context * Raised in https://github.com/crosspoint-reader/crosspoint-reader/discussions/673 --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? No --- src/activities/browser/OpdsBookBrowserActivity.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/activities/browser/OpdsBookBrowserActivity.h b/src/activities/browser/OpdsBookBrowserActivity.h index e778f6b7..879a5a39 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.h +++ b/src/activities/browser/OpdsBookBrowserActivity.h @@ -64,4 +64,5 @@ class OpdsBookBrowserActivity final : public ActivityWithSubactivity { void navigateToEntry(const OpdsEntry& entry); void navigateBack(); void downloadBook(const OpdsEntry& book); + bool preventAutoSleep() override { return true; } }; From 7e93411f4600bc650e4e2f2e3c67d8d057bd2222 Mon Sep 17 00:00:00 2001 From: Jonas Diemer Date: Tue, 10 Feb 2026 13:23:14 +0100 Subject: [PATCH 24/29] docs: Update USER_GUIDE.md (#817) Added explanation how to recover from broken config/cache. ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? no --- USER_GUIDE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/USER_GUIDE.md b/USER_GUIDE.md index f3139521..ed12494d 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -243,3 +243,5 @@ pio device monitor ``` If the device is stuck in a bootloop, press and release the Reset button. Then, press and hold on to the configured Back button and the Power Button to boot to the Home Screen. + +There can be issues with broken cache or config. In this case, delete the `.crosspoint` directory on your SD card (or consider deleting only `settings.bin`, `state.bin`, or `epub_*` cache directories in the `.crosspoint/` folder). From f5b85f5ca187bb2545ef7446b4ec01c12e813a5e Mon Sep 17 00:00:00 2001 From: Jonas Diemer Date: Tue, 10 Feb 2026 16:15:23 +0100 Subject: [PATCH 25/29] fix: Reduce MIN_SIZE_FOR_POPUP to 10KB (#809) Noticed that the Indexing... popup went missing despite 3-5 seconds delay. Reducing to 10KB, so we get a popup for delays > ~2s. ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? no --- lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index cc0ecfda..b94f9e8e 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -11,7 +11,7 @@ const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"}; constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]); // Minimum file size (in bytes) to show indexing popup - smaller chapters don't benefit from it -constexpr size_t MIN_SIZE_FOR_POPUP = 50 * 1024; // 50KB +constexpr size_t MIN_SIZE_FOR_POPUP = 10 * 1024; // 10KB const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"}; constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]); From 4a210823a879411dbfa3770b741ded3fbf200291 Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Wed, 11 Feb 2026 21:42:37 +1100 Subject: [PATCH 26/29] fix: Manually trigger GPIO update in File Browser mode (#819) ## Summary * Manually trigger GPIO update in File Browser mode * Previously just assumed that the GPIO data would update automatically (presumably via yield), the data is currently updated in the main loop (and now here as well during the middle of the processing loop). * This allows the back button to be correctly detected instead of only being checked once every 100ms or so for the button state. ## Additional Context * Fixes https://github.com/crosspoint-reader/crosspoint-reader/issues/579 --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? No ## Summary by CodeRabbit * **Bug Fixes** * Enhanced input state detection in the web server interface for more responsive and accurate user command recognition during high-frequency operations. --- src/MappedInputManager.h | 1 + src/activities/network/CrossPointWebServerActivity.cpp | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/MappedInputManager.h b/src/MappedInputManager.h index bd594a25..67f094cc 100644 --- a/src/MappedInputManager.h +++ b/src/MappedInputManager.h @@ -15,6 +15,7 @@ class MappedInputManager { explicit MappedInputManager(HalGPIO& gpio) : gpio(gpio) {} + void update() const { gpio.update(); } bool wasPressed(Button button) const; bool wasReleased(Button button) const; bool isPressed(Button button) const; diff --git a/src/activities/network/CrossPointWebServerActivity.cpp b/src/activities/network/CrossPointWebServerActivity.cpp index 0338d825..fe568503 100644 --- a/src/activities/network/CrossPointWebServerActivity.cpp +++ b/src/activities/network/CrossPointWebServerActivity.cpp @@ -348,6 +348,9 @@ void CrossPointWebServerActivity::loop() { // Yield and check for exit button every 64 iterations if ((i & 0x3F) == 0x3F) { yield(); + // Force trigger an update of which buttons are being pressed so be have accurate state + // for back button checking + mappedInput.update(); // Check for exit button inside loop for responsiveness if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { onGoBack(); From efb9b72e64d1cb3eeffe19d4de542d365e599eef Mon Sep 17 00:00:00 2001 From: Jonas Diemer Date: Wed, 11 Feb 2026 14:44:10 +0100 Subject: [PATCH 27/29] fix: Show "Back" in file browser if not in root, "Home" otherwise. (#822) ## Summary Show "Back" in file browser if not in root, "Home" otherwise. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? YES --- src/activities/home/MyLibraryActivity.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index 3d5dbf1a..9d2f4073 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -246,7 +246,7 @@ void MyLibraryActivity::render() const { } // Help text - const auto labels = mappedInput.mapLabels("« Home", "Open", "Up", "Down"); + const auto labels = mappedInput.mapLabels(basepath == "/" ? "« Home" : "« Back", "Open", "Up", "Down"); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.displayBuffer(); From 3ae1007cbe9965c8b18f4ac5c94192a5f9ab2c6a Mon Sep 17 00:00:00 2001 From: jpirnay Date: Wed, 11 Feb 2026 16:25:17 +0100 Subject: [PATCH 28/29] fix: chore: make all debug messages uniform (#825) ## Summary * Unify all serial port debug messages ## Additional Context * All messages sent to the serial port now follow the "[timestamp] [origin] payload" format (notable exception framework messages) --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? No --- src/RecentBooksStore.cpp | 2 +- src/activities/boot_sleep/SleepActivity.cpp | 14 +++++++------- src/activities/home/RecentBooksActivity.cpp | 2 +- src/activities/reader/EpubReaderActivity.cpp | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index 9885fcdb..00d641dd 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -83,7 +83,7 @@ RecentBook RecentBooksStore::getDataFromBook(std::string path) const { lastBookFileName = path.substr(lastSlash + 1); } - Serial.printf("Loading recent book: %s\n", path.c_str()); + Serial.printf("[%lu] [RBS] Loading recent book: %s\n", millis(), path.c_str()); // If epub, try to load the metadata for title/author and cover if (StringUtils::checkFileExtension(lastBookFileName, ".epub")) { diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index 846df22d..83e68d3d 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -218,12 +218,12 @@ void SleepActivity::renderCoverSleepScreen() const { // Handle XTC file Xtc lastXtc(APP_STATE.openEpubPath, "/.crosspoint"); if (!lastXtc.load()) { - Serial.println("[SLP] Failed to load last XTC"); + Serial.printf("[%lu] [SLP] Failed to load last XTC\n", millis()); return (this->*renderNoCoverSleepScreen)(); } if (!lastXtc.generateCoverBmp()) { - Serial.println("[SLP] Failed to generate XTC cover bmp"); + Serial.printf("[%lu] [SLP] Failed to generate XTC cover bmp\n", millis()); return (this->*renderNoCoverSleepScreen)(); } @@ -232,12 +232,12 @@ void SleepActivity::renderCoverSleepScreen() const { // Handle TXT file - looks for cover image in the same folder Txt lastTxt(APP_STATE.openEpubPath, "/.crosspoint"); if (!lastTxt.load()) { - Serial.println("[SLP] Failed to load last TXT"); + Serial.printf("[%lu] [SLP] Failed to load last TXT\n", millis()); return (this->*renderNoCoverSleepScreen)(); } if (!lastTxt.generateCoverBmp()) { - Serial.println("[SLP] No cover image found for TXT file"); + Serial.printf("[%lu] [SLP] No cover image found for TXT file\n", millis()); return (this->*renderNoCoverSleepScreen)(); } @@ -247,12 +247,12 @@ void SleepActivity::renderCoverSleepScreen() const { Epub lastEpub(APP_STATE.openEpubPath, "/.crosspoint"); // Skip loading css since we only need metadata here if (!lastEpub.load(true, true)) { - Serial.println("[SLP] Failed to load last epub"); + Serial.printf("[%lu] [SLP] Failed to load last epub\n", millis()); return (this->*renderNoCoverSleepScreen)(); } if (!lastEpub.generateCoverBmp(cropped)) { - Serial.println("[SLP] Failed to generate cover bmp"); + Serial.printf("[%lu] [SLP] Failed to generate cover bmp\n", millis()); return (this->*renderNoCoverSleepScreen)(); } @@ -265,7 +265,7 @@ void SleepActivity::renderCoverSleepScreen() const { if (Storage.openFileForRead("SLP", coverBmpPath, file)) { Bitmap bitmap(file); if (bitmap.parseHeaders() == BmpReaderError::Ok) { - Serial.printf("[SLP] Rendering sleep cover: %s\n", coverBmpPath.c_str()); + Serial.printf("[%lu] [SLP] Rendering sleep cover: %s\n", millis(), coverBmpPath.c_str()); renderBitmapSleepScreen(bitmap); return; } diff --git a/src/activities/home/RecentBooksActivity.cpp b/src/activities/home/RecentBooksActivity.cpp index 657d05c9..2301cbc4 100644 --- a/src/activities/home/RecentBooksActivity.cpp +++ b/src/activities/home/RecentBooksActivity.cpp @@ -73,7 +73,7 @@ void RecentBooksActivity::loop() { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (!recentBooks.empty() && selectorIndex < static_cast(recentBooks.size())) { - Serial.printf("Selected recent book: %s\n", recentBooks[selectorIndex].path.c_str()); + Serial.printf("[%lu] [RBA] Selected recent book: %s\n", millis(), recentBooks[selectorIndex].path.c_str()); onSelectBook(recentBooks[selectorIndex].path); return; } diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index fb68d62b..8b2e47e2 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -664,9 +664,9 @@ void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageC data[5] = (pageCount >> 8) & 0xFF; f.write(data, 6); f.close(); - Serial.printf("[ERS] Progress saved: Chapter %d, Page %d\n", spineIndex, currentPage); + Serial.printf("[%lu] [ERS] Progress saved: Chapter %d, Page %d\n", millis(), spineIndex, currentPage); } else { - Serial.printf("[ERS] Could not save progress!\n"); + Serial.printf("[%lu] [ERS] Could not save progress!\n", millis()); } } void EpubReaderActivity::renderContents(std::unique_ptr page, const int orientedMarginTop, From 0991782fb4e050297fc237d358dc33cf4223ce85 Mon Sep 17 00:00:00 2001 From: Xuan-Son Nguyen Date: Thu, 12 Feb 2026 09:49:05 +0100 Subject: [PATCH 29/29] feat: more power saving on idle (#801) ## Summary This PR extends the delay in main loop from 10ms to 50ms after the device is idle for a while. This translates to extended battery life in a longer period (see testing section above), while not hurting too much the user experience. With the help from [this patch](https://github.com/ngxson/crosspoint-reader/tree/xsn/measure_cpu_usage), I was able to measure the CPU usage on idle: ``` PR: [20017] [MEM] Free: 150188 bytes, Total: 232092 bytes, Min Free: 150092 bytes [20017] [IDLE] Idle time: 99.62% (CPU load: 0.38%) [30042] [MEM] Free: 150188 bytes, Total: 232092 bytes, Min Free: 150092 bytes [30042] [IDLE] Idle time: 99.63% (CPU load: 0.37%) [40067] [MEM] Free: 150188 bytes, Total: 232092 bytes, Min Free: 150092 bytes [40067] [IDLE] Idle time: 99.62% (CPU load: 0.38%) master: [20012] [MEM] Free: 195016 bytes, Total: 231532 bytes, Min Free: 132460 bytes [20012] [IDLE] Idle time: 98.53% (CPU load: 1.47%) [30017] [MEM] Free: 195016 bytes, Total: 231532 bytes, Min Free: 132460 bytes [30017] [IDLE] Idle time: 98.53% (CPU load: 1.47%) [40022] [MEM] Free: 195016 bytes, Total: 231532 bytes, Min Free: 132460 bytes [40022] [IDLE] Idle time: 98.53% (CPU load: 1.47%) ``` While this is a x3.8 reduce in CPU usage, it doesn't translate to the same amount of battery life extension in real life. The reasons are: 1. The CPU is not shut down completely 2. freeRTOS tick is still running (however, I planned to experiment with tickless functionality) 3. Current leakage to other components, for example: voltage dividers, eink screen, SD card, etc A note on [light-sleep](https://docs.espressif.com/projects/esp-idf/en/stable/esp32c3/api-reference/system/sleep_modes.html) functionality: it is not possible in our use case because: - Light-sleep for 50ms introduce too much overhead on wake up, it has negative effect on battery life - Light-sleep for longer period doesn't work because the ADC GPIO buttons cannot be used as wake up source ## Testing (duration = 6 hrs) To test this, I patched the `CrossPointSettings::getSleepTimeoutMs()` to always returns a timeout of 6 hrs. This allow me to leave the device idle for 6 hrs straight. - On master branch, 6 hrs costs 26% battery life (100% --> 74%), meaning battery life is ~23 hrs - With this PR, 6 hrs costs 20% battery life (100% --> 80%), meaning battery life is ~30 hrs So in theory, this extends the battery by about 7 hrs. Even with some error margin added, I think 3 hrs increase is possible with a normal usage setup (i.e. only read ebooks, no wifi) ## Additional Context Would appreciate if someone can test this with an oscilloscope. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? **NO** --- src/main.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index 80dd36ac..fa782556 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -410,6 +410,13 @@ void loop() { if (currentActivity && currentActivity->skipLoopDelay()) { yield(); // Give FreeRTOS a chance to run tasks, but return immediately } else { - delay(10); // Normal delay when no activity requires fast response + static constexpr unsigned long IDLE_POWER_SAVING_MS = 3000; // 3 seconds + if (millis() - lastActivityTime >= IDLE_POWER_SAVING_MS) { + // If we've been inactive for a while, increase the delay to save power + delay(50); + } else { + // Short delay to prevent tight loop while still being responsive + delay(10); + } } }