From b000b42963ae866850119246bdae7ee373f3f6c3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 20:34:52 +0000 Subject: [PATCH 01/18] Initial plan From 4d437032fb2671966c250583d97441aeb4ffece3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 20:39:38 +0000 Subject: [PATCH 02/18] Add Chapter Progress Bar status bar option Co-authored-by: lukestein <44452336+lukestein@users.noreply.github.com> --- src/CrossPointSettings.h | 1 + src/ScreenComponents.cpp | 11 +++++++++++ src/ScreenComponents.h | 1 + src/activities/reader/EpubReaderActivity.cpp | 20 +++++++++++++++++--- src/activities/reader/TxtReaderActivity.cpp | 17 ++++++++++++++--- src/activities/settings/SettingsActivity.cpp | 3 ++- 6 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index c450d348..fed2401e 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -31,6 +31,7 @@ class CrossPointSettings { FULL = 2, FULL_WITH_PROGRESS_BAR = 3, ONLY_PROGRESS_BAR = 4, + CHAPTER_PROGRESS_BAR = 5, STATUS_BAR_MODE_COUNT }; diff --git a/src/ScreenComponents.cpp b/src/ScreenComponents.cpp index ef47dfc5..a0702f6c 100644 --- a/src/ScreenComponents.cpp +++ b/src/ScreenComponents.cpp @@ -53,6 +53,17 @@ void ScreenComponents::drawBookProgressBar(const GfxRenderer& renderer, const si renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, BOOK_PROGRESS_BAR_HEIGHT, true); } +void ScreenComponents::drawChapterProgressBar(const GfxRenderer& renderer, const size_t chapterProgress) { + int vieweableMarginTop, vieweableMarginRight, vieweableMarginBottom, vieweableMarginLeft; + renderer.getOrientedViewableTRBL(&vieweableMarginTop, &vieweableMarginRight, &vieweableMarginBottom, + &vieweableMarginLeft); + + const int progressBarMaxWidth = renderer.getScreenWidth() - vieweableMarginLeft - vieweableMarginRight; + const int progressBarY = renderer.getScreenHeight() - vieweableMarginBottom - BOOK_PROGRESS_BAR_HEIGHT; + const int barWidth = progressBarMaxWidth * chapterProgress / 100; + renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, BOOK_PROGRESS_BAR_HEIGHT, true); +} + int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector& tabs) { constexpr int tabPadding = 20; // Horizontal padding between tabs constexpr int leftMargin = 20; // Left margin for first tab diff --git a/src/ScreenComponents.h b/src/ScreenComponents.h index 15403f60..9f2dec05 100644 --- a/src/ScreenComponents.h +++ b/src/ScreenComponents.h @@ -17,6 +17,7 @@ class ScreenComponents { static void drawBattery(const GfxRenderer& renderer, int left, int top, bool showPercentage = true); static void drawBookProgressBar(const GfxRenderer& renderer, size_t bookProgress); + static void drawChapterProgressBar(const GfxRenderer& renderer, size_t chapterProgress); // Draw a horizontal tab bar with underline indicator for selected tab // Returns the height of the tab bar (for positioning content below) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 58668c68..d5267170 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -467,14 +467,18 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in const bool showProgressPercentage = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR || SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR; + const bool showChapterProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR; const bool showProgressText = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL || SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR; + const bool showBookPercentage = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR; const bool showBattery = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS || SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL || - SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR; + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR || + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR; const bool showChapterTitle = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS || SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL || - SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR; + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR || + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR; const bool showBatteryPercentage = SETTINGS.hideBatteryPercentage == CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_NEVER; @@ -487,7 +491,7 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in const float sectionChapterProg = static_cast(section->currentPage) / section->pageCount; const float bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg) * 100; - if (showProgressText || showProgressPercentage) { + if (showProgressText || showProgressPercentage || showBookPercentage) { // Right aligned text for progress counter char progressStr[32]; @@ -495,6 +499,8 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in if (showProgressPercentage) { snprintf(progressStr, sizeof(progressStr), "%d/%d %.0f%%", section->currentPage + 1, section->pageCount, bookProgress); + } else if (showBookPercentage) { + snprintf(progressStr, sizeof(progressStr), "%.0f%%", bookProgress); } else { snprintf(progressStr, sizeof(progressStr), "%d/%d", section->currentPage + 1, section->pageCount); } @@ -509,6 +515,14 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in ScreenComponents::drawBookProgressBar(renderer, static_cast(bookProgress)); } + if (showChapterProgressBar) { + // Draw chapter progress bar at the very bottom of the screen, from edge to edge of viewable area + const float chapterProgress = (section->pageCount > 0) + ? (static_cast(section->currentPage + 1) / section->pageCount) * 100 + : 0; + ScreenComponents::drawChapterProgressBar(renderer, static_cast(chapterProgress)); + } + if (showBattery) { ScreenComponents::drawBattery(renderer, orientedMarginLeft + 1, textY, showBatteryPercentage); } diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index e9303de3..21d76bcf 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -519,14 +519,18 @@ void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int const bool showProgressPercentage = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR || SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR; + const bool showChapterProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR; const bool showProgressText = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL || SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR; + const bool showBookPercentage = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR; const bool showBattery = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS || SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL || - SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR; + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR || + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR; const bool showTitle = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS || SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL || - SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR; + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR || + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR; const bool showBatteryPercentage = SETTINGS.hideBatteryPercentage == CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_NEVER; @@ -536,10 +540,12 @@ void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int const float progress = totalPages > 0 ? (currentPage + 1) * 100.0f / totalPages : 0; - if (showProgressText || showProgressPercentage) { + if (showProgressText || showProgressPercentage || showBookPercentage) { char progressStr[32]; if (showProgressPercentage) { snprintf(progressStr, sizeof(progressStr), "%d/%d %.0f%%", currentPage + 1, totalPages, progress); + } else if (showBookPercentage) { + snprintf(progressStr, sizeof(progressStr), "%.0f%%", progress); } else { snprintf(progressStr, sizeof(progressStr), "%d/%d", currentPage + 1, totalPages); } @@ -554,6 +560,11 @@ void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int ScreenComponents::drawBookProgressBar(renderer, static_cast(progress)); } + if (showChapterProgressBar) { + // For text mode, treat the entire book as one chapter, so chapter progress == book progress + ScreenComponents::drawChapterProgressBar(renderer, static_cast(progress)); + } + if (showBattery) { ScreenComponents::drawBattery(renderer, orientedMarginLeft, textY, showBatteryPercentage); } diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 7316db05..55688baa 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -19,7 +19,8 @@ const SettingInfo displaySettings[displaySettingsCount] = { SettingInfo::Enum("Sleep Screen Cover Filter", &CrossPointSettings::sleepScreenCoverFilter, {"None", "Contrast", "Inverted"}), SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar, - {"None", "No Progress", "Full w/ Percentage", "Full w/ Progress Bar", "Progress Bar"}), + {"None", "No Progress", "Full w/ Percentage", "Full w/ Progress Bar", "Progress Bar", + "Chapter Progress Bar"}), SettingInfo::Enum("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}), SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency, {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})}; From 533a8f2eb4196f1a41f640e4afb25a9d5aea873f Mon Sep 17 00:00:00 2001 From: Luke Stein <44452336+lukestein@users.noreply.github.com> Date: Sat, 31 Jan 2026 15:52:07 -0500 Subject: [PATCH 03/18] Rename progress bar height variable and make 6 pixels --- src/ScreenComponents.cpp | 8 ++++---- src/ScreenComponents.h | 2 +- src/activities/reader/EpubReaderActivity.cpp | 2 +- src/activities/reader/TxtReaderActivity.cpp | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ScreenComponents.cpp b/src/ScreenComponents.cpp index a0702f6c..0e77c855 100644 --- a/src/ScreenComponents.cpp +++ b/src/ScreenComponents.cpp @@ -48,9 +48,9 @@ void ScreenComponents::drawBookProgressBar(const GfxRenderer& renderer, const si &vieweableMarginLeft); const int progressBarMaxWidth = renderer.getScreenWidth() - vieweableMarginLeft - vieweableMarginRight; - const int progressBarY = renderer.getScreenHeight() - vieweableMarginBottom - BOOK_PROGRESS_BAR_HEIGHT; + const int progressBarY = renderer.getScreenHeight() - vieweableMarginBottom - PROGRESS_BAR_HEIGHT; const int barWidth = progressBarMaxWidth * bookProgress / 100; - renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, BOOK_PROGRESS_BAR_HEIGHT, true); + renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, PROGRESS_BAR_HEIGHT, true); } void ScreenComponents::drawChapterProgressBar(const GfxRenderer& renderer, const size_t chapterProgress) { @@ -59,9 +59,9 @@ void ScreenComponents::drawChapterProgressBar(const GfxRenderer& renderer, const &vieweableMarginLeft); const int progressBarMaxWidth = renderer.getScreenWidth() - vieweableMarginLeft - vieweableMarginRight; - const int progressBarY = renderer.getScreenHeight() - vieweableMarginBottom - BOOK_PROGRESS_BAR_HEIGHT; + const int progressBarY = renderer.getScreenHeight() - vieweableMarginBottom - PROGRESS_BAR_HEIGHT; const int barWidth = progressBarMaxWidth * chapterProgress / 100; - renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, BOOK_PROGRESS_BAR_HEIGHT, true); + renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, PROGRESS_BAR_HEIGHT, true); } int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector& tabs) { diff --git a/src/ScreenComponents.h b/src/ScreenComponents.h index 9f2dec05..72c86d58 100644 --- a/src/ScreenComponents.h +++ b/src/ScreenComponents.h @@ -13,7 +13,7 @@ struct TabInfo { class ScreenComponents { public: - static const int BOOK_PROGRESS_BAR_HEIGHT = 4; + static const int PROGRESS_BAR_HEIGHT = 6; static void drawBattery(const GfxRenderer& renderer, int left, int top, bool showPercentage = true); static void drawBookProgressBar(const GfxRenderer& renderer, size_t bookProgress); diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index d5267170..ea1a092b 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -292,7 +292,7 @@ void EpubReaderActivity::renderScreen() { const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR || SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR; orientedMarginBottom += statusBarMargin - SETTINGS.screenMargin + - (showProgressBar ? (ScreenComponents::BOOK_PROGRESS_BAR_HEIGHT + progressBarMarginTop) : 0); + (showProgressBar ? (ScreenComponents::PROGRESS_BAR_HEIGHT + progressBarMarginTop) : 0); } if (!section) { diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 21d76bcf..ea9aae14 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -174,7 +174,7 @@ void TxtReaderActivity::initializeReader() { const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR || SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR; orientedMarginBottom += statusBarMargin - cachedScreenMargin + - (showProgressBar ? (ScreenComponents::BOOK_PROGRESS_BAR_HEIGHT + progressBarMarginTop) : 0); + (showProgressBar ? (ScreenComponents::PROGRESS_BAR_HEIGHT + progressBarMarginTop) : 0); } viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight; From 8658786db55d932d72662ed51a3424e3d4005eb8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 21:00:49 +0000 Subject: [PATCH 04/18] Run clang-format on modified files Co-authored-by: lukestein <44452336+lukestein@users.noreply.github.com> --- llvm.sh | 233 +++++++++++++++++++ src/activities/reader/EpubReaderActivity.cpp | 5 +- src/activities/settings/SettingsActivity.cpp | 6 +- 3 files changed, 238 insertions(+), 6 deletions(-) create mode 100755 llvm.sh diff --git a/llvm.sh b/llvm.sh new file mode 100755 index 00000000..1ab9dd9f --- /dev/null +++ b/llvm.sh @@ -0,0 +1,233 @@ +#!/bin/bash +################################################################################ +# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +# See https://llvm.org/LICENSE.txt for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +################################################################################ +# +# This script will install the llvm toolchain on the different +# Debian and Ubuntu versions + +set -eux + +usage() { + set +x + echo "Usage: $0 [llvm_major_version] [all] [OPTIONS]" 1>&2 + echo -e "all\t\t\tInstall all packages." 1>&2 + echo -e "-n=code_name\t\tSpecifies the distro codename, for example bionic" 1>&2 + echo -e "-h\t\t\tPrints this help." 1>&2 + echo -e "-m=repo_base_url\tSpecifies the base URL from which to download." 1>&2 + exit 1; +} + +CURRENT_LLVM_STABLE=20 +BASE_URL="http://apt.llvm.org" + +NEW_DEBIAN_DISTROS=("trixie" "forky" "unstable") +# Set default values for commandline arguments +# We default to the current stable branch of LLVM +LLVM_VERSION=$CURRENT_LLVM_STABLE +ALL=0 +DISTRO=$(lsb_release -is) +VERSION_CODENAME=$(lsb_release -cs) +VERSION=$(lsb_release -sr) +UBUNTU_CODENAME="" +CODENAME_FROM_ARGUMENTS="" +# Obtain VERSION_CODENAME and UBUNTU_CODENAME (for Ubuntu and its derivatives) +source /etc/os-release +DISTRO=${DISTRO,,} + +# Check for required tools + +# Check if this is a new Debian distro +is_new_debian=0 +if [[ "${DISTRO}" == "debian" ]]; then + for new_distro in "${NEW_DEBIAN_DISTROS[@]}"; do + if [[ "${VERSION_CODENAME}" == "${new_distro}" ]]; then + is_new_debian=1 + break + fi + done +fi + +# Check for required tools +needed_binaries=(lsb_release wget gpg) +# add-apt-repository is not needed for newer Debian distros +if [[ $is_new_debian -eq 0 ]]; then + needed_binaries+=(add-apt-repository) +fi + +missing_binaries=() +using_curl= +for binary in "${needed_binaries[@]}"; do + if ! command -v $binary &>/dev/null ; then + if [[ "$binary" == "wget" ]] && command -v curl &>/dev/null; then + using_curl=1 + continue + fi + missing_binaries+=($binary) + fi +done + +if [[ ${#missing_binaries[@]} -gt 0 ]] ; then + echo "You are missing some tools this script requires: ${missing_binaries[@]}" + echo "(hint: apt install lsb-release wget software-properties-common gnupg)" + echo "curl is also supported" + exit 4 +fi + +case ${DISTRO} in + debian) + # Debian Forky has a workaround because of + # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1038383 + if [[ "${VERSION}" == "unstable" ]] || [[ "${VERSION}" == "testing" ]] || [[ "${VERSION_CODENAME}" == "forky" ]]; then + CODENAME=unstable + LINKNAME= + else + # "stable" Debian release + CODENAME=${VERSION_CODENAME} + LINKNAME=-${CODENAME} + fi + ;; + *) + # ubuntu and its derivatives + if [[ -n "${UBUNTU_CODENAME}" ]]; then + CODENAME=${UBUNTU_CODENAME} + if [[ -n "${CODENAME}" ]]; then + LINKNAME=-${CODENAME} + fi + fi + ;; +esac + +# read optional command line arguments +if [ "$#" -ge 1 ] && [ "${1::1}" != "-" ]; then + if [ "$1" != "all" ]; then + LLVM_VERSION=$1 + else + # special case for ./llvm.sh all + ALL=1 + fi + OPTIND=2 + if [ "$#" -ge 2 ]; then + if [ "$2" == "all" ]; then + # Install all packages + ALL=1 + OPTIND=3 + fi + fi +fi + +while getopts ":hm:n:" arg; do + case $arg in + h) + usage + ;; + m) + BASE_URL=${OPTARG} + ;; + n) + CODENAME=${OPTARG} + if [[ "${CODENAME}" == "unstable" ]]; then + # link name does not apply to unstable repository + LINKNAME= + else + LINKNAME=-${CODENAME} + fi + CODENAME_FROM_ARGUMENTS="true" + ;; + esac +done + +if [[ $EUID -ne 0 ]]; then + echo "This script must be run as root!" + exit 1 +fi + +declare -A LLVM_VERSION_PATTERNS +LLVM_VERSION_PATTERNS[9]="-9" +LLVM_VERSION_PATTERNS[10]="-10" +LLVM_VERSION_PATTERNS[11]="-11" +LLVM_VERSION_PATTERNS[12]="-12" +LLVM_VERSION_PATTERNS[13]="-13" +LLVM_VERSION_PATTERNS[14]="-14" +LLVM_VERSION_PATTERNS[15]="-15" +LLVM_VERSION_PATTERNS[16]="-16" +LLVM_VERSION_PATTERNS[17]="-17" +LLVM_VERSION_PATTERNS[18]="-18" +LLVM_VERSION_PATTERNS[19]="-19" +LLVM_VERSION_PATTERNS[20]="-20" +LLVM_VERSION_PATTERNS[21]="-21" +LLVM_VERSION_PATTERNS[22]="" + +if [ ! ${LLVM_VERSION_PATTERNS[$LLVM_VERSION]+_} ]; then + echo "This script does not support LLVM version $LLVM_VERSION" + exit 3 +fi + +LLVM_VERSION_STRING=${LLVM_VERSION_PATTERNS[$LLVM_VERSION]} + +# join the repository name +if [[ -n "${CODENAME}" ]]; then + REPO_NAME="deb ${BASE_URL}/${CODENAME}/ llvm-toolchain${LINKNAME}${LLVM_VERSION_STRING} main" + # check if the repository exists for the distro and version + if ! wget -q --method=HEAD ${BASE_URL}/${CODENAME} &> /dev/null && \ + ! curl -sSLI -XHEAD ${BASE_URL}/${CODENAME} &> /dev/null; then + if [[ -n "${CODENAME_FROM_ARGUMENTS}" ]]; then + echo "Specified codename '${CODENAME}' is not supported by this script." + else + echo "Distribution '${DISTRO}' in version '${VERSION}' is not supported by this script." + fi + exit 2 + fi +fi + + +# install everything + +if [[ ! -f /etc/apt/trusted.gpg.d/apt.llvm.org.asc ]]; then + # download GPG key once + if [[ -z "$using_curl" ]]; then + wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc + else + curl -sSL https://apt.llvm.org/llvm-snapshot.gpg.key | tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc + fi +fi + +if [[ -z "`apt-key list 2> /dev/null | grep -i llvm`" ]]; then + # Delete the key in the old format + apt-key del AF4F7421 || true +fi + + +# Add repository based on distribution +if [[ "${VERSION_CODENAME}" == "bookworm" ]]; then + # add it twice to workaround: + # https://github.com/llvm/llvm-project/issues/62475 + add-apt-repository -y "${REPO_NAME}" + add-apt-repository -y "${REPO_NAME}" +elif [[ $is_new_debian -eq 1 ]]; then + # workaround missing add-apt-repository in newer Debian and use new source.list format + SOURCES_FILE="/etc/apt/sources.list.d/http_apt_llvm_org_${CODENAME}_-${VERSION_CODENAME}.sources" + TEXT_TO_ADD="Types: deb +Architectures: amd64 arm64 +Signed-By: /etc/apt/trusted.gpg.d/apt.llvm.org.asc +URIs: ${BASE_URL}/${CODENAME}/ +Suites: llvm-toolchain${LINKNAME}${LLVM_VERSION_STRING} +Components: main" + echo "$TEXT_TO_ADD" | tee -a "$SOURCES_FILE" > /dev/null +else + add-apt-repository -y "${REPO_NAME}" +fi + +apt-get update +PKG="clang-$LLVM_VERSION lldb-$LLVM_VERSION lld-$LLVM_VERSION clangd-$LLVM_VERSION" +if [[ $ALL -eq 1 ]]; then + # same as in test-install.sh + # No worries if we have dups + PKG="$PKG clang-tidy-$LLVM_VERSION clang-format-$LLVM_VERSION clang-tools-$LLVM_VERSION llvm-$LLVM_VERSION-dev lld-$LLVM_VERSION lldb-$LLVM_VERSION llvm-$LLVM_VERSION-tools libomp-$LLVM_VERSION-dev libc++-$LLVM_VERSION-dev libc++abi-$LLVM_VERSION-dev libclang-common-$LLVM_VERSION-dev libclang-$LLVM_VERSION-dev libclang-cpp$LLVM_VERSION-dev liblldb-$LLVM_VERSION-dev libunwind-$LLVM_VERSION-dev" + if test $LLVM_VERSION -gt 14; then + PKG="$PKG libclang-rt-$LLVM_VERSION-dev libpolly-$LLVM_VERSION-dev" + fi +fi +apt-get install -y $PKG diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index ea1a092b..46a96f5a 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -517,9 +517,8 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in if (showChapterProgressBar) { // Draw chapter progress bar at the very bottom of the screen, from edge to edge of viewable area - const float chapterProgress = (section->pageCount > 0) - ? (static_cast(section->currentPage + 1) / section->pageCount) * 100 - : 0; + const float chapterProgress = + (section->pageCount > 0) ? (static_cast(section->currentPage + 1) / section->pageCount) * 100 : 0; ScreenComponents::drawChapterProgressBar(renderer, static_cast(chapterProgress)); } diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 55688baa..73707cdb 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -18,9 +18,9 @@ const SettingInfo displaySettings[displaySettingsCount] = { SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}), SettingInfo::Enum("Sleep Screen Cover Filter", &CrossPointSettings::sleepScreenCoverFilter, {"None", "Contrast", "Inverted"}), - SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar, - {"None", "No Progress", "Full w/ Percentage", "Full w/ Progress Bar", "Progress Bar", - "Chapter Progress Bar"}), + SettingInfo::Enum( + "Status Bar", &CrossPointSettings::statusBar, + {"None", "No Progress", "Full w/ Percentage", "Full w/ Progress Bar", "Progress Bar", "Chapter Progress Bar"}), SettingInfo::Enum("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}), SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency, {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})}; From 7667935aed569357225724af862400af23c1e5cc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 21:01:00 +0000 Subject: [PATCH 05/18] Remove accidentally added llvm.sh script Co-authored-by: lukestein <44452336+lukestein@users.noreply.github.com> --- llvm.sh | 233 -------------------------------------------------------- 1 file changed, 233 deletions(-) delete mode 100755 llvm.sh diff --git a/llvm.sh b/llvm.sh deleted file mode 100755 index 1ab9dd9f..00000000 --- a/llvm.sh +++ /dev/null @@ -1,233 +0,0 @@ -#!/bin/bash -################################################################################ -# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. -# See https://llvm.org/LICENSE.txt for license information. -# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -################################################################################ -# -# This script will install the llvm toolchain on the different -# Debian and Ubuntu versions - -set -eux - -usage() { - set +x - echo "Usage: $0 [llvm_major_version] [all] [OPTIONS]" 1>&2 - echo -e "all\t\t\tInstall all packages." 1>&2 - echo -e "-n=code_name\t\tSpecifies the distro codename, for example bionic" 1>&2 - echo -e "-h\t\t\tPrints this help." 1>&2 - echo -e "-m=repo_base_url\tSpecifies the base URL from which to download." 1>&2 - exit 1; -} - -CURRENT_LLVM_STABLE=20 -BASE_URL="http://apt.llvm.org" - -NEW_DEBIAN_DISTROS=("trixie" "forky" "unstable") -# Set default values for commandline arguments -# We default to the current stable branch of LLVM -LLVM_VERSION=$CURRENT_LLVM_STABLE -ALL=0 -DISTRO=$(lsb_release -is) -VERSION_CODENAME=$(lsb_release -cs) -VERSION=$(lsb_release -sr) -UBUNTU_CODENAME="" -CODENAME_FROM_ARGUMENTS="" -# Obtain VERSION_CODENAME and UBUNTU_CODENAME (for Ubuntu and its derivatives) -source /etc/os-release -DISTRO=${DISTRO,,} - -# Check for required tools - -# Check if this is a new Debian distro -is_new_debian=0 -if [[ "${DISTRO}" == "debian" ]]; then - for new_distro in "${NEW_DEBIAN_DISTROS[@]}"; do - if [[ "${VERSION_CODENAME}" == "${new_distro}" ]]; then - is_new_debian=1 - break - fi - done -fi - -# Check for required tools -needed_binaries=(lsb_release wget gpg) -# add-apt-repository is not needed for newer Debian distros -if [[ $is_new_debian -eq 0 ]]; then - needed_binaries+=(add-apt-repository) -fi - -missing_binaries=() -using_curl= -for binary in "${needed_binaries[@]}"; do - if ! command -v $binary &>/dev/null ; then - if [[ "$binary" == "wget" ]] && command -v curl &>/dev/null; then - using_curl=1 - continue - fi - missing_binaries+=($binary) - fi -done - -if [[ ${#missing_binaries[@]} -gt 0 ]] ; then - echo "You are missing some tools this script requires: ${missing_binaries[@]}" - echo "(hint: apt install lsb-release wget software-properties-common gnupg)" - echo "curl is also supported" - exit 4 -fi - -case ${DISTRO} in - debian) - # Debian Forky has a workaround because of - # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1038383 - if [[ "${VERSION}" == "unstable" ]] || [[ "${VERSION}" == "testing" ]] || [[ "${VERSION_CODENAME}" == "forky" ]]; then - CODENAME=unstable - LINKNAME= - else - # "stable" Debian release - CODENAME=${VERSION_CODENAME} - LINKNAME=-${CODENAME} - fi - ;; - *) - # ubuntu and its derivatives - if [[ -n "${UBUNTU_CODENAME}" ]]; then - CODENAME=${UBUNTU_CODENAME} - if [[ -n "${CODENAME}" ]]; then - LINKNAME=-${CODENAME} - fi - fi - ;; -esac - -# read optional command line arguments -if [ "$#" -ge 1 ] && [ "${1::1}" != "-" ]; then - if [ "$1" != "all" ]; then - LLVM_VERSION=$1 - else - # special case for ./llvm.sh all - ALL=1 - fi - OPTIND=2 - if [ "$#" -ge 2 ]; then - if [ "$2" == "all" ]; then - # Install all packages - ALL=1 - OPTIND=3 - fi - fi -fi - -while getopts ":hm:n:" arg; do - case $arg in - h) - usage - ;; - m) - BASE_URL=${OPTARG} - ;; - n) - CODENAME=${OPTARG} - if [[ "${CODENAME}" == "unstable" ]]; then - # link name does not apply to unstable repository - LINKNAME= - else - LINKNAME=-${CODENAME} - fi - CODENAME_FROM_ARGUMENTS="true" - ;; - esac -done - -if [[ $EUID -ne 0 ]]; then - echo "This script must be run as root!" - exit 1 -fi - -declare -A LLVM_VERSION_PATTERNS -LLVM_VERSION_PATTERNS[9]="-9" -LLVM_VERSION_PATTERNS[10]="-10" -LLVM_VERSION_PATTERNS[11]="-11" -LLVM_VERSION_PATTERNS[12]="-12" -LLVM_VERSION_PATTERNS[13]="-13" -LLVM_VERSION_PATTERNS[14]="-14" -LLVM_VERSION_PATTERNS[15]="-15" -LLVM_VERSION_PATTERNS[16]="-16" -LLVM_VERSION_PATTERNS[17]="-17" -LLVM_VERSION_PATTERNS[18]="-18" -LLVM_VERSION_PATTERNS[19]="-19" -LLVM_VERSION_PATTERNS[20]="-20" -LLVM_VERSION_PATTERNS[21]="-21" -LLVM_VERSION_PATTERNS[22]="" - -if [ ! ${LLVM_VERSION_PATTERNS[$LLVM_VERSION]+_} ]; then - echo "This script does not support LLVM version $LLVM_VERSION" - exit 3 -fi - -LLVM_VERSION_STRING=${LLVM_VERSION_PATTERNS[$LLVM_VERSION]} - -# join the repository name -if [[ -n "${CODENAME}" ]]; then - REPO_NAME="deb ${BASE_URL}/${CODENAME}/ llvm-toolchain${LINKNAME}${LLVM_VERSION_STRING} main" - # check if the repository exists for the distro and version - if ! wget -q --method=HEAD ${BASE_URL}/${CODENAME} &> /dev/null && \ - ! curl -sSLI -XHEAD ${BASE_URL}/${CODENAME} &> /dev/null; then - if [[ -n "${CODENAME_FROM_ARGUMENTS}" ]]; then - echo "Specified codename '${CODENAME}' is not supported by this script." - else - echo "Distribution '${DISTRO}' in version '${VERSION}' is not supported by this script." - fi - exit 2 - fi -fi - - -# install everything - -if [[ ! -f /etc/apt/trusted.gpg.d/apt.llvm.org.asc ]]; then - # download GPG key once - if [[ -z "$using_curl" ]]; then - wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc - else - curl -sSL https://apt.llvm.org/llvm-snapshot.gpg.key | tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc - fi -fi - -if [[ -z "`apt-key list 2> /dev/null | grep -i llvm`" ]]; then - # Delete the key in the old format - apt-key del AF4F7421 || true -fi - - -# Add repository based on distribution -if [[ "${VERSION_CODENAME}" == "bookworm" ]]; then - # add it twice to workaround: - # https://github.com/llvm/llvm-project/issues/62475 - add-apt-repository -y "${REPO_NAME}" - add-apt-repository -y "${REPO_NAME}" -elif [[ $is_new_debian -eq 1 ]]; then - # workaround missing add-apt-repository in newer Debian and use new source.list format - SOURCES_FILE="/etc/apt/sources.list.d/http_apt_llvm_org_${CODENAME}_-${VERSION_CODENAME}.sources" - TEXT_TO_ADD="Types: deb -Architectures: amd64 arm64 -Signed-By: /etc/apt/trusted.gpg.d/apt.llvm.org.asc -URIs: ${BASE_URL}/${CODENAME}/ -Suites: llvm-toolchain${LINKNAME}${LLVM_VERSION_STRING} -Components: main" - echo "$TEXT_TO_ADD" | tee -a "$SOURCES_FILE" > /dev/null -else - add-apt-repository -y "${REPO_NAME}" -fi - -apt-get update -PKG="clang-$LLVM_VERSION lldb-$LLVM_VERSION lld-$LLVM_VERSION clangd-$LLVM_VERSION" -if [[ $ALL -eq 1 ]]; then - # same as in test-install.sh - # No worries if we have dups - PKG="$PKG clang-tidy-$LLVM_VERSION clang-format-$LLVM_VERSION clang-tools-$LLVM_VERSION llvm-$LLVM_VERSION-dev lld-$LLVM_VERSION lldb-$LLVM_VERSION llvm-$LLVM_VERSION-tools libomp-$LLVM_VERSION-dev libc++-$LLVM_VERSION-dev libc++abi-$LLVM_VERSION-dev libclang-common-$LLVM_VERSION-dev libclang-$LLVM_VERSION-dev libclang-cpp$LLVM_VERSION-dev liblldb-$LLVM_VERSION-dev libunwind-$LLVM_VERSION-dev" - if test $LLVM_VERSION -gt 14; then - PKG="$PKG libclang-rt-$LLVM_VERSION-dev libpolly-$LLVM_VERSION-dev" - fi -fi -apt-get install -y $PKG From 547d1e4963355f0d62403964ddf9e4d1c6831a8f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 31 Jan 2026 21:36:08 +0000 Subject: [PATCH 06/18] Fix text overlap with chapter progress bar by adjusting textY position Co-authored-by: lukestein <44452336+lukestein@users.noreply.github.com> --- src/activities/reader/EpubReaderActivity.cpp | 4 +++- src/activities/reader/TxtReaderActivity.cpp | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 46a96f5a..906dc77e 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -484,7 +484,9 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in // Position status bar near the bottom of the logical screen, regardless of orientation const auto screenHeight = renderer.getScreenHeight(); - const auto textY = screenHeight - orientedMarginBottom - 4; + // Adjust text position upward when progress bar is shown to avoid overlap + const auto textY = screenHeight - orientedMarginBottom - 4 - + ((showProgressBar || showChapterProgressBar) ? ScreenComponents::PROGRESS_BAR_HEIGHT + 2 : 0); int progressTextWidth = 0; // Calculate progress in book diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index ea9aae14..7c1daf08 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -535,7 +535,9 @@ void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int SETTINGS.hideBatteryPercentage == CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_NEVER; const auto screenHeight = renderer.getScreenHeight(); - const auto textY = screenHeight - orientedMarginBottom - 4; + // Adjust text position upward when progress bar is shown to avoid overlap + const auto textY = screenHeight - orientedMarginBottom - 4 - + ((showProgressBar || showChapterProgressBar) ? ScreenComponents::PROGRESS_BAR_HEIGHT + 2 : 0); int progressTextWidth = 0; const float progress = totalPages > 0 ? (currentPage + 1) * 100.0f / totalPages : 0; From 0b9a2c77d74d80aaa388d77e621ff1f2db563ff0 Mon Sep 17 00:00:00 2001 From: Luke Stein <44452336+lukestein@users.noreply.github.com> Date: Sat, 31 Jan 2026 22:56:26 -0500 Subject: [PATCH 07/18] Fix progress bar vertical alignment --- src/activities/reader/EpubReaderActivity.cpp | 13 ++++++------- src/activities/reader/TxtReaderActivity.cpp | 6 +++--- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 906dc77e..e87cadfb 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -290,7 +290,8 @@ void EpubReaderActivity::renderScreen() { if (SETTINGS.statusBar != CrossPointSettings::STATUS_BAR_MODE::NONE) { // Add additional margin for status bar if progress bar is shown const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR || - SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR; + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR || + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR; orientedMarginBottom += statusBarMargin - SETTINGS.screenMargin + (showProgressBar ? (ScreenComponents::PROGRESS_BAR_HEIGHT + progressBarMarginTop) : 0); } @@ -465,8 +466,8 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in const int orientedMarginLeft) const { // determine visible status bar elements const bool showProgressPercentage = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; - const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR || - SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR; + const bool showBookProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR || + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR; const bool showChapterProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR; const bool showProgressText = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL || SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR; @@ -484,9 +485,7 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in // Position status bar near the bottom of the logical screen, regardless of orientation const auto screenHeight = renderer.getScreenHeight(); - // Adjust text position upward when progress bar is shown to avoid overlap - const auto textY = screenHeight - orientedMarginBottom - 4 - - ((showProgressBar || showChapterProgressBar) ? ScreenComponents::PROGRESS_BAR_HEIGHT + 2 : 0); + const auto textY = screenHeight - orientedMarginBottom - 4; int progressTextWidth = 0; // Calculate progress in book @@ -512,7 +511,7 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in progressStr); } - if (showProgressBar) { + if (showBookProgressBar) { // Draw progress bar at the very bottom of the screen, from edge to edge of viewable area ScreenComponents::drawBookProgressBar(renderer, static_cast(bookProgress)); } diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 7c1daf08..689d6852 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -172,7 +172,8 @@ void TxtReaderActivity::initializeReader() { if (SETTINGS.statusBar != CrossPointSettings::STATUS_BAR_MODE::NONE) { // Add additional margin for status bar if progress bar is shown const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR || - SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR; + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR || + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::CHAPTER_PROGRESS_BAR; orientedMarginBottom += statusBarMargin - cachedScreenMargin + (showProgressBar ? (ScreenComponents::PROGRESS_BAR_HEIGHT + progressBarMarginTop) : 0); } @@ -536,8 +537,7 @@ void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int const auto screenHeight = renderer.getScreenHeight(); // Adjust text position upward when progress bar is shown to avoid overlap - const auto textY = screenHeight - orientedMarginBottom - 4 - - ((showProgressBar || showChapterProgressBar) ? ScreenComponents::PROGRESS_BAR_HEIGHT + 2 : 0); + const auto textY = screenHeight - orientedMarginBottom - 4; int progressTextWidth = 0; const float progress = totalPages > 0 ? (currentPage + 1) * 100.0f / totalPages : 0; From 1eeea5459d945efb34486e02e24aa9ac16b1e04d Mon Sep 17 00:00:00 2001 From: Jonas Diemer Date: Sun, 1 Feb 2026 08:34:30 +0100 Subject: [PATCH 08/18] feat: Add reading menu and delete cache function (#433) ## Summary * Adds a menu in the Epub reader * The Chapter selection is moved there to pos 1 (so it can be reached by double tapping the confirm button) * A Go Home is there, too * Most significantly, a function "Delete Book Cache" is added. This returns to main (to avoid directly rebuilding cached items, eg. if this is used to debug/develop other areas - and it's also easier ;)) Probably, the Sync function could now be moved from the Chapter selection to this menu, too. --- ### 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/activities/reader/EpubReaderActivity.cpp | 126 +++++++++++++----- src/activities/reader/EpubReaderActivity.h | 4 + .../EpubReaderChapterSelectionActivity.cpp | 4 +- .../reader/EpubReaderMenuActivity.cpp | 103 ++++++++++++++ .../reader/EpubReaderMenuActivity.h | 51 +++++++ 5 files changed, 255 insertions(+), 33 deletions(-) create mode 100644 src/activities/reader/EpubReaderMenuActivity.cpp create mode 100644 src/activities/reader/EpubReaderMenuActivity.h diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index e87cadfb..eb2bc889 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -130,31 +130,9 @@ void EpubReaderActivity::loop() { const int currentPage = section ? section->currentPage : 0; const int totalPages = section ? section->pageCount : 0; exitActivity(); - enterNewActivity(new EpubReaderChapterSelectionActivity( - this->renderer, this->mappedInput, epub, epub->getPath(), currentSpineIndex, currentPage, totalPages, - [this] { - exitActivity(); - updateRequired = true; - }, - [this](const int newSpineIndex) { - if (currentSpineIndex != newSpineIndex) { - currentSpineIndex = newSpineIndex; - nextPageNumber = 0; - section.reset(); - } - exitActivity(); - updateRequired = true; - }, - [this](const int newSpineIndex, const int newPage) { - // Handle sync position - if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) { - currentSpineIndex = newSpineIndex; - nextPageNumber = newPage; - section.reset(); - } - exitActivity(); - updateRequired = true; - })); + enterNewActivity(new EpubReaderMenuActivity( + this->renderer, this->mappedInput, epub->getTitle(), [this]() { onReaderMenuBack(); }, + [this](EpubReaderMenuActivity::MenuAction action) { onReaderMenuConfirm(action); })); xSemaphoreGive(renderingMutex); } @@ -242,6 +220,89 @@ void EpubReaderActivity::loop() { } } +void EpubReaderActivity::onReaderMenuBack() { + exitActivity(); + updateRequired = true; +} + +void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action) { + switch (action) { + case EpubReaderMenuActivity::MenuAction::SELECT_CHAPTER: { + // Calculate values BEFORE we start destroying things + const int currentP = section ? section->currentPage : 0; + const int totalP = section ? section->pageCount : 0; + const int spineIdx = currentSpineIndex; + const std::string path = epub->getPath(); + + xSemaphoreTake(renderingMutex, portMAX_DELAY); + + // 1. Close the menu + exitActivity(); + + // 2. Open the Chapter Selector + enterNewActivity(new EpubReaderChapterSelectionActivity( + this->renderer, this->mappedInput, epub, path, spineIdx, currentP, totalP, + [this] { + exitActivity(); + updateRequired = true; + }, + [this](const int newSpineIndex) { + if (currentSpineIndex != newSpineIndex) { + currentSpineIndex = newSpineIndex; + nextPageNumber = 0; + section.reset(); + } + exitActivity(); + updateRequired = true; + }, + [this](const int newSpineIndex, const int newPage) { + if (currentSpineIndex != newSpineIndex || (section && section->currentPage != newPage)) { + currentSpineIndex = newSpineIndex; + nextPageNumber = newPage; + section.reset(); + } + exitActivity(); + updateRequired = true; + })); + + xSemaphoreGive(renderingMutex); + break; + } + case EpubReaderMenuActivity::MenuAction::GO_HOME: { + // 2. Trigger the reader's "Go Home" callback + if (onGoHome) { + onGoHome(); + } + + break; + } + case EpubReaderMenuActivity::MenuAction::DELETE_CACHE: { + xSemaphoreTake(renderingMutex, portMAX_DELAY); + if (epub) { + // 2. BACKUP: Read current progress + // We use the current variables that track our position + uint16_t backupSpine = currentSpineIndex; + uint16_t backupPage = section->currentPage; + uint16_t backupPageCount = section->pageCount; + + section.reset(); + // 3. WIPE: Clear the cache directory + epub->clearCache(); + + // 4. RESTORE: Re-setup the directory and rewrite the progress file + epub->setupCacheDir(); + + saveProgress(backupSpine, backupPage, backupPageCount); + } + exitActivity(); + updateRequired = true; + xSemaphoreGive(renderingMutex); + if (onGoHome) onGoHome(); + break; + } + } +} + void EpubReaderActivity::displayTaskLoop() { while (true) { if (updateRequired) { @@ -408,21 +469,26 @@ void EpubReaderActivity::renderScreen() { renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft); Serial.printf("[%lu] [ERS] Rendered page in %dms\n", millis(), millis() - start); } + saveProgress(currentSpineIndex, section->currentPage, section->pageCount); +} +void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) { FsFile f; if (SdMan.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) { uint8_t data[6]; data[0] = currentSpineIndex & 0xFF; data[1] = (currentSpineIndex >> 8) & 0xFF; - data[2] = section->currentPage & 0xFF; - data[3] = (section->currentPage >> 8) & 0xFF; - data[4] = section->pageCount & 0xFF; - data[5] = (section->pageCount >> 8) & 0xFF; + data[2] = currentPage & 0xFF; + data[3] = (currentPage >> 8) & 0xFF; + data[4] = pageCount & 0xFF; + data[5] = (pageCount >> 8) & 0xFF; f.write(data, 6); f.close(); + Serial.printf("[ERS] Progress saved: Chapter %d, Page %d\n", spineIndex, currentPage); + } else { + Serial.printf("[ERS] Could not save progress!\n"); } } - void EpubReaderActivity::renderContents(std::unique_ptr page, const int orientedMarginTop, const int orientedMarginRight, const int orientedMarginBottom, const int orientedMarginLeft) { diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index ab4aff2d..ca7c0dc9 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -5,6 +5,7 @@ #include #include +#include "EpubReaderMenuActivity.h" #include "activities/ActivityWithSubactivity.h" class EpubReaderActivity final : public ActivityWithSubactivity { @@ -27,6 +28,9 @@ class EpubReaderActivity final : public ActivityWithSubactivity { void renderContents(std::unique_ptr page, int orientedMarginTop, int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft); void renderStatusBar(int orientedMarginRight, int orientedMarginBottom, int orientedMarginLeft) const; + void saveProgress(int spineIndex, int currentPage, int pageCount); + void onReaderMenuBack(); + void onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action); public: explicit EpubReaderActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::unique_ptr epub, diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index 1b35e143..614227de 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -181,9 +181,7 @@ void EpubReaderChapterSelectionActivity::renderScreen() { const int pageItems = getPageItems(); const int totalItems = getTotalItems(); - const std::string title = - renderer.truncatedText(UI_12_FONT_ID, epub->getTitle().c_str(), pageWidth - 40, EpdFontFamily::BOLD); - renderer.drawCenteredText(UI_12_FONT_ID, 15, title.c_str(), true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, 15, "Go to Chapter", true, EpdFontFamily::BOLD); const auto pageStartIndex = selectorIndex / pageItems * pageItems; renderer.fillRect(0, 60 + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1, 30); diff --git a/src/activities/reader/EpubReaderMenuActivity.cpp b/src/activities/reader/EpubReaderMenuActivity.cpp new file mode 100644 index 00000000..5ce4881d --- /dev/null +++ b/src/activities/reader/EpubReaderMenuActivity.cpp @@ -0,0 +1,103 @@ +#include "EpubReaderMenuActivity.h" + +#include + +#include "fontIds.h" + +void EpubReaderMenuActivity::onEnter() { + ActivityWithSubactivity::onEnter(); + renderingMutex = xSemaphoreCreateMutex(); + updateRequired = true; + + xTaskCreate(&EpubReaderMenuActivity::taskTrampoline, "EpubMenuTask", 4096, this, 1, &displayTaskHandle); +} + +void EpubReaderMenuActivity::onExit() { + ActivityWithSubactivity::onExit(); + xSemaphoreTake(renderingMutex, portMAX_DELAY); + if (displayTaskHandle) { + vTaskDelete(displayTaskHandle); + displayTaskHandle = nullptr; + } + vSemaphoreDelete(renderingMutex); + renderingMutex = nullptr; +} + +void EpubReaderMenuActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); + self->displayTaskLoop(); +} + +void EpubReaderMenuActivity::displayTaskLoop() { + while (true) { + if (updateRequired && !subActivity) { + updateRequired = false; + xSemaphoreTake(renderingMutex, portMAX_DELAY); + renderScreen(); + xSemaphoreGive(renderingMutex); + } + vTaskDelay(10 / portTICK_PERIOD_MS); + } +} + +void EpubReaderMenuActivity::loop() { + if (subActivity) { + subActivity->loop(); + return; + } + + // 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)) { + // 1. Capture the callback and action locally + auto actionCallback = onAction; + auto selectedAction = menuItems[selectedIndex].action; + + // 2. Execute the callback + actionCallback(selectedAction); + + // 3. CRITICAL: Return immediately. 'this' is likely deleted now. + return; + } else if (mappedInput.wasReleased(MappedInputManager::Button::Back)) { + onBack(); + return; // Also return here just in case + } +} + +void EpubReaderMenuActivity::renderScreen() { + renderer.clearScreen(); + const auto pageWidth = renderer.getScreenWidth(); + + // Title + const std::string truncTitle = + renderer.truncatedText(UI_12_FONT_ID, title.c_str(), pageWidth - 40, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, 15, truncTitle.c_str(), true, EpdFontFamily::BOLD); + + // Menu Items + constexpr int startY = 60; + constexpr int lineHeight = 30; + + for (size_t i = 0; i < menuItems.size(); ++i) { + const int displayY = startY + (i * lineHeight); + const bool isSelected = (static_cast(i) == selectedIndex); + + if (isSelected) { + renderer.fillRect(0, displayY, pageWidth - 1, lineHeight, true); + } + + renderer.drawText(UI_10_FONT_ID, 20, displayY, menuItems[i].label.c_str(), !isSelected); + } + + // Footer / Hints + const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + + renderer.displayBuffer(); +} diff --git a/src/activities/reader/EpubReaderMenuActivity.h b/src/activities/reader/EpubReaderMenuActivity.h new file mode 100644 index 00000000..bd253f81 --- /dev/null +++ b/src/activities/reader/EpubReaderMenuActivity.h @@ -0,0 +1,51 @@ +#pragma once +#include +#include +#include +#include + +#include +#include +#include + +#include "../ActivityWithSubactivity.h" +#include "MappedInputManager.h" + +class EpubReaderMenuActivity final : public ActivityWithSubactivity { + public: + enum class MenuAction { SELECT_CHAPTER, GO_HOME, DELETE_CACHE }; + + explicit EpubReaderMenuActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, const std::string& title, + const std::function& onBack, const std::function& onAction) + : ActivityWithSubactivity("EpubReaderMenu", renderer, mappedInput), + title(title), + onBack(onBack), + onAction(onAction) {} + + void onEnter() override; + void onExit() override; + void loop() override; + + private: + struct MenuItem { + MenuAction action; + std::string label; + }; + + const std::vector menuItems = {{MenuAction::SELECT_CHAPTER, "Go to Chapter"}, + {MenuAction::GO_HOME, "Go Home"}, + {MenuAction::DELETE_CACHE, "Delete Book Cache"}}; + + int selectedIndex = 0; + bool updateRequired = false; + TaskHandle_t displayTaskHandle = nullptr; + SemaphoreHandle_t renderingMutex = nullptr; + std::string title = "Reader Menu"; + + const std::function onBack; + const std::function onAction; + + static void taskTrampoline(void* param); + [[noreturn]] void displayTaskLoop(); + void renderScreen(); +}; From 28fdbe09389b76b5e94e244c73eaaa6102849e0c Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Sun, 1 Feb 2026 12:41:24 +0500 Subject: [PATCH 09/18] feat(ui): change popup logic (#442) ## Summary * refactors Indexing popups into ScreenComponents (they had different implementations in different files) * removes Indexing popup for small chapters * only show Indexing popup (without progress bar) for large chapters (using same minimum file size condition as for progress bar before) ## Additional Context * Having to show even single popup message and redraw the screen slows down the flow significantly * Testing results: * Opening large chapter with progress bar - 11 seconds * Same chapter without progress bar, only single Indexing popup - 5 seconds --- ### AI Usage Did you use AI tools to help write this code? _**< PARTIALLY>**_ --- lib/Epub/Epub/Section.cpp | 12 +----- lib/Epub/Epub/Section.h | 3 +- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 23 +++------- lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h | 6 +-- lib/GfxRenderer/GfxRenderer.h | 2 +- src/ScreenComponents.cpp | 32 ++++++++++++++ src/ScreenComponents.h | 11 +++++ src/activities/boot_sleep/SleepActivity.cpp | 18 ++------ src/activities/boot_sleep/SleepActivity.h | 1 - src/activities/reader/EpubReaderActivity.cpp | 42 +------------------ src/activities/reader/TxtReaderActivity.cpp | 34 +-------------- 11 files changed, 62 insertions(+), 122 deletions(-) diff --git a/lib/Epub/Epub/Section.cpp b/lib/Epub/Epub/Section.cpp index 581a364f..cf67108b 100644 --- a/lib/Epub/Epub/Section.cpp +++ b/lib/Epub/Epub/Section.cpp @@ -123,9 +123,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 std::function& progressSetupFn, - const std::function& progressFn) { - constexpr uint32_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB + const std::function& popupFn) { const auto localPath = epub->getSpineItem(spineIndex).href; const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html"; @@ -171,11 +169,6 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c Serial.printf("[%lu] [SCT] Streamed temp HTML to %s (%d bytes)\n", millis(), tmpHtmlPath.c_str(), fileSize); - // Only show progress bar for larger chapters where rendering overhead is worth it - if (progressSetupFn && fileSize >= MIN_SIZE_FOR_PROGRESS) { - progressSetupFn(); - } - if (!SdMan.openFileForWrite("SCT", filePath, file)) { return false; } @@ -186,8 +179,7 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c 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))); }, - progressFn); + [this, &lut](std::unique_ptr page) { lut.emplace_back(this->onPageComplete(std::move(page))); }, popupFn); Hyphenator::setPreferredLanguage(epub->getLanguage()); success = visitor.parseAndBuildPages(); diff --git a/lib/Epub/Epub/Section.h b/lib/Epub/Epub/Section.h index 5b726141..5fdf210a 100644 --- a/lib/Epub/Epub/Section.h +++ b/lib/Epub/Epub/Section.h @@ -33,7 +33,6 @@ class Section { bool clearCache() const; bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment, uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, - const std::function& progressSetupFn = nullptr, - const std::function& progressFn = nullptr); + const std::function& popupFn = nullptr); std::unique_ptr loadPageFromSectionFile(); }; diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 298c4ec6..ac1f537f 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -10,8 +10,8 @@ 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 progress bar - smaller chapters don't benefit from it -constexpr size_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB +// 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 const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"}; constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]); @@ -289,10 +289,10 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() { return false; } - // Get file size for progress calculation - const size_t totalSize = file.size(); - size_t bytesRead = 0; - int lastProgress = -1; + // Get file size to decide whether to show indexing popup. + if (popupFn && file.size() >= MIN_SIZE_FOR_POPUP) { + popupFn(); + } XML_SetUserData(parser, this); XML_SetElementHandler(parser, startElement, endElement); @@ -322,17 +322,6 @@ bool ChapterHtmlSlimParser::parseAndBuildPages() { return false; } - // Update progress (call every 10% change to avoid too frequent updates) - // Only show progress for larger chapters where rendering overhead is worth it - bytesRead += len; - if (progressFn && totalSize >= MIN_SIZE_FOR_PROGRESS) { - const int progress = static_cast((bytesRead * 100) / totalSize); - if (lastProgress / 10 != progress / 10) { - lastProgress = progress; - progressFn(progress); - } - } - done = file.available() == 0; if (XML_ParseBuffer(parser, static_cast(len), done) == XML_STATUS_ERROR) { diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index 2d8ebe5c..38202e6e 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -18,7 +18,7 @@ class ChapterHtmlSlimParser { const std::string& filepath; GfxRenderer& renderer; std::function)> completePageFn; - std::function progressFn; // Progress callback (0-100) + std::function popupFn; // Popup callback int depth = 0; int skipUntilDepth = INT_MAX; int boldUntilDepth = INT_MAX; @@ -52,7 +52,7 @@ class ChapterHtmlSlimParser { const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint16_t viewportHeight, const bool hyphenationEnabled, const std::function)>& completePageFn, - const std::function& progressFn = nullptr) + const std::function& popupFn = nullptr) : filepath(filepath), renderer(renderer), fontId(fontId), @@ -63,7 +63,7 @@ class ChapterHtmlSlimParser { viewportHeight(viewportHeight), hyphenationEnabled(hyphenationEnabled), completePageFn(completePageFn), - progressFn(progressFn) {} + popupFn(popupFn) {} ~ChapterHtmlSlimParser() = default; bool parseAndBuildPages(); void addLineToPage(std::shared_ptr line); diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index 733975f4..86ddc8fc 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -56,7 +56,7 @@ class GfxRenderer { int getScreenHeight() const; void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const; // EXPERIMENTAL: Windowed update - display only a rectangular region - void displayWindow(int x, int y, int width, int height) const; + // void displayWindow(int x, int y, int width, int height) const; void invertScreen() const; void clearScreen(uint8_t color = 0xFF) const; diff --git a/src/ScreenComponents.cpp b/src/ScreenComponents.cpp index 0e77c855..e119a795 100644 --- a/src/ScreenComponents.cpp +++ b/src/ScreenComponents.cpp @@ -42,6 +42,38 @@ void ScreenComponents::drawBattery(const GfxRenderer& renderer, const int left, renderer.fillRect(x + 2, y + 2, filledWidth, batteryHeight - 4); } +ScreenComponents::PopupLayout ScreenComponents::drawPopup(const GfxRenderer& renderer, const char* message) { + constexpr int margin = 15; + constexpr int y = 60; + const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, message, EpdFontFamily::BOLD); + const int textHeight = renderer.getLineHeight(UI_12_FONT_ID); + const int w = textWidth + margin * 2; + const int h = textHeight + margin * 2; + const int x = (renderer.getScreenWidth() - w) / 2; + + renderer.fillRect(x - 2, y - 2, w + 4, h + 4, true); // frame thickness 2 + renderer.fillRect(x, y, w, h, false); + + const int textX = x + (w - textWidth) / 2; + const int textY = y + margin - 2; + renderer.drawText(UI_12_FONT_ID, textX, textY, message, true, EpdFontFamily::BOLD); + renderer.displayBuffer(); + return {x, y, w, h}; +} + +void ScreenComponents::fillPopupProgress(const GfxRenderer& renderer, const PopupLayout& layout, const int progress) { + constexpr int barHeight = 4; + const int barWidth = layout.width - 30; // twice the margin in drawPopup to match text width + const int barX = layout.x + (layout.width - barWidth) / 2; + const int barY = layout.y + layout.height - 10; + + int fillWidth = barWidth * progress / 100; + + renderer.fillRect(barX, barY, fillWidth, barHeight, true); + + renderer.displayBuffer(HalDisplay::FAST_REFRESH); +} + void ScreenComponents::drawBookProgressBar(const GfxRenderer& renderer, const size_t bookProgress) { int vieweableMarginTop, vieweableMarginRight, vieweableMarginBottom, vieweableMarginLeft; renderer.getOrientedViewableTRBL(&vieweableMarginTop, &vieweableMarginRight, &vieweableMarginBottom, diff --git a/src/ScreenComponents.h b/src/ScreenComponents.h index 72c86d58..a00a1edb 100644 --- a/src/ScreenComponents.h +++ b/src/ScreenComponents.h @@ -15,10 +15,21 @@ class ScreenComponents { public: static const int PROGRESS_BAR_HEIGHT = 6; + struct PopupLayout { + int x; + int y; + int width; + int height; + }; + static void drawBattery(const GfxRenderer& renderer, int left, int top, bool showPercentage = true); static void drawBookProgressBar(const GfxRenderer& renderer, size_t bookProgress); static void drawChapterProgressBar(const GfxRenderer& renderer, size_t chapterProgress); + static PopupLayout drawPopup(const GfxRenderer& renderer, const char* message); + + static void fillPopupProgress(const GfxRenderer& renderer, const PopupLayout& layout, int progress); + // Draw a horizontal tab bar with underline indicator for selected tab // Returns the height of the tab bar (for positioning content below) static int drawTabBar(const GfxRenderer& renderer, int y, const std::vector& tabs); diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index aace2095..7ffc5851 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -8,13 +8,15 @@ #include "CrossPointSettings.h" #include "CrossPointState.h" +#include "ScreenComponents.h" #include "fontIds.h" #include "images/CrossLarge.h" #include "util/StringUtils.h" void SleepActivity::onEnter() { Activity::onEnter(); - renderPopup("Entering Sleep..."); + + ScreenComponents::drawPopup(renderer, "Entering Sleep..."); if (SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::BLANK) { return renderBlankSleepScreen(); @@ -31,20 +33,6 @@ void SleepActivity::onEnter() { renderDefaultSleepScreen(); } -void SleepActivity::renderPopup(const char* message) const { - const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, message, EpdFontFamily::BOLD); - constexpr int margin = 20; - const int x = (renderer.getScreenWidth() - textWidth - margin * 2) / 2; - constexpr int y = 117; - const int w = textWidth + margin * 2; - const int h = renderer.getLineHeight(UI_12_FONT_ID) + margin * 2; - // renderer.clearScreen(); - renderer.fillRect(x - 5, y - 5, w + 10, h + 10, true); - renderer.fillRect(x + 5, y + 5, w - 10, h - 10, false); - renderer.drawText(UI_12_FONT_ID, x + margin, y + margin, message, true, EpdFontFamily::BOLD); - renderer.displayBuffer(); -} - void SleepActivity::renderCustomSleepScreen() const { // Check if we have a /sleep directory auto dir = SdMan.open("/sleep"); diff --git a/src/activities/boot_sleep/SleepActivity.h b/src/activities/boot_sleep/SleepActivity.h index 283220ce..87df8ba1 100644 --- a/src/activities/boot_sleep/SleepActivity.h +++ b/src/activities/boot_sleep/SleepActivity.h @@ -10,7 +10,6 @@ class SleepActivity final : public Activity { void onEnter() override; private: - void renderPopup(const char* message) const; void renderDefaultSleepScreen() const; void renderCustomSleepScreen() const; void renderCoverSleepScreen() const; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index eb2bc889..71d606cb 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -370,49 +370,11 @@ void EpubReaderActivity::renderScreen() { viewportHeight, SETTINGS.hyphenationEnabled)) { Serial.printf("[%lu] [ERS] Cache not found, building...\n", millis()); - // Progress bar dimensions - constexpr int barWidth = 200; - constexpr int barHeight = 10; - constexpr int boxMargin = 20; - const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, "Indexing..."); - const int boxWidthWithBar = (barWidth > textWidth ? barWidth : textWidth) + boxMargin * 2; - const int boxWidthNoBar = textWidth + boxMargin * 2; - const int boxHeightWithBar = renderer.getLineHeight(UI_12_FONT_ID) + barHeight + boxMargin * 3; - const int boxHeightNoBar = renderer.getLineHeight(UI_12_FONT_ID) + boxMargin * 2; - const int boxXWithBar = (renderer.getScreenWidth() - boxWidthWithBar) / 2; - const int boxXNoBar = (renderer.getScreenWidth() - boxWidthNoBar) / 2; - constexpr int boxY = 50; - const int barX = boxXWithBar + (boxWidthWithBar - barWidth) / 2; - const int barY = boxY + renderer.getLineHeight(UI_12_FONT_ID) + boxMargin * 2; - - // Always show "Indexing..." text first - { - renderer.fillRect(boxXNoBar, boxY, boxWidthNoBar, boxHeightNoBar, false); - renderer.drawText(UI_12_FONT_ID, boxXNoBar + boxMargin, boxY + boxMargin, "Indexing..."); - renderer.drawRect(boxXNoBar + 5, boxY + 5, boxWidthNoBar - 10, boxHeightNoBar - 10); - renderer.displayBuffer(); - pagesUntilFullRefresh = 0; - } - - // Setup callback - only called for chapters >= 50KB, redraws with progress bar - auto progressSetup = [this, boxXWithBar, boxWidthWithBar, boxHeightWithBar, barX, barY] { - renderer.fillRect(boxXWithBar, boxY, boxWidthWithBar, boxHeightWithBar, false); - renderer.drawText(UI_12_FONT_ID, boxXWithBar + boxMargin, boxY + boxMargin, "Indexing..."); - renderer.drawRect(boxXWithBar + 5, boxY + 5, boxWidthWithBar - 10, boxHeightWithBar - 10); - renderer.drawRect(barX, barY, barWidth, barHeight); - renderer.displayBuffer(); - }; - - // Progress callback to update progress bar - auto progressCallback = [this, barX, barY, barWidth, barHeight](int progress) { - const int fillWidth = (barWidth - 2) * progress / 100; - renderer.fillRect(barX + 1, barY + 1, fillWidth, barHeight - 2, true); - renderer.displayBuffer(HalDisplay::FAST_REFRESH); - }; + const auto popupFn = [this]() { ScreenComponents::drawPopup(renderer, "Indexing..."); }; if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, - viewportHeight, SETTINGS.hyphenationEnabled, progressSetup, progressCallback)) { + viewportHeight, SETTINGS.hyphenationEnabled, popupFn)) { Serial.printf("[%lu] [ERS] Failed to persist page data to SD\n", millis()); section.reset(); return; diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 689d6852..86e38e7c 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -208,28 +208,10 @@ void TxtReaderActivity::buildPageIndex() { size_t offset = 0; const size_t fileSize = txt->getFileSize(); - int lastProgressPercent = -1; Serial.printf("[%lu] [TRS] Building page index for %zu bytes...\n", millis(), fileSize); - // Progress bar dimensions (matching EpubReaderActivity style) - constexpr int barWidth = 200; - constexpr int barHeight = 10; - constexpr int boxMargin = 20; - const int textWidth = renderer.getTextWidth(UI_12_FONT_ID, "Indexing..."); - const int boxWidth = (barWidth > textWidth ? barWidth : textWidth) + boxMargin * 2; - const int boxHeight = renderer.getLineHeight(UI_12_FONT_ID) + barHeight + boxMargin * 3; - const int boxX = (renderer.getScreenWidth() - boxWidth) / 2; - constexpr int boxY = 50; - const int barX = boxX + (boxWidth - barWidth) / 2; - const int barY = boxY + renderer.getLineHeight(UI_12_FONT_ID) + boxMargin * 2; - - // Draw initial progress box - renderer.fillRect(boxX, boxY, boxWidth, boxHeight, false); - renderer.drawText(UI_12_FONT_ID, boxX + boxMargin, boxY + boxMargin, "Indexing..."); - renderer.drawRect(boxX + 5, boxY + 5, boxWidth - 10, boxHeight - 10); - renderer.drawRect(barX, barY, barWidth, barHeight); - renderer.displayBuffer(); + ScreenComponents::drawPopup(renderer, "Indexing..."); while (offset < fileSize) { std::vector tempLines; @@ -249,17 +231,6 @@ void TxtReaderActivity::buildPageIndex() { pageOffsets.push_back(offset); } - // Update progress bar every 10% (matching EpubReaderActivity logic) - int progressPercent = (offset * 100) / fileSize; - if (lastProgressPercent / 10 != progressPercent / 10) { - lastProgressPercent = progressPercent; - - // Fill progress bar - const int fillWidth = (barWidth - 2) * progressPercent / 100; - renderer.fillRect(barX + 1, barY + 1, fillWidth, barHeight - 2, true); - renderer.displayBuffer(HalDisplay::FAST_REFRESH); - } - // Yield to other tasks periodically if (pageOffsets.size() % 20 == 0) { vTaskDelay(1); @@ -403,9 +374,6 @@ void TxtReaderActivity::renderScreen() { // Initialize reader if not done if (!initialized) { - renderer.clearScreen(); - renderer.drawCenteredText(UI_12_FONT_ID, 300, "Indexing...", true, EpdFontFamily::BOLD); - renderer.displayBuffer(); initializeReader(); } From 9870c662c7731467b14cf86d8e5e50eb7eeb2739 Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Sun, 1 Feb 2026 12:51:31 +0500 Subject: [PATCH 10/18] fix: don't wake up after USB connect (#576) ## Summary * fixes problem that if short power button press is enabled, connecting device to usb leads to waking up --- lib/hal/HalGPIO.cpp | 23 ++++++++++++++++------- lib/hal/HalGPIO.h | 5 +++-- src/main.cpp | 20 ++++++++++++++++---- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/lib/hal/HalGPIO.cpp b/lib/hal/HalGPIO.cpp index 803efba0..89ce13ba 100644 --- a/lib/hal/HalGPIO.cpp +++ b/lib/hal/HalGPIO.cpp @@ -24,12 +24,13 @@ bool HalGPIO::wasAnyReleased() const { return inputMgr.wasAnyReleased(); } unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); } void HalGPIO::startDeepSleep() { - esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); // Ensure that the power button has been released to avoid immediately turning back on if you're holding it while (inputMgr.isPressed(BTN_POWER)) { delay(50); inputMgr.update(); } + // Arm the wakeup trigger *after* the button is released + esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); // Enter Deep Sleep esp_deep_sleep_start(); } @@ -44,12 +45,20 @@ bool HalGPIO::isUsbConnected() const { return digitalRead(UART0_RXD) == HIGH; } -bool HalGPIO::isWakeupByPowerButton() const { +HalGPIO::WakeupReason HalGPIO::getWakeupReason() const { + const bool usbConnected = isUsbConnected(); const auto wakeupCause = esp_sleep_get_wakeup_cause(); const auto resetReason = esp_reset_reason(); - if (isUsbConnected()) { - return wakeupCause == ESP_SLEEP_WAKEUP_GPIO; - } else { - return (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED) && (resetReason == ESP_RST_POWERON); + + if ((wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_POWERON && !usbConnected) || + (wakeupCause == ESP_SLEEP_WAKEUP_GPIO && resetReason == ESP_RST_DEEPSLEEP && usbConnected)) { + return WakeupReason::PowerButton; } -} + if (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_UNKNOWN && usbConnected) { + return WakeupReason::AfterFlash; + } + if (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_POWERON && usbConnected) { + return WakeupReason::AfterUSBPower; + } + return WakeupReason::Other; +} \ No newline at end of file diff --git a/lib/hal/HalGPIO.h b/lib/hal/HalGPIO.h index 11ffb22e..615a8d63 100644 --- a/lib/hal/HalGPIO.h +++ b/lib/hal/HalGPIO.h @@ -47,8 +47,9 @@ class HalGPIO { // Check if USB is connected bool isUsbConnected() const; - // Check if wakeup was caused by power button press - bool isWakeupByPowerButton() const; + enum class WakeupReason { PowerButton, AfterFlash, AfterUSBPower, Other }; + + WakeupReason getWakeupReason() const; // Button indices static constexpr uint8_t BTN_BACK = 0; diff --git a/src/main.cpp b/src/main.cpp index 2308f0a2..f099cba1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -294,10 +294,22 @@ void setup() { SETTINGS.loadFromFile(); KOREADER_STORE.loadFromFile(); - if (gpio.isWakeupByPowerButton()) { - // For normal wakeups, verify power button press duration - Serial.printf("[%lu] [ ] Verifying power button press duration\n", millis()); - verifyPowerButtonDuration(); + switch (gpio.getWakeupReason()) { + case HalGPIO::WakeupReason::PowerButton: + // For normal wakeups, verify power button press duration + Serial.printf("[%lu] [ ] Verifying power button press duration\n", millis()); + verifyPowerButtonDuration(); + break; + case HalGPIO::WakeupReason::AfterUSBPower: + // If USB power caused a cold boot, go back to sleep + Serial.printf("[%lu] [ ] Wakeup reason: After USB Power\n", millis()); + gpio.startDeepSleep(); + break; + case HalGPIO::WakeupReason::AfterFlash: + // After flashing, just proceed to boot + case HalGPIO::WakeupReason::Other: + default: + break; } // First serial output only here to avoid timing inconsistencies for power button press duration verification From 2add1e63c0bb1e9d29359b2e9302a1fd10b74f7c Mon Sep 17 00:00:00 2001 From: Luke Stein <44452336+lukestein@users.noreply.github.com> Date: Sun, 1 Feb 2026 03:19:23 -0500 Subject: [PATCH 11/18] fix: WiFi error screen text clarifications (#612) ## Summary * Clarify strings on Wifi connection error screens * I have confirmed on device that these are short enough not to overflow screen margins ## Additional Context * Several screens give duplicative text (e.g., header "Connection Failed" with contents text "Connection failed") or slightly confusing text (header "Forget Network?" with text "Remove saved password?") --- ### AI Usage Did you use AI tools to help write this code? **No** --- src/activities/network/WifiSelectionActivity.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index 5c45223b..8bf83a93 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -266,9 +266,9 @@ void WifiSelectionActivity::checkConnectionStatus() { } if (status == WL_CONNECT_FAILED || status == WL_NO_SSID_AVAIL) { - connectionError = "Connection failed"; + connectionError = "Error: General failure"; if (status == WL_NO_SSID_AVAIL) { - connectionError = "Network not found"; + connectionError = "Error: Network not found"; } state = WifiSelectionState::CONNECTION_FAILED; updateRequired = true; @@ -278,7 +278,7 @@ void WifiSelectionActivity::checkConnectionStatus() { // Check for timeout if (millis() - connectionStartTime > CONNECTION_TIMEOUT_MS) { WiFi.disconnect(); - connectionError = "Connection timeout"; + connectionError = "Error: Connection timeout"; state = WifiSelectionState::CONNECTION_FAILED; updateRequired = true; return; @@ -689,7 +689,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, "Forget Network?", true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Connection Failed", true, EpdFontFamily::BOLD); std::string ssidInfo = "Network: " + selectedSSID; if (ssidInfo.length() > 28) { @@ -697,7 +697,7 @@ void WifiSelectionActivity::renderForgetPrompt() const { } renderer.drawCenteredText(UI_10_FONT_ID, top, ssidInfo.c_str()); - renderer.drawCenteredText(UI_10_FONT_ID, top + 40, "Remove saved password?"); + renderer.drawCenteredText(UI_10_FONT_ID, top + 40, "Forget network and remove saved password?"); // Draw Cancel/Forget network buttons const int buttonY = top + 80; From 1b11e919323774784eb570c05388c2ab53826dde Mon Sep 17 00:00:00 2001 From: nscheung <6036668+nscheung@users.noreply.github.com> Date: Sun, 1 Feb 2026 00:21:28 -0800 Subject: [PATCH 12/18] fix: Hide button hints in landscape CW mode (#637) ## Summary * This change hides the button hints from overlapping chapter titles when in landscape CW mode. Before ![Before](https://github.com/user-attachments/assets/3015a9b3-3fa5-443b-a641-3e65841a6fbc) After ![After](https://github.com/user-attachments/assets/011de15d-5ae6-429c-8f91-d8f37abe52d9) ## Additional Context * I initially considered implementing an offset fix, but with potential UI changes on the horizon, hiding the button hints appears to be the simplest solution for now. --- ### 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 > --- .../reader/EpubReaderChapterSelectionActivity.cpp | 7 +++++-- .../reader/XtcReaderChapterSelectionActivity.cpp | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index 614227de..a77b37d0 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -206,8 +206,11 @@ void EpubReaderChapterSelectionActivity::renderScreen() { } } - const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); - renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + // Skip button hints in landscape CW mode (they overlap content) + if (renderer.getOrientation() != GfxRenderer::LandscapeClockwise) { + const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + } renderer.displayBuffer(); } diff --git a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp index b2cfecaa..ad806a30 100644 --- a/src/activities/reader/XtcReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/XtcReaderChapterSelectionActivity.cpp @@ -149,8 +149,11 @@ void XtcReaderChapterSelectionActivity::renderScreen() { renderer.drawText(UI_10_FONT_ID, 20, 60 + (i % pageItems) * 30, title, i != selectorIndex); } - const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); - renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + // Skip button hints in landscape CW mode (they overlap content) + if (renderer.getOrientation() != GfxRenderer::LandscapeClockwise) { + const auto labels = mappedInput.mapLabels("« Back", "Select", "Up", "Down"); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); + } renderer.displayBuffer(); } From c79fcac3adf978193b03cf57224c127f7ac98822 Mon Sep 17 00:00:00 2001 From: Thomas Foskolos Date: Sun, 1 Feb 2026 10:21:36 +0200 Subject: [PATCH 13/18] docs: Update USER_GUIDE.md (#625) Add Greek as not supported language atm on the use guide --- USER_GUIDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 06973c92..e4d72b0c 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -201,7 +201,7 @@ CrossPoint renders text using the following Unicode character blocks, enabling s * **Latin Script (Basic, Supplement, Extended-A):** Covers English, German, French, Spanish, Portuguese, Italian, Dutch, Swedish, Norwegian, Danish, Finnish, Polish, Czech, Hungarian, Romanian, Slovak, Slovenian, Turkish, and others. * **Cyrillic Script (Standard and Extended):** Covers Russian, Ukrainian, Belarusian, Bulgarian, Serbian, Macedonian, Kazakh, Kyrgyz, Mongolian, and others. -What is not supported: Chinese, Japanese, Korean, Vietnamese, Hebrew, Arabic and Farsi. +What is not supported: Chinese, Japanese, Korean, Vietnamese, Hebrew, Arabic, Greek and Farsi. --- From 81f4770618771284fe6767293651a35d42c6083e Mon Sep 17 00:00:00 2001 From: Gaspar Fabrega Ragni Date: Sun, 1 Feb 2026 05:22:52 -0300 Subject: [PATCH 14/18] fix: custom sleep not showing image at index 0 (#639) ## Summary * Fixing custom sleep behaviour where the first image in the /sleep directory is not shown * image at index 0 is not being rendered when more than 1 image is stored in /sleep directory, because `APP_STATE.lastSleepImage` is always 0. ## Additional Context * `APP_STATE.lastSleepImage` is reset to 0 when a epub is open, this value is only used to compare it to the randomly selected one in `renderCustomSleepScreen()` that should always be a valid index, since the list of valid bmp images is colected from scratch. -> no need to reset it and block image @ index 0 from being rendered --- ### 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: Oyster --- src/main.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index f099cba1..89c4e13c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -329,7 +329,6 @@ void setup() { // Clear app state to avoid getting into a boot loop if the epub doesn't load const auto path = APP_STATE.openEpubPath; APP_STATE.openEpubPath = ""; - APP_STATE.lastSleepImage = 0; APP_STATE.saveToFile(); onGoToReader(path, MyLibraryActivity::Tab::Recent); } From 6228a9f14c8705a03b80433b070e4736db2b97c0 Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Sun, 1 Feb 2026 21:35:25 +1100 Subject: [PATCH 15/18] Revert "fix: don't wake up after USB connect" (#643) Reverts crosspoint-reader/crosspoint-reader#576 Causing a boot loop on master --- lib/hal/HalGPIO.cpp | 23 +++++++---------------- lib/hal/HalGPIO.h | 5 ++--- src/main.cpp | 20 ++++---------------- 3 files changed, 13 insertions(+), 35 deletions(-) diff --git a/lib/hal/HalGPIO.cpp b/lib/hal/HalGPIO.cpp index 89ce13ba..803efba0 100644 --- a/lib/hal/HalGPIO.cpp +++ b/lib/hal/HalGPIO.cpp @@ -24,13 +24,12 @@ bool HalGPIO::wasAnyReleased() const { return inputMgr.wasAnyReleased(); } unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); } void HalGPIO::startDeepSleep() { + esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); // Ensure that the power button has been released to avoid immediately turning back on if you're holding it while (inputMgr.isPressed(BTN_POWER)) { delay(50); inputMgr.update(); } - // Arm the wakeup trigger *after* the button is released - esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); // Enter Deep Sleep esp_deep_sleep_start(); } @@ -45,20 +44,12 @@ bool HalGPIO::isUsbConnected() const { return digitalRead(UART0_RXD) == HIGH; } -HalGPIO::WakeupReason HalGPIO::getWakeupReason() const { - const bool usbConnected = isUsbConnected(); +bool HalGPIO::isWakeupByPowerButton() const { const auto wakeupCause = esp_sleep_get_wakeup_cause(); const auto resetReason = esp_reset_reason(); - - if ((wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_POWERON && !usbConnected) || - (wakeupCause == ESP_SLEEP_WAKEUP_GPIO && resetReason == ESP_RST_DEEPSLEEP && usbConnected)) { - return WakeupReason::PowerButton; + if (isUsbConnected()) { + return wakeupCause == ESP_SLEEP_WAKEUP_GPIO; + } else { + return (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED) && (resetReason == ESP_RST_POWERON); } - if (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_UNKNOWN && usbConnected) { - return WakeupReason::AfterFlash; - } - if (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_POWERON && usbConnected) { - return WakeupReason::AfterUSBPower; - } - return WakeupReason::Other; -} \ No newline at end of file +} diff --git a/lib/hal/HalGPIO.h b/lib/hal/HalGPIO.h index 615a8d63..11ffb22e 100644 --- a/lib/hal/HalGPIO.h +++ b/lib/hal/HalGPIO.h @@ -47,9 +47,8 @@ class HalGPIO { // Check if USB is connected bool isUsbConnected() const; - enum class WakeupReason { PowerButton, AfterFlash, AfterUSBPower, Other }; - - WakeupReason getWakeupReason() const; + // Check if wakeup was caused by power button press + bool isWakeupByPowerButton() const; // Button indices static constexpr uint8_t BTN_BACK = 0; diff --git a/src/main.cpp b/src/main.cpp index 89c4e13c..df54fb6d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -294,22 +294,10 @@ void setup() { SETTINGS.loadFromFile(); KOREADER_STORE.loadFromFile(); - switch (gpio.getWakeupReason()) { - case HalGPIO::WakeupReason::PowerButton: - // For normal wakeups, verify power button press duration - Serial.printf("[%lu] [ ] Verifying power button press duration\n", millis()); - verifyPowerButtonDuration(); - break; - case HalGPIO::WakeupReason::AfterUSBPower: - // If USB power caused a cold boot, go back to sleep - Serial.printf("[%lu] [ ] Wakeup reason: After USB Power\n", millis()); - gpio.startDeepSleep(); - break; - case HalGPIO::WakeupReason::AfterFlash: - // After flashing, just proceed to boot - case HalGPIO::WakeupReason::Other: - default: - break; + if (gpio.isWakeupByPowerButton()) { + // For normal wakeups, verify power button press duration + Serial.printf("[%lu] [ ] Verifying power button press duration\n", millis()); + verifyPowerButtonDuration(); } // First serial output only here to avoid timing inconsistencies for power button press duration verification From cc2f125809b11ffac7db28fc7809e9e2cbaa951a Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Sun, 1 Feb 2026 16:19:33 +0500 Subject: [PATCH 16/18] fix: don't wake up after USB connect (#644) ## Summary * fixes problem that if short power button press is enabled, connecting device to usb leads to waking up --- lib/hal/HalGPIO.cpp | 23 ++++++++++++++++------- lib/hal/HalGPIO.h | 5 +++-- src/main.cpp | 20 ++++++++++++++++---- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/lib/hal/HalGPIO.cpp b/lib/hal/HalGPIO.cpp index 803efba0..89ce13ba 100644 --- a/lib/hal/HalGPIO.cpp +++ b/lib/hal/HalGPIO.cpp @@ -24,12 +24,13 @@ bool HalGPIO::wasAnyReleased() const { return inputMgr.wasAnyReleased(); } unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); } void HalGPIO::startDeepSleep() { - esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); // Ensure that the power button has been released to avoid immediately turning back on if you're holding it while (inputMgr.isPressed(BTN_POWER)) { delay(50); inputMgr.update(); } + // Arm the wakeup trigger *after* the button is released + esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); // Enter Deep Sleep esp_deep_sleep_start(); } @@ -44,12 +45,20 @@ bool HalGPIO::isUsbConnected() const { return digitalRead(UART0_RXD) == HIGH; } -bool HalGPIO::isWakeupByPowerButton() const { +HalGPIO::WakeupReason HalGPIO::getWakeupReason() const { + const bool usbConnected = isUsbConnected(); const auto wakeupCause = esp_sleep_get_wakeup_cause(); const auto resetReason = esp_reset_reason(); - if (isUsbConnected()) { - return wakeupCause == ESP_SLEEP_WAKEUP_GPIO; - } else { - return (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED) && (resetReason == ESP_RST_POWERON); + + if ((wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_POWERON && !usbConnected) || + (wakeupCause == ESP_SLEEP_WAKEUP_GPIO && resetReason == ESP_RST_DEEPSLEEP && usbConnected)) { + return WakeupReason::PowerButton; } -} + if (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_UNKNOWN && usbConnected) { + return WakeupReason::AfterFlash; + } + if (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED && resetReason == ESP_RST_POWERON && usbConnected) { + return WakeupReason::AfterUSBPower; + } + return WakeupReason::Other; +} \ No newline at end of file diff --git a/lib/hal/HalGPIO.h b/lib/hal/HalGPIO.h index 11ffb22e..615a8d63 100644 --- a/lib/hal/HalGPIO.h +++ b/lib/hal/HalGPIO.h @@ -47,8 +47,9 @@ class HalGPIO { // Check if USB is connected bool isUsbConnected() const; - // Check if wakeup was caused by power button press - bool isWakeupByPowerButton() const; + enum class WakeupReason { PowerButton, AfterFlash, AfterUSBPower, Other }; + + WakeupReason getWakeupReason() const; // Button indices static constexpr uint8_t BTN_BACK = 0; diff --git a/src/main.cpp b/src/main.cpp index df54fb6d..89c4e13c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -294,10 +294,22 @@ void setup() { SETTINGS.loadFromFile(); KOREADER_STORE.loadFromFile(); - if (gpio.isWakeupByPowerButton()) { - // For normal wakeups, verify power button press duration - Serial.printf("[%lu] [ ] Verifying power button press duration\n", millis()); - verifyPowerButtonDuration(); + switch (gpio.getWakeupReason()) { + case HalGPIO::WakeupReason::PowerButton: + // For normal wakeups, verify power button press duration + Serial.printf("[%lu] [ ] Verifying power button press duration\n", millis()); + verifyPowerButtonDuration(); + break; + case HalGPIO::WakeupReason::AfterUSBPower: + // If USB power caused a cold boot, go back to sleep + Serial.printf("[%lu] [ ] Wakeup reason: After USB Power\n", millis()); + gpio.startDeepSleep(); + break; + case HalGPIO::WakeupReason::AfterFlash: + // After flashing, just proceed to boot + case HalGPIO::WakeupReason::Other: + default: + break; } // First serial output only here to avoid timing inconsistencies for power button press duration verification From 4bb8a869e7a09f64d99f1b91a5fb1989af1aecb0 Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Sun, 1 Feb 2026 16:23:48 +0500 Subject: [PATCH 17/18] fix: truncating chapter titles using UTF-8 safe function (#599) ## Summary * Truncating chapter titles using utf8 safe functions (Cyrillic titles were split mid codepoint) * refactoring of lib/Utf8 --- ### 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 >**_ --- lib/GfxRenderer/GfxRenderer.cpp | 18 +++++++++++++----- lib/Utf8/Utf8.cpp | 17 +++++++++++++++++ lib/Utf8/Utf8.h | 6 +++++- src/activities/home/HomeActivity.cpp | 11 ++++++----- src/activities/reader/EpubReaderActivity.cpp | 4 ++-- src/activities/reader/TxtReaderActivity.cpp | 4 ++-- src/util/StringUtils.cpp | 19 ------------------- src/util/StringUtils.h | 6 ------ 8 files changed, 45 insertions(+), 40 deletions(-) diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index fa1c61c6..b5aa7710 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -415,13 +415,21 @@ void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth, const EpdFontFamily::Style style) const { + if (!text || maxWidth <= 0) return ""; + std::string item = text; - int itemWidth = getTextWidth(fontId, item.c_str(), style); - while (itemWidth > maxWidth && item.length() > 8) { - item.replace(item.length() - 5, 5, "..."); - itemWidth = getTextWidth(fontId, item.c_str(), style); + const char* ellipsis = "..."; + int textWidth = getTextWidth(fontId, item.c_str(), style); + if (textWidth <= maxWidth) { + // Text fits, return as is + return item; } - return item; + + while (!item.empty() && getTextWidth(fontId, (item + ellipsis).c_str(), style) >= maxWidth) { + utf8RemoveLastChar(item); + } + + return item.empty() ? ellipsis : item + ellipsis; } // Note: Internal driver treats screen in command orientation; this library exposes a logical orientation diff --git a/lib/Utf8/Utf8.cpp b/lib/Utf8/Utf8.cpp index d5f7ebce..f77cce55 100644 --- a/lib/Utf8/Utf8.cpp +++ b/lib/Utf8/Utf8.cpp @@ -29,3 +29,20 @@ uint32_t utf8NextCodepoint(const unsigned char** string) { return cp; } + +size_t utf8RemoveLastChar(std::string& str) { + if (str.empty()) return 0; + size_t pos = str.size() - 1; + while (pos > 0 && (static_cast(str[pos]) & 0xC0) == 0x80) { + --pos; + } + str.resize(pos); + return pos; +} + +// Truncate string by removing N UTF-8 characters from the end +void utf8TruncateChars(std::string& str, const size_t numChars) { + for (size_t i = 0; i < numChars && !str.empty(); ++i) { + utf8RemoveLastChar(str); + } +} diff --git a/lib/Utf8/Utf8.h b/lib/Utf8/Utf8.h index 095c1584..23d63a4e 100644 --- a/lib/Utf8/Utf8.h +++ b/lib/Utf8/Utf8.h @@ -1,7 +1,11 @@ #pragma once #include - +#include #define REPLACEMENT_GLYPH 0xFFFD uint32_t utf8NextCodepoint(const unsigned char** string); +// Remove the last UTF-8 codepoint from a std::string and return the new size. +size_t utf8RemoveLastChar(std::string& str); +// Truncate string by removing N UTF-8 codepoints from the end. +void utf8TruncateChars(std::string& str, size_t numChars); diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 58b29505..678af7cb 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -366,7 +367,7 @@ void HomeActivity::render() { while (!lines.back().empty() && renderer.getTextWidth(UI_12_FONT_ID, lines.back().c_str()) > maxLineWidth) { // Remove "..." first, then remove one UTF-8 char, then add "..." back lines.back().resize(lines.back().size() - 3); // Remove "..." - StringUtils::utf8RemoveLastChar(lines.back()); + utf8RemoveLastChar(lines.back()); lines.back().append("..."); } break; @@ -375,7 +376,7 @@ void HomeActivity::render() { int wordWidth = renderer.getTextWidth(UI_12_FONT_ID, i.c_str()); while (wordWidth > maxLineWidth && !i.empty()) { // Word itself is too long, trim it (UTF-8 safe) - StringUtils::utf8RemoveLastChar(i); + utf8RemoveLastChar(i); // Check if we have room for ellipsis std::string withEllipsis = i + "..."; wordWidth = renderer.getTextWidth(UI_12_FONT_ID, withEllipsis.c_str()); @@ -428,7 +429,7 @@ void HomeActivity::render() { if (!lastBookAuthor.empty()) { std::string trimmedAuthor = lastBookAuthor; while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) { - StringUtils::utf8RemoveLastChar(trimmedAuthor); + utf8RemoveLastChar(trimmedAuthor); } if (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) < renderer.getTextWidth(UI_10_FONT_ID, lastBookAuthor.c_str())) { @@ -462,14 +463,14 @@ void HomeActivity::render() { // Trim author if too long (UTF-8 safe) bool wasTrimmed = false; while (renderer.getTextWidth(UI_10_FONT_ID, trimmedAuthor.c_str()) > maxLineWidth && !trimmedAuthor.empty()) { - StringUtils::utf8RemoveLastChar(trimmedAuthor); + utf8RemoveLastChar(trimmedAuthor); wasTrimmed = true; } if (wasTrimmed && !trimmedAuthor.empty()) { // Make room for ellipsis while (renderer.getTextWidth(UI_10_FONT_ID, (trimmedAuthor + "...").c_str()) > maxLineWidth && !trimmedAuthor.empty()) { - StringUtils::utf8RemoveLastChar(trimmedAuthor); + utf8RemoveLastChar(trimmedAuthor); } trimmedAuthor.append("..."); } diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 71d606cb..fa85212e 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -584,8 +584,8 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in availableTitleSpace = rendererableScreenWidth - titleMarginLeft - titleMarginRight; titleMarginLeftAdjusted = titleMarginLeft; } - while (titleWidth > availableTitleSpace && title.length() > 11) { - title.replace(title.length() - 8, 8, "..."); + if (titleWidth > availableTitleSpace) { + title = renderer.truncatedText(SMALL_FONT_ID, title.c_str(), availableTitleSpace); titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str()); } } diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 86e38e7c..ef730e1a 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -546,8 +546,8 @@ void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int std::string title = txt->getTitle(); int titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str()); - while (titleWidth > availableTextWidth && title.length() > 11) { - title.replace(title.length() - 8, 8, "..."); + if (titleWidth > availableTextWidth) { + title = renderer.truncatedText(SMALL_FONT_ID, title.c_str(), availableTextWidth); titleWidth = renderer.getTextWidth(SMALL_FONT_ID, title.c_str()); } diff --git a/src/util/StringUtils.cpp b/src/util/StringUtils.cpp index 2426b687..8e2ce58e 100644 --- a/src/util/StringUtils.cpp +++ b/src/util/StringUtils.cpp @@ -61,23 +61,4 @@ bool checkFileExtension(const String& fileName, const char* extension) { return localFile.endsWith(localExtension); } -size_t utf8RemoveLastChar(std::string& str) { - if (str.empty()) return 0; - size_t pos = str.size() - 1; - // Walk back to find the start of the last UTF-8 character - // UTF-8 continuation bytes start with 10xxxxxx (0x80-0xBF) - while (pos > 0 && (static_cast(str[pos]) & 0xC0) == 0x80) { - --pos; - } - str.resize(pos); - return pos; -} - -// Truncate string by removing N UTF-8 characters from the end -void utf8TruncateChars(std::string& str, const size_t numChars) { - for (size_t i = 0; i < numChars && !str.empty(); ++i) { - utf8RemoveLastChar(str); - } -} - } // namespace StringUtils diff --git a/src/util/StringUtils.h b/src/util/StringUtils.h index 5c8332f0..4b93729b 100644 --- a/src/util/StringUtils.h +++ b/src/util/StringUtils.h @@ -19,10 +19,4 @@ std::string sanitizeFilename(const std::string& name, size_t maxLength = 100); bool checkFileExtension(const std::string& fileName, const char* extension); bool checkFileExtension(const String& fileName, const char* extension); -// UTF-8 safe string truncation - removes one character from the end -// Returns the new size after removing one UTF-8 character -size_t utf8RemoveLastChar(std::string& str); - -// Truncate string by removing N UTF-8 characters from the end -void utf8TruncateChars(std::string& str, size_t numChars); } // namespace StringUtils From 4436e44666d97affe2c57645f039587acf66715e Mon Sep 17 00:00:00 2001 From: Uri Tauber <142022451+Uri-Tauber@users.noreply.github.com> Date: Sun, 1 Feb 2026 13:53:20 +0200 Subject: [PATCH 18/18] feat: Debugging monitor script (#555) ## Summary * **What is the goal of this PR?** Add a debugging script to help developers monitor the ESP32 serial port directly from a PC. * **What changes are included?** Added a new script: scripts/debugging_monitor.py ## Additional Context While working on a new Crosspoint-Reader feature, it quickly became clear that watching the ESP32 serial output without any visual cues was inconvenient and easy to mess up. This script improves the debugging experience by reading data from the serial port and providing: 1. A timestamp prefix for every log line (instead of milliseconds since power-up) 2. Color-coded output for different message types 3. A secondary window displaying a live graph of RAM usage, which is especially useful for tracking the memory impact of new features Screenshot_20260126_183811 --- ### AI Usage Did you use AI tools to help write this code? _**< PARTIALLY >**_ I wrote the initial version of the script. Gemini was used to help add the Matplotlib-based graphing and threading logic. --- README.md | 14 +++ scripts/debugging_monitor.py | 214 +++++++++++++++++++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100755 scripts/debugging_monitor.py diff --git a/README.md b/README.md index 633ae3b8..9efa6801 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,20 @@ Connect your Xteink X4 to your computer via USB-C and run the following command. ```sh pio run --target upload ``` +### Debugging + +After flashing the new features, it’s recommended to capture detailed logs from the serial port. + +First, make sure all required Python packages are installed: + +```python +python3 -m pip install serial colorama matplotlib +``` +after that run the script: +```sh +python3 scripts/debugging_monitor.py +``` +This was tested on Debian and should work on most Linux systems. Minor adjustments may be required for Windows or macOS. ## Internals diff --git a/scripts/debugging_monitor.py b/scripts/debugging_monitor.py new file mode 100755 index 00000000..57695e2b --- /dev/null +++ b/scripts/debugging_monitor.py @@ -0,0 +1,214 @@ +import sys +import argparse +import re +import threading +from datetime import datetime +from collections import deque +import time + +# Try to import potentially missing packages +try: + import serial + from colorama import init, Fore, Style + import matplotlib.pyplot as plt + import matplotlib.animation as animation +except ImportError as e: + missing_package = e.name + print("\n" + "!" * 50) + print(f" Error: The required package '{missing_package}' is not installed.") + 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("\nExiting...") + sys.exit(1) + +# --- 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 + +# Initialize colors +init(autoreset=True) + +def get_color_for_line(line): + """ + 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 + + return Fore.WHITE + +def parse_memory_line(line): + """ + Extracts Free and Total bytes from the specific log line. + Format: [MEM] Free: 196344 bytes, Total: 226412 bytes, Min Free: 112620 bytes + """ + # Regex to find 'Free: ' and 'Total: ' + match = re.search(r"Free:\s*(\d+).*Total:\s*(\d+)", line) + if match: + try: + free_bytes = int(match.group(1)) + total_bytes = int(match.group(2)) + return free_bytes, total_bytes + except ValueError: + return None, None + return None, None + +def serial_worker(port, baud): + """ + 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}") + + try: + ser = serial.Serial(port, baud, timeout=0.1) + ser.dtr = False + ser.rts = False + except serial.SerialException as e: + print(f"{Fore.RED}Error opening port: {e}{Style.RESET_ALL}") + return + + try: + while True: + try: + raw_data = ser.readline().decode('utf-8', errors='replace') + + if not raw_data: + continue + + clean_line = raw_data.strip() + if not clean_line: + continue + + # Add PC timestamp + pc_time = datetime.now().strftime("%H:%M:%S") + formatted_line = re.sub(r"^\[\d+\]", f"[{pc_time}]", clean_line) + + # Check for Memory Line + if "[MEM]" in formatted_line: + free_val, total_val = parse_memory_line(formatted_line) + if free_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 + + # 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}") + break + except Exception as e: + # If thread is killed violently (e.g. main exit), silence errors + pass + finally: + if 'ser' in locals() and ser.is_open: + ser.close() + +def update_graph(frame): + """ + Called by Matplotlib animation to redraw the chart. + """ + with data_lock: + if not time_data: + 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 + + # Plot Total RAM + 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') + + # Fill area under Free RAM + 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) + + # Rotate date labels + plt.xticks(rotation=45, ha='right') + plt.tight_layout() + +def main(): + 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") + 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) + t.start() + + # 2. Set up the Graph (Main Thread) + try: + plt.style.use('light_background') + except: + pass + + fig = plt.figure(figsize=(10, 6)) + + # Update graph every 1000ms + ani = animation.FuncAnimation(fig, update_graph, interval=1000) + + try: + 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 + +if __name__ == "__main__": + main()