• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dynobo / normcap / 18953976799

30 Oct 2025 08:21PM UTC coverage: 80.119% (-1.4%) from 81.509%
18953976799

Pull #798

github

web-flow
Merge a4477b844 into 50be529a6
Pull Request #798: Refactoring

858 of 1175 new or added lines in 51 files covered. (73.02%)

23 existing lines in 5 files now uncovered.

2974 of 3712 relevant lines covered (80.12%)

4.57 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.86
/normcap/gui/notification_utils.py
1
import logging
6✔
2
import os
6✔
3
import tempfile
6✔
4
import textwrap
6✔
5
from collections.abc import Callable
6✔
6
from pathlib import Path
6✔
7

8
from PySide6 import QtCore, QtGui
6✔
9

10
from normcap import app_id
6✔
11
from normcap.detection.models import (
6✔
12
    DetectionResult,
13
    PlaintextTextTypes,
14
    TextDetector,
15
    TextType,
16
)
17
from normcap.gui.localization import _, translate
6✔
18
from normcap.notification.models import NotificationAction
6✔
19
from normcap.platform import system_info
6✔
20

21
logger = logging.getLogger(__name__)
6✔
22

23

24
def _get_shared_temp_dir() -> Path:
6✔
25
    """Get a temporary directory suitable for beeing opened as URI."""
26
    if system_info.is_flatpak():
6✔
27
        if xdg_runtime_dir := os.getenv("XDG_RUNTIME_DIR"):
×
NEW
28
            temp_dir = Path(xdg_runtime_dir) / "app" / app_id
×
29
        else:
30
            temp_dir = Path.home() / ".cache" / "normcap-temp"
×
31
    else:
32
        temp_dir = Path(tempfile.gettempdir())
6✔
33

34
    temp_dir.mkdir(exist_ok=True, parents=True)
6✔
35
    return temp_dir
6✔
36

37

38
def _open_uris(urls: list[str]) -> None:
6✔
39
    logger.info("urls %s", urls)
6✔
40
    for url in urls:
6✔
41
        logger.debug("Opening URI %s …", url)
6✔
42
        result = QtGui.QDesktopServices.openUrl(
6✔
43
            QtCore.QUrl(url, QtCore.QUrl.ParsingMode.TolerantMode)
44
        )
45
        logger.debug("Opened URI with result=%s", result)
6✔
46

47

48
def _get_line_ending(text: str) -> str:
6✔
49
    if "\r\n" in text:
6✔
50
        return "\r\n"  # Windows-style line endings
×
51

52
    if "\r" in text:
6✔
53
        return "\r"  # Old Mac-style line endings
×
54

55
    return "\n"  # Unix/Linux-style line endings
6✔
56

57

58
def perform_action(texts_and_types: list) -> None:
6✔
59
    logger.debug("Notification clicked.")
6✔
60

61
    texts = [t[0] for t in texts_and_types]
6✔
62
    unique_text_types = list({t[1] for t in texts_and_types})
6✔
63
    line_sep = _get_line_ending(text="".join(texts))
6✔
64
    logger.info("unique %s", unique_text_types)
6✔
65

66
    urls = []
6✔
67
    match unique_text_types:
6✔
68
        case [TextType.URL]:
6✔
69
            urls = texts
6✔
70
        case [TextType.MAIL]:
6✔
71
            urls = [f"mailto:{t.replace(',', ';').replace(' ', '')}" for t in texts]
6✔
72
        case [TextType.PHONE_NUMBER]:
6✔
NEW
73
            urls = [
×
74
                f"tel:{t.replace(' ', '').replace('-', '').replace('/', '')}"
75
                for t in texts
76
            ]
77
        case [TextType.VCARD]:
6✔
NEW
78
            text = line_sep.join(texts)
×
NEW
79
            temp_file = _get_shared_temp_dir() / "normcap_result.vcf"
×
NEW
80
            temp_file.write_text(text)
×
NEW
81
            urls = [temp_file.as_uri()]
×
82
        case [TextType.VEVENT]:
6✔
NEW
83
            text = line_sep.join(texts)
×
NEW
84
            text = f"BEGIN:VCALENDAR{line_sep}{text}{line_sep}END:VCALENDAR"
×
NEW
85
            temp_file = _get_shared_temp_dir() / "normcap_result.ics"
×
NEW
86
            temp_file.write_text(text)
×
NEW
87
            urls = [temp_file.as_uri()]
×
88
        case _:
6✔
89
            temp_file = _get_shared_temp_dir() / "normcap_result.txt"
6✔
90
            temp_file.write_text(line_sep.join(texts))
6✔
91
            urls = [temp_file.as_uri()]
6✔
92

93
    _open_uris(urls)
6✔
94

95

96
def get_actions(
6✔
97
    detection_results: list[DetectionResult], action_func: Callable
98
) -> list[NotificationAction]:
NEW
99
    text_types = [r.text_type for r in detection_results]
×
100

UNCOV
101
    actions = [
×
102
        NotificationAction(
103
            label=get_action_label(text_types=text_types),
104
            func=action_func,
105
            args=[(d.text, d.text_type) for d in detection_results],
106
        )
107
    ]
NEW
108
    if not set(text_types).issubset(set(PlaintextTextTypes)):
×
UNCOV
109
        actions.append(
×
110
            NotificationAction(
111
                label=get_action_label(text_types=[TextType.NONE]),
112
                func=action_func,
113
                args=[(d.text, TextType.NONE) for d in detection_results],
114
            )
115
        )
116
    return actions
×
117

118

119
def _get_code_postfix(detection_results: list[DetectionResult]) -> str:
6✔
120
    if not detection_results:
6✔
121
        # L10N: Notification title when nothing got detected
UNCOV
122
        return _("Nothing captured!")
×
123

124
    detectors = [r.detector for r in detection_results]
6✔
125
    unique_detectors = list(set(detectors))
6✔
126

127
    count = len(detection_results)
6✔
128

129
    match unique_detectors:
6✔
130
        case [TextDetector.QR]:
6✔
131
            # L10N: Notification title.
132
            # Do NOT translate the variables in curly brackets "{some_variable}"!
133
            postfix = translate.ngettext(
6✔
134
                "in 1 QR code", "in {count} QR codes", count
135
            ).format(count=count)
136
        case [TextDetector.BARCODE]:
6✔
137
            # L10N: Notification title.
138
            # Do NOT translate the variables in curly brackets "{some_variable}"!
139
            postfix = translate.ngettext(
6✔
140
                "in 1 barcode", "in {count} barcodes", count
141
            ).format(count=count)
142
        case [TextDetector.BARCODE, TextDetector.QR]:
6✔
143
            # L10N: Notification title.
144
            # Do NOT translate the variables in curly brackets "{some_variable}"!
NEW
145
            postfix = translate.ngettext("in 1 code", "in {count} codes", count).format(
×
146
                count=count
147
            )
148
        case _:
6✔
149
            postfix = ""
6✔
150

151
    return postfix
6✔
152

153

154
def _get_elements_description(detection_results: list[DetectionResult]) -> str:
6✔
155
    unqiue_text_types = list({r.text_type for r in detection_results})
6✔
156
    count = len(detection_results)
6✔
157

158
    match unqiue_text_types:
6✔
159
        case [TextType.MAIL]:
6✔
160
            count = sum(d.text.count("@") for d in detection_results)
6✔
161
            # l10n: notification title.
162
            # do not translate the variables in curly brackets "{some_variable}"!
163
            title = translate.ngettext(
6✔
164
                "1 email captured", "{count} emails captured", count
165
            ).format(count=count)
166
        case [TextType.URL]:
6✔
167
            count = len(detection_results)
6✔
168
            # L10N: Notification title.
169
            # Do NOT translate the variables in curly brackets "{some_variable}"!
170
            title = translate.ngettext(
6✔
171
                "1 URL captured", "{count} URLs captured", count
172
            ).format(count=count)
173
        case [TextType.PHONE_NUMBER]:
6✔
NEW
174
            count = len(detection_results)
×
175
            # L10N: Notification title.
176
            # Do NOT translate the variables in curly brackets "{some_variable}"!
NEW
177
            title = translate.ngettext(
×
178
                "1 phone number captured", "{count} phone numbers captured", count
179
            ).format(count=count)
180
        case [TextType.VEVENT]:
6✔
NEW
181
            count = len(detection_results)
×
182
            # L10N: Notification title.
183
            # Do NOT translate the variables in curly brackets "{some_variable}"!
NEW
184
            title = translate.ngettext(
×
185
                "1 calendar event captured", "{count} calender events captured", count
186
            ).format(count=count)
187
        case [TextType.VCARD]:
6✔
NEW
188
            count = len(detection_results)
×
189
            # L10N: Notification title.
190
            # Do NOT translate the variables in curly brackets "{some_variable}"!
NEW
191
            title = translate.ngettext(
×
192
                "1 contact captured", "{count} contact captured", count
193
            ).format(count=count)
194
        case [TextType.PARAGRAPH]:
6✔
195
            count = sum(d.text.count(os.linesep * 2) for d in detection_results) + 1
6✔
196
            title = translate.ngettext(
6✔
197
                "1 paragraph captured", "{count} paragraphs captured", count
198
            ).format(count=count)
199
        case [TextType.MULTI_LINE]:
6✔
200
            count = sum(d.text.count(os.linesep) + 1 for d in detection_results)
6✔
201
            # L10N: Notification title.
202
            # Do NOT translate the variables in curly brackets "{some_variable}"!
203
            title = translate.ngettext(
6✔
204
                "1 line captured", "{count} lines captured", count
205
            ).format(count=count)
206
        case _:
6✔
207
            count = sum(d.text.count(" ") + 1 for d in detection_results)
6✔
208
            # L10N: Notification title.
209
            # Do NOT translate the variables in curly brackets "{some_variable}"!
210
            title = translate.ngettext(
6✔
211
                "1 word captured", "{count} words captured", count
212
            ).format(count=count)
213

214
    return title
6✔
215

216

217
def get_title(detection_results: list[DetectionResult]) -> str:
6✔
218
    if not detection_results:
6✔
219
        # L10N: Notification title when nothing got detected
220
        return _("Nothing captured!")
6✔
221

222
    description = _get_elements_description(detection_results=detection_results)
6✔
223
    postfix = _get_code_postfix(detection_results=detection_results)
6✔
224
    return f"{description} {postfix}"
6✔
225

226

227
def get_text(detection_results: list[DetectionResult]) -> str:
6✔
228
    if not detection_results:
6✔
229
        # L10N: Notification text when nothing got detected
230
        return _("Please try again.")
6✔
231

232
    text = " ".join(d.text.strip().replace(os.linesep, " ") for d in detection_results)
6✔
233

234
    shortened = textwrap.shorten(text, width=45, placeholder=" […]").strip()
6✔
235
    if shortened == "[…]":
6✔
236
        # We have one long word which shorten() can not handle
237
        shortened = text
×
238

239
    return shortened
6✔
240

241

242
def get_action_label(text_types: list[TextType]) -> str:
6✔
243
    unique_text_types = list(set(text_types))
6✔
244
    match unique_text_types:
6✔
245
        case [TextType.MAIL]:
6✔
246
            # L10N: Button text of notification action in Linux.
247
            action_name = _("Compose Email")
6✔
248
        case [TextType.PHONE_NUMBER]:
6✔
249
            # L10N: Button text of notification action in Linux.
250
            action_name = _("Call Number")
6✔
251
        case [TextType.URL]:
6✔
252
            # L10N: Button text of notification action in Linux.
253
            action_name = _("Open in Browser")
6✔
254
        case [TextType.VCARD]:
6✔
255
            # L10N: Button text of notification action in Linux.
256
            action_name = _("Import to Adressbook")
6✔
257
        case [TextType.VEVENT]:
6✔
258
            # L10N: Button text of notification action in Linux.
259
            action_name = _("Import to Calendar")
6✔
260
        case _:
6✔
261
            # L10N: Button text of notification action in Linux.
262
            action_name = _("Open in Editor")
6✔
263
    return action_name
6✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc