diff --git a/README.md b/README.md index 4a52ef4..1019d48 100644 --- a/README.md +++ b/README.md @@ -86,10 +86,9 @@ report from within the app, or optionally to check for new versions to upgrade t ## How to install -Unless you are using the Debian option below: +Make sure you have `libxcb-cursor0` installed (on Debian-based distributions) or `xcb-util-cursor` (RedHat/Fedora-based distributions). - * Make sure you have `libxcb-cursor0` installed (on Debian-based distributions) or `xcb-util-cursor` (RedHat/Fedora-based distributions). - * If downloading from my Forgejo's Releases page, you may wish to verify the GPG signatures with my [GPG key](https://mig5.net/static/mig5.asc). +If downloading from my Forgejo's Releases page, you may wish to verify the GPG signatures with my [GPG key](https://mig5.net/static/mig5.asc). ### Debian 13 ('Trixie') diff --git a/debian/changelog b/debian/changelog index 0216393..146e349 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,16 +1,3 @@ -bouquin (0.8.0) unstable; urgency=medium - - * Add .desktop file for Debian - * Fix Pomodoro timer rounding so it rounds up to 0.25, but rounds to closest quarter (up or down) - * Allow setting a code block on a line that already has text (it will start a newline for the codeblock) - * Retain indentation when tab is used to indent a line, unless enter is pressed twice or user deletes the indentation - * Add ability to collapse/expand sections of text. - * Add 'Last Month' date range for timesheet reports - * Add missing strings (for English and French) - * Don't offer to download latest AppImage unless we are running as an AppImage already - - -- Miguel Jacq Tue, 23 Dec 2025 17:30:00 +1100 - bouquin (0.7.5) unstable; urgency=medium * Add libxcb-cursor0 dependency diff --git a/tests-debian-packaging.sh b/tests-debian-packaging.sh new file mode 100755 index 0000000..cbefc31 --- /dev/null +++ b/tests-debian-packaging.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +set -eou pipefail + +DISTS=( + debian:trixie +) + +for dist in ${DISTS[@]}; do + release=$(echo ${dist} | cut -d: -f2) + mkdir -p dist/${release} + + docker build -f Dockerfile.debbuild -t bouquin-deb:${release} \ + --no-cache \ + --progress=plain \ + --build-arg BASE_IMAGE=${dist} . + + docker run --rm \ + -e SUITE="${release}" \ + -v "$PWD":/src \ + -v "$PWD/dist/${release}":/out \ + bouquin-deb:${release} + + debfile=$(ls -1 dist/${release}/*.deb) +done diff --git a/tests/test_code_block_editor_dialog.py b/tests/test_code_block_editor_dialog.py index 1ced14c..e64199b 100644 --- a/tests/test_code_block_editor_dialog.py +++ b/tests/test_code_block_editor_dialog.py @@ -3,8 +3,8 @@ from bouquin.code_block_editor_dialog import ( CodeBlockEditorDialog, CodeEditorWithLineNumbers, ) -from PySide6.QtCore import QRect, QSize, Qt -from PySide6.QtGui import QFont, QPaintEvent, QTextCursor +from PySide6.QtCore import QRect, QSize +from PySide6.QtGui import QFont, QPaintEvent from PySide6.QtWidgets import QPushButton @@ -323,42 +323,3 @@ def test_code_editor_viewport_margins(qtbot, app): assert margins.top() == 0 assert margins.right() == 0 assert margins.bottom() == 0 - - -def test_code_editor_retains_indentation_on_enter(qtbot, app): - """Pressing Enter on an indented line retains indentation in code editor.""" - editor = CodeEditorWithLineNumbers() - qtbot.addWidget(editor) - editor.setPlainText("\tfoo") - editor.show() - - cursor = editor.textCursor() - cursor.movePosition(QTextCursor.End) - editor.setTextCursor(cursor) - - qtbot.keyPress(editor, Qt.Key_Return) - qtbot.wait(0) - - assert editor.toPlainText().endswith("\tfoo\n\t") - - -def test_code_editor_double_enter_on_empty_indent_resets(qtbot, app): - """Second Enter on an indentation-only line clears the indent in code editor.""" - editor = CodeEditorWithLineNumbers() - qtbot.addWidget(editor) - editor.setPlainText("\tfoo") - editor.show() - - cursor = editor.textCursor() - cursor.movePosition(QTextCursor.End) - editor.setTextCursor(cursor) - - qtbot.keyPress(editor, Qt.Key_Return) - qtbot.wait(0) - assert editor.toPlainText().endswith("\tfoo\n\t") - - qtbot.keyPress(editor, Qt.Key_Return) - qtbot.wait(0) - - assert editor.toPlainText().endswith("\tfoo\n\n") - assert editor.textCursor().block().text() == "" diff --git a/tests/test_markdown_editor.py b/tests/test_markdown_editor.py index a36a09e..dcacbc5 100644 --- a/tests/test_markdown_editor.py +++ b/tests/test_markdown_editor.py @@ -150,53 +150,6 @@ def test_enter_on_nonempty_list_continues(qtbot, editor): assert "\n\u2022 " in txt -def test_tab_indentation_is_retained_on_newline(editor, qtbot): - """Pressing Enter on an indented line should retain the indentation.""" - qtbot.addWidget(editor) - editor.show() - editor.setPlainText("\tfoo") - editor.moveCursor(QTextCursor.End) - - qtbot.keyPress(editor, Qt.Key_Return) - qtbot.wait(0) - - assert editor.toPlainText().endswith("\tfoo\n\t") - - -def test_double_enter_on_empty_indented_line_resets_indent(editor, qtbot): - """A second Enter on an indentation-only line should reset to column 0.""" - qtbot.addWidget(editor) - editor.show() - editor.setPlainText("\tfoo") - editor.moveCursor(QTextCursor.End) - - # First Enter inserts a new indented line - qtbot.keyPress(editor, Qt.Key_Return) - qtbot.wait(0) - assert editor.toPlainText().endswith("\tfoo\n\t") - - # Second Enter on the now-empty indented line removes the indent - qtbot.keyPress(editor, Qt.Key_Return) - qtbot.wait(0) - - assert editor.toPlainText().endswith("\tfoo\n\n") - # Cursor should be on a fresh unindented blank line - assert editor.textCursor().block().text() == "" - - -def test_nested_list_continuation_preserves_indentation(editor, qtbot): - """Enter on an indented bullet should keep indent + bullet prefix.""" - qtbot.addWidget(editor) - editor.show() - editor.from_markdown("\t- item") - editor.moveCursor(QTextCursor.End) - - qtbot.keyPress(editor, Qt.Key_Return) - qtbot.wait(0) - - assert "\n\t\u2022 " in editor.toPlainText() - - def test_enter_on_empty_list_marks_empty(qtbot, editor): qtbot.addWidget(editor) editor.show() @@ -228,116 +181,6 @@ def test_triple_backtick_triggers_code_dialog_but_no_block_on_empty_code(editor, assert t == "" -def _find_first_block(doc, predicate): - b = doc.begin() - while b.isValid(): - if predicate(b): - return b - b = b.next() - return None - - -def test_collapse_selection_wraps_and_hides_blocks(editor, qtbot): - """Collapsing a selection should insert header/end marker and hide content.""" - qtbot.addWidget(editor) - editor.show() - editor.setPlainText("a\nb\nc\n") - doc = editor.document() - - # Select lines b..c - b_block = doc.findBlockByNumber(1) - c_block = doc.findBlockByNumber(2) - cur = editor.textCursor() - cur.setPosition(b_block.position()) - cur.setPosition(c_block.position() + c_block.length() - 1, QTextCursor.KeepAnchor) - editor.setTextCursor(cur) - - editor.collapse_selection() - qtbot.wait(0) - - # Header and end marker should exist as their own blocks - header = _find_first_block(doc, lambda bl: bl.text().lstrip().startswith("▸")) - assert header is not None - assert "▸" in header.text() and "expand" in header.text() - - end_marker = _find_first_block(doc, lambda bl: "bouquin:collapse:end" in bl.text()) - assert end_marker is not None - - # Inner blocks should be hidden; end marker always hidden - inner1 = header.next() - inner2 = inner1.next() - assert inner1.text() == "b" - assert inner2.text() == "c" - assert inner1.isVisible() is False - assert inner2.isVisible() is False - assert end_marker.isVisible() is False - - -def test_toggle_collapse_expands_and_updates_header(editor, qtbot): - """Toggling a collapse header should reveal hidden blocks and flip label.""" - qtbot.addWidget(editor) - editor.show() - editor.setPlainText("a\nb\nc\n") - doc = editor.document() - - # Select b..c and collapse - b_block = doc.findBlockByNumber(1) - c_block = doc.findBlockByNumber(2) - cur = editor.textCursor() - cur.setPosition(b_block.position(), QTextCursor.MoveMode.MoveAnchor) - cur.setPosition( - c_block.position() + c_block.length() - 1, QTextCursor.MoveMode.KeepAnchor - ) - editor.setTextCursor(cur) - editor.collapse_selection() - qtbot.wait(0) - - header = _find_first_block(doc, lambda bl: bl.text().lstrip().startswith("▸")) - assert header is not None - - # Toggle to expand - editor._toggle_collapse_at_block(header) - qtbot.wait(0) - - header2 = doc.findBlock(header.position()) - assert "▾" in header2.text() and "collapse" in header2.text() - assert header2.next().isVisible() is True - assert header2.next().next().isVisible() is True - - -def test_collapse_selection_without_trailing_newline_keeps_marker_on_own_line( - editor, qtbot -): - """Selections reaching EOF without a trailing newline should still fold correctly.""" - qtbot.addWidget(editor) - editor.show() - editor.setPlainText("a\nb\nc") # no trailing newline - doc = editor.document() - - # Bottom-up selection of last two lines (c..b) - b_block = doc.findBlockByNumber(1) - c_block = doc.findBlockByNumber(2) - cur = editor.textCursor() - cur.setPosition(c_block.position() + len(c_block.text())) - cur.setPosition(b_block.position(), QTextCursor.KeepAnchor) - editor.setTextCursor(cur) - - editor.collapse_selection() - qtbot.wait(0) - - end_marker = _find_first_block(doc, lambda bl: "bouquin:collapse:end" in bl.text()) - assert end_marker is not None - - # End marker is its own block, and remains hidden - assert end_marker.text().strip() == "" - assert end_marker.isVisible() is False - - # The last content line should be hidden (folded) - header = _find_first_block(doc, lambda bl: bl.text().lstrip().startswith("▸")) - assert header is not None - assert header.next().isVisible() is False - - def test_down_escapes_from_last_code_line(editor, qtbot): editor.from_markdown("```\nLINE\n```\n") # Put caret at end of "LINE" diff --git a/tests/test_pomodoro_timer.py b/tests/test_pomodoro_timer.py index 1dd4d95..1c2e450 100644 --- a/tests/test_pomodoro_timer.py +++ b/tests/test_pomodoro_timer.py @@ -276,7 +276,7 @@ def test_pomodoro_manager_on_timer_stopped_minimum_hours( def test_pomodoro_manager_on_timer_stopped_rounding(qtbot, app, fresh_db, monkeypatch): - """Elapsed time should be rounded to a 0.25-hour increment.""" + """Elapsed time should be rounded up to the nearest 0.25 hours.""" parent = DummyMainWindow(app) qtbot.addWidget(parent) qtbot.addWidget(parent.time_log) @@ -300,31 +300,6 @@ def test_pomodoro_manager_on_timer_stopped_rounding(qtbot, app, fresh_db, monkey assert hours_set * 4 == int(hours_set * 4) -def test_seconds_to_logged_hours_nearest_quarter_rounding(): - """Seconds -> hours uses nearest-quarter rounding with a 15-min minimum.""" - # Import the pure conversion helper directly (no Qt required) - from bouquin.pomodoro_timer import PomodoroManager - - # <15 minutes always rounds up to 0.25 - assert PomodoroManager._seconds_to_logged_hours(1) == 0.25 - assert PomodoroManager._seconds_to_logged_hours(899) == 0.25 - - # 15 minutes exact - assert PomodoroManager._seconds_to_logged_hours(900) == 0.25 - - # Examples from the spec: closest quarter-hour - # 33 minutes -> closer to 0.50 than 0.75 - assert PomodoroManager._seconds_to_logged_hours(33 * 60) == 0.50 - # 40 minutes -> closer to 0.75 than 0.50 - assert PomodoroManager._seconds_to_logged_hours(40 * 60) == 0.75 - - # Halfway case: 22.5 min is exactly between 0.25 and 0.50 -> round up - assert PomodoroManager._seconds_to_logged_hours(int(22.5 * 60)) == 0.50 - - # Sanity: 1 hour stays 1.0 - assert PomodoroManager._seconds_to_logged_hours(60 * 60) == 1.00 - - def test_pomodoro_manager_on_timer_stopped_prefills_note( qtbot, app, fresh_db, monkeypatch ): diff --git a/tests/test_time_log.py b/tests/test_time_log.py index b03029e..45db626 100644 --- a/tests/test_time_log.py +++ b/tests/test_time_log.py @@ -1211,26 +1211,6 @@ def test_time_report_dialog_default_date_range(qtbot, fresh_db): assert dialog.to_date.date() == today -def test_time_report_dialog_last_month_preset_sets_full_previous_month(qtbot, fresh_db): - """Selecting 'Last month' sets the date range to the previous calendar month.""" - strings.load_strings("en") - dialog = TimeReportDialog(fresh_db) - qtbot.addWidget(dialog) - - idx = dialog.range_preset.findData("last_month") - assert idx != -1 - - today = QDate.currentDate() - start_of_this_month = QDate(today.year(), today.month(), 1) - expected_start = start_of_this_month.addMonths(-1) - expected_end = start_of_this_month.addDays(-1) - - dialog.range_preset.setCurrentIndex(idx) - - assert dialog.from_date.date() == expected_start - assert dialog.to_date.date() == expected_end - - def test_time_report_dialog_run_report(qtbot, fresh_db): """Run a time report.""" strings.load_strings("en")