Skip to content

Conflict detection

conflicts

Detect trigger-recognition conflicts between candidate and existing hotstrings.

Conflict detection models the three trigger-recognition dimensions relevant to whether two hotstrings can activate on overlapping typed text:

  • case sensitivity (C / C0 / C1);
  • whether an alphanumeric predecessor is permitted (? / ?0);
  • whether an ending character is required (* / *0).

Other hotstring options affect replacement or execution behavior rather than trigger recognition and therefore do not restrict conflict checking.

ConflictKind

Bases: Enum

Classify why two hotstrings conflict.

Attributes:

Name Type Description
SAME_TRIGGER

Candidate and existing definitions can recognize the same complete trigger text.

EXISTING_FIRES_DURING_CANDIDATE

The existing definition can become eligible while a valid typed form of the candidate trigger is being entered.

CANDIDATE_FIRES_DURING_EXISTING

The candidate can become eligible while a valid typed form of the existing trigger is being entered.

Source code in hotstring\core\conflicts.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class ConflictKind(Enum):
    """Classify why two hotstrings conflict.

    Attributes:
        SAME_TRIGGER:
            Candidate and existing definitions can recognize the same complete
            trigger text.
        EXISTING_FIRES_DURING_CANDIDATE:
            The existing definition can become eligible while a valid typed
            form of the candidate trigger is being entered.
        CANDIDATE_FIRES_DURING_EXISTING:
            The candidate can become eligible while a valid typed form of the
            existing trigger is being entered.
    """

    SAME_TRIGGER = "same trigger"
    EXISTING_FIRES_DURING_CANDIDATE = (
        "existing hotstring can activate while candidate is typed"
    )
    CANDIDATE_FIRES_DURING_EXISTING = (
        "candidate can activate while existing hotstring is typed"
    )

HotstringConflict dataclass

Describe one conflict between a candidate and an existing definition.

Attributes:

Name Type Description
candidate CandidateHotstring

Candidate involved in the conflict.

existing ExistingHotstring

Existing definition involved in the conflict.

kind ConflictKind

Conflict classification.

reason str

Human-readable explanation of the matching overlap.

Source code in hotstring\core\conflicts.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@dataclass(frozen=True, slots=True)
class HotstringConflict:
    """Describe one conflict between a candidate and an existing definition.

    Attributes:
        candidate:
            Candidate involved in the conflict.
        existing:
            Existing definition involved in the conflict.
        kind:
            Conflict classification.
        reason:
            Human-readable explanation of the matching overlap.
    """

    candidate: CandidateHotstring
    existing: ExistingHotstring
    kind: ConflictKind
    reason: str

CandidateAssessment dataclass

Represent all conflicts discovered for one candidate.

Attributes:

Name Type Description
candidate CandidateHotstring

Candidate that was checked.

conflicts tuple[HotstringConflict, ...]

All conflicting existing hotstrings.

Source code in hotstring\core\conflicts.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
@dataclass(frozen=True, slots=True)
class CandidateAssessment:
    """Represent all conflicts discovered for one candidate.

    Attributes:
        candidate:
            Candidate that was checked.
        conflicts:
            All conflicting existing hotstrings.
    """

    candidate: CandidateHotstring
    conflicts: tuple[HotstringConflict, ...]

    @property
    def is_accepted(self) -> bool:
        """Return whether the candidate has no detected conflicts.

        Returns:
            Whether the candidate is conflict-free.
        """
        return not self.conflicts

is_accepted property

is_accepted: bool

Return whether the candidate has no detected conflicts.

Returns:

Type Description
bool

Whether the candidate is conflict-free.

_TriggerOverlap dataclass

Describe a trigger occurrence satisfying both recognition boundaries.

Attributes:

Name Type Description
start int

Inclusive start index inside the containing semantic trigger.

end int

Exclusive end index inside the containing semantic trigger.

left_reason str

Explanation of the valid left boundary.

right_reason str

Explanation of the valid right boundary.

Source code in hotstring\core\conflicts.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
@dataclass(frozen=True, slots=True)
class _TriggerOverlap:
    """Describe a trigger occurrence satisfying both recognition boundaries.

    Attributes:
        start:
            Inclusive start index inside the containing semantic trigger.
        end:
            Exclusive end index inside the containing semantic trigger.
        left_reason:
            Explanation of the valid left boundary.
        right_reason:
            Explanation of the valid right boundary.
    """

    start: int
    end: int
    left_reason: str
    right_reason: str

    @property
    def reason(self) -> str:
        """Return the combined boundary explanation.

        Returns:
            Human-readable explanation of both boundaries.
        """
        return f"{self.left_reason}; {self.right_reason}"

reason property

reason: str

Return the combined boundary explanation.

Returns:

Type Description
str

Human-readable explanation of both boundaries.

find_conflict

find_conflict(
    candidate: CandidateHotstring,
    existing: ExistingHotstring,
    *,
    ending_chars: frozenset[str] = DEFAULT_ENDING_CHARS,
    option_defaults: ResolvedHotstringOptions = DEFAULT_HOTSTRING_OPTIONS,
) -> HotstringConflict | None

Check one candidate against one existing hotstring.

All combinations of case sensitivity, inside-word recognition, and ending-character requirements are supported for both definitions.

Parameters:

Name Type Description Default
candidate CandidateHotstring

Candidate to check.

required
existing ExistingHotstring

Existing definition to compare against.

required
ending_chars frozenset[str]

Effective AutoHotkey ending-character set.

DEFAULT_ENDING_CHARS
option_defaults ResolvedHotstringOptions

Fully resolved defaults applicable to inherited hotstring options.

DEFAULT_HOTSTRING_OPTIONS

Returns:

Type Description
HotstringConflict | None

Detected conflict, or None when the pair does not conflict.

Source code in hotstring\core\conflicts.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
def find_conflict(
    candidate: CandidateHotstring,
    existing: ExistingHotstring,
    *,
    ending_chars: frozenset[str] = DEFAULT_ENDING_CHARS,
    option_defaults: ResolvedHotstringOptions = DEFAULT_HOTSTRING_OPTIONS,
) -> HotstringConflict | None:
    """Check one candidate against one existing hotstring.

    All combinations of case sensitivity, inside-word recognition, and
    ending-character requirements are supported for both definitions.

    Args:
        candidate:
            Candidate to check.
        existing:
            Existing definition to compare against.
        ending_chars:
            Effective AutoHotkey ending-character set.
        option_defaults:
            Fully resolved defaults applicable to inherited hotstring options.

    Returns:
        Detected conflict, or `None` when the pair does not conflict.
    """
    candidate_options = ResolvedHotstringOptions.from_options(
        candidate.options,
        defaults=option_defaults,
    )
    existing_options = ResolvedHotstringOptions.from_options(
        existing.options,
        defaults=option_defaults,
    )

    return _find_candidate_conflict(
        candidate,
        candidate_options,
        existing,
        existing_options,
        ending_chars=ending_chars,
    )

assess_candidate

assess_candidate(
    candidate: CandidateHotstring,
    existing_hotstrings: Sequence[ExistingHotstring],
    *,
    ending_chars: frozenset[str] = DEFAULT_ENDING_CHARS,
    option_defaults: ResolvedHotstringOptions = DEFAULT_HOTSTRING_OPTIONS,
) -> CandidateAssessment

Check one candidate against all existing definitions.

Parameters:

Name Type Description Default
candidate CandidateHotstring

Candidate to evaluate.

required
existing_hotstrings Sequence[ExistingHotstring]

Existing definitions to compare against.

required
ending_chars frozenset[str]

Effective AutoHotkey ending-character set.

DEFAULT_ENDING_CHARS
option_defaults ResolvedHotstringOptions

Fully resolved defaults applicable to inherited hotstring options.

DEFAULT_HOTSTRING_OPTIONS

Returns:

Type Description
CandidateAssessment

Complete candidate assessment.

Source code in hotstring\core\conflicts.py
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
def assess_candidate(
    candidate: CandidateHotstring,
    existing_hotstrings: Sequence[ExistingHotstring],
    *,
    ending_chars: frozenset[str] = DEFAULT_ENDING_CHARS,
    option_defaults: ResolvedHotstringOptions = DEFAULT_HOTSTRING_OPTIONS,
) -> CandidateAssessment:
    """Check one candidate against all existing definitions.

    Args:
        candidate:
            Candidate to evaluate.
        existing_hotstrings:
            Existing definitions to compare against.
        ending_chars:
            Effective AutoHotkey ending-character set.
        option_defaults:
            Fully resolved defaults applicable to inherited hotstring options.

    Returns:
        Complete candidate assessment.
    """
    candidate_options = ResolvedHotstringOptions.from_options(
        candidate.options,
        defaults=option_defaults,
    )

    conflicts: list[HotstringConflict] = []
    for existing in existing_hotstrings:
        existing_options = ResolvedHotstringOptions.from_options(
            existing.options,
            defaults=option_defaults,
        )
        conflict = _find_candidate_conflict(
            candidate,
            candidate_options,
            existing,
            existing_options,
            ending_chars=ending_chars,
        )
        if conflict is not None:
            conflicts.append(conflict)

    return CandidateAssessment(candidate=candidate, conflicts=tuple(conflicts))

assess_candidates

assess_candidates(
    candidates: Sequence[CandidateHotstring],
    existing_hotstrings: Sequence[ExistingHotstring],
    *,
    ending_chars: frozenset[str] = DEFAULT_ENDING_CHARS,
    option_defaults: ResolvedHotstringOptions = DEFAULT_HOTSTRING_OPTIONS,
) -> tuple[CandidateAssessment, ...]

Check multiple candidates against all existing definitions.

Candidate-to-candidate checking is intentionally not performed here; typo-generation aggregation handles internal candidate ambiguity before the full pipeline reaches this stage.

Parameters:

Name Type Description Default
candidates Sequence[CandidateHotstring]

Candidates to evaluate.

required
existing_hotstrings Sequence[ExistingHotstring]

Existing definitions to compare against.

required
ending_chars frozenset[str]

Effective ending-character set.

DEFAULT_ENDING_CHARS
option_defaults ResolvedHotstringOptions

Fully resolved defaults applicable to inherited hotstring options.

DEFAULT_HOTSTRING_OPTIONS

Returns:

Type Description
tuple[CandidateAssessment, ...]

Candidate assessments in input order.

Source code in hotstring\core\conflicts.py
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
def assess_candidates(
    candidates: Sequence[CandidateHotstring],
    existing_hotstrings: Sequence[ExistingHotstring],
    *,
    ending_chars: frozenset[str] = DEFAULT_ENDING_CHARS,
    option_defaults: ResolvedHotstringOptions = DEFAULT_HOTSTRING_OPTIONS,
) -> tuple[CandidateAssessment, ...]:
    """Check multiple candidates against all existing definitions.

    Candidate-to-candidate checking is intentionally not performed here;
    typo-generation aggregation handles internal candidate ambiguity before
    the full pipeline reaches this stage.

    Args:
        candidates:
            Candidates to evaluate.
        existing_hotstrings:
            Existing definitions to compare against.
        ending_chars:
            Effective ending-character set.
        option_defaults:
            Fully resolved defaults applicable to inherited hotstring options.

    Returns:
        Candidate assessments in input order.
    """
    return tuple(
        assess_candidate(
            candidate,
            existing_hotstrings,
            ending_chars=ending_chars,
            option_defaults=option_defaults,
        )
        for candidate in candidates
    )

_find_candidate_conflict

_find_candidate_conflict(
    candidate: CandidateHotstring,
    candidate_options: ResolvedHotstringOptions,
    existing: ExistingHotstring,
    existing_options: ResolvedHotstringOptions,
    *,
    ending_chars: frozenset[str],
) -> HotstringConflict | None

Check one existing definition against one candidate.

Parameters:

Name Type Description Default
candidate CandidateHotstring

Candidate definition.

required
candidate_options ResolvedHotstringOptions

Fully resolved candidate options.

required
existing ExistingHotstring

Existing definition.

required
existing_options ResolvedHotstringOptions

Fully resolved existing options.

required
ending_chars frozenset[str]

Effective ending-character set.

required

Returns:

Type Description
HotstringConflict | None

Detected conflict, or None.

Source code in hotstring\core\conflicts.py
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
def _find_candidate_conflict(
    candidate: CandidateHotstring,
    candidate_options: ResolvedHotstringOptions,
    existing: ExistingHotstring,
    existing_options: ResolvedHotstringOptions,
    *,
    ending_chars: frozenset[str],
) -> HotstringConflict | None:
    """Check one existing definition against one candidate.

    Args:
        candidate:
            Candidate definition.
        candidate_options:
            Fully resolved candidate options.
        existing:
            Existing definition.
        existing_options:
            Fully resolved existing options.
        ending_chars:
            Effective ending-character set.

    Returns:
        Detected conflict, or `None`.
    """
    candidate_case_sensitive = candidate_options.case_mode is CaseMode.SENSITIVE
    existing_case_sensitive = existing_options.case_mode is CaseMode.SENSITIVE

    if _same_trigger_can_match(
        candidate,
        candidate_case_sensitive=candidate_case_sensitive,
        existing=existing,
        existing_case_sensitive=existing_case_sensitive,
    ):
        return HotstringConflict(
            candidate=candidate,
            existing=existing,
            kind=ConflictKind.SAME_TRIGGER,
            reason=(
                "The candidate and existing hotstring can recognize the same "
                "complete trigger text."
            ),
        )

    existing_overlap = next(
        _iter_trigger_overlaps(
            trigger=existing,
            container=candidate,
            allow_alphanumeric_predecessor=(
                existing_options.trigger_inside_word is SettingState.ENABLED
            ),
            require_ending_character=(
                existing_options.ending_character_optional is SettingState.DISABLED
            ),
            trigger_case_sensitive=existing_case_sensitive,
            container_case_sensitive=candidate_case_sensitive,
            ending_chars=ending_chars,
        ),
        None,
    )
    if existing_overlap is not None:
        return HotstringConflict(
            candidate=candidate,
            existing=existing,
            kind=ConflictKind.EXISTING_FIRES_DURING_CANDIDATE,
            reason=(
                "The existing hotstring can activate while the candidate is "
                f"being typed: {existing_overlap.reason}."
            ),
        )

    candidate_overlap = next(
        _iter_trigger_overlaps(
            trigger=candidate,
            container=existing,
            allow_alphanumeric_predecessor=(
                candidate_options.trigger_inside_word is SettingState.ENABLED
            ),
            require_ending_character=(
                candidate_options.ending_character_optional is SettingState.DISABLED
            ),
            trigger_case_sensitive=candidate_case_sensitive,
            container_case_sensitive=existing_case_sensitive,
            ending_chars=ending_chars,
        ),
        None,
    )
    if candidate_overlap is not None:
        return HotstringConflict(
            candidate=candidate,
            existing=existing,
            kind=ConflictKind.CANDIDATE_FIRES_DURING_EXISTING,
            reason=(
                "The candidate can activate while the existing hotstring is "
                f"being typed: {candidate_overlap.reason}."
            ),
        )

    return None

_same_trigger_can_match

_same_trigger_can_match(
    candidate: Hotstring,
    *,
    candidate_case_sensitive: bool,
    existing: Hotstring,
    existing_case_sensitive: bool,
) -> bool

Return whether two complete trigger definitions share a typed form.

If both hotstrings are case-sensitive, their semantic triggers must match exactly. If either definition is case-insensitive, a shared casing exists whenever their AutoHotkey-compatible case-insensitive keys are equal.

Parameters:

Name Type Description Default
candidate Hotstring

First hotstring.

required
candidate_case_sensitive bool

Whether the first hotstring requires exact case.

required
existing Hotstring

Second hotstring.

required
existing_case_sensitive bool

Whether the second hotstring requires exact case.

required

Returns:

Type Description
bool

Whether the two complete trigger definitions can recognize the same

bool

typed text.

Source code in hotstring\core\conflicts.py
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
def _same_trigger_can_match(
    candidate: Hotstring,
    *,
    candidate_case_sensitive: bool,
    existing: Hotstring,
    existing_case_sensitive: bool,
) -> bool:
    """Return whether two complete trigger definitions share a typed form.

    If both hotstrings are case-sensitive, their semantic triggers must match
    exactly. If either definition is case-insensitive, a shared casing exists
    whenever their AutoHotkey-compatible case-insensitive keys are equal.

    Args:
        candidate:
            First hotstring.
        candidate_case_sensitive:
            Whether the first hotstring requires exact case.
        existing:
            Second hotstring.
        existing_case_sensitive:
            Whether the second hotstring requires exact case.

    Returns:
        Whether the two complete trigger definitions can recognize the same
        typed text.
    """
    if candidate_case_sensitive and existing_case_sensitive:
        return candidate.semantic_trigger == existing.semantic_trigger

    return (
        candidate.case_insensitive_semantic_trigger_key
        == existing.case_insensitive_semantic_trigger_key
    )

_iter_trigger_overlaps

_iter_trigger_overlaps(
    *,
    trigger: Hotstring,
    container: Hotstring,
    allow_alphanumeric_predecessor: bool,
    require_ending_character: bool,
    trigger_case_sensitive: bool,
    container_case_sensitive: bool,
    ending_chars: frozenset[str],
) -> Iterator[_TriggerOverlap]

Yield occurrences satisfying trigger recognition and both boundaries.

trigger is the hotstring whose ability to activate is being tested. container is the other hotstring whose trigger is being typed.

Case handling must consider both definitions. If both are case-sensitive, only the exact source-defined casing of the container is a valid typed form. If either is case-insensitive, a shared casing can exist for an occurrence whenever the corresponding case-insensitive keys match.

Parameters:

Name Type Description Default
trigger Hotstring

Hotstring whose activation is being tested.

required
container Hotstring

Other hotstring whose semantic trigger is being typed around it.

required
allow_alphanumeric_predecessor bool

Whether the tested hotstring permits an alphanumeric predecessor.

required
require_ending_character bool

Whether the tested hotstring requires an ending character.

required
trigger_case_sensitive bool

Whether the tested hotstring requires exact trigger casing.

required
container_case_sensitive bool

Whether the containing hotstring requires exact trigger casing.

required
ending_chars frozenset[str]

Effective ending-character set.

required

Yields:

Type Description
_TriggerOverlap

Every boundary-valid occurrence.

Source code in hotstring\core\conflicts.py
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
def _iter_trigger_overlaps(
    *,
    trigger: Hotstring,
    container: Hotstring,
    allow_alphanumeric_predecessor: bool,
    require_ending_character: bool,
    trigger_case_sensitive: bool,
    container_case_sensitive: bool,
    ending_chars: frozenset[str],
) -> Iterator[_TriggerOverlap]:
    """Yield occurrences satisfying trigger recognition and both boundaries.

    `trigger` is the hotstring whose ability to activate is being tested.
    `container` is the other hotstring whose trigger is being typed.

    Case handling must consider *both* definitions. If both are case-sensitive,
    only the exact source-defined casing of the container is a valid typed
    form. If either is case-insensitive, a shared casing can exist for an
    occurrence whenever the corresponding case-insensitive keys match.

    Args:
        trigger:
            Hotstring whose activation is being tested.
        container:
            Other hotstring whose semantic trigger is being typed around it.
        allow_alphanumeric_predecessor:
            Whether the tested hotstring permits an alphanumeric predecessor.
        require_ending_character:
            Whether the tested hotstring requires an ending character.
        trigger_case_sensitive:
            Whether the tested hotstring requires exact trigger casing.
        container_case_sensitive:
            Whether the containing hotstring requires exact trigger casing.
        ending_chars:
            Effective ending-character set.

    Yields:
        Every boundary-valid occurrence.
    """
    if len(trigger.semantic_trigger) > len(container.semantic_trigger):
        return

    require_exact_case = trigger_case_sensitive and container_case_sensitive

    for start in _iter_hotstring_occurrence_starts(
        trigger=trigger,
        container=container,
        require_exact_case=require_exact_case,
    ):
        end = start + len(trigger.semantic_trigger)

        left_reason = _left_boundary_reason(
            container=container.semantic_trigger,
            start=start,
            allow_alphanumeric_predecessor=allow_alphanumeric_predecessor,
        )
        if left_reason is None:
            continue

        right_reason = _right_boundary_reason(
            container=container.semantic_trigger,
            end=end,
            require_ending_character=require_ending_character,
            ending_chars=ending_chars,
        )
        if right_reason is None:
            continue

        yield _TriggerOverlap(
            start=start,
            end=end,
            left_reason=left_reason,
            right_reason=right_reason,
        )

_iter_hotstring_occurrence_starts

_iter_hotstring_occurrence_starts(
    *,
    trigger: Hotstring,
    container: Hotstring,
    require_exact_case: bool,
) -> Iterator[int]

Yield semantic occurrence starts under the required case semantics.

The normal Windows path searches the already cached semantic or case-insensitive trigger strings directly. A defensive fallback preserves original semantic indices if a non-Windows str.lower() transformation changes Unicode string length.

Parameters:

Name Type Description Default
trigger Hotstring

Hotstring whose semantic trigger is being located.

required
container Hotstring

Hotstring whose semantic trigger is being searched.

required
require_exact_case bool

Whether both definitions require exact casing.

required

Yields:

Type Description
int

Start index of each occurrence in container.semantic_trigger.

Source code in hotstring\core\conflicts.py
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
def _iter_hotstring_occurrence_starts(
    *,
    trigger: Hotstring,
    container: Hotstring,
    require_exact_case: bool,
) -> Iterator[int]:
    """Yield semantic occurrence starts under the required case semantics.

    The normal Windows path searches the already cached semantic or
    case-insensitive trigger strings directly. A defensive fallback preserves
    original semantic indices if a non-Windows `str.lower()` transformation
    changes Unicode string length.

    Args:
        trigger:
            Hotstring whose semantic trigger is being located.
        container:
            Hotstring whose semantic trigger is being searched.
        require_exact_case:
            Whether both definitions require exact casing.

    Yields:
        Start index of each occurrence in `container.semantic_trigger`.
    """
    if require_exact_case:
        yield from _iter_occurrence_starts(
            trigger=trigger.semantic_trigger,
            container=container.semantic_trigger,
        )
        return

    trigger_key = trigger.case_insensitive_semantic_trigger_key
    container_key = container.case_insensitive_semantic_trigger_key

    if (
        len(trigger_key) == len(trigger.semantic_trigger)
        and len(container_key) == len(container.semantic_trigger)
    ):
        yield from _iter_occurrence_starts(
            trigger=trigger_key,
            container=container_key,
        )
        return

    trigger_length = len(trigger.semantic_trigger)
    for start in range(len(container.semantic_trigger) - trigger_length + 1):
        segment = container.semantic_trigger[start : start + trigger_length]
        if make_case_insensitive_trigger_key(segment) == trigger_key:
            yield start

_iter_occurrence_starts

_iter_occurrence_starts(
    *, trigger: str, container: str
) -> Iterator[int]

Yield every exact string occurrence start, including overlaps.

Repeated str.find() is used instead of a regex lookahead. Advancing the next search by one character, rather than by the matched trigger length, preserves overlapping occurrences such as both ana matches in banana.

Parameters:

Name Type Description Default
trigger str

Exact string to locate.

required
container str

String in which to locate it.

required

Yields:

Type Description
int

Start index of each occurrence.

Source code in hotstring\core\conflicts.py
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
def _iter_occurrence_starts(*, trigger: str, container: str) -> Iterator[int]:
    """Yield every exact string occurrence start, including overlaps.

    Repeated `str.find()` is used instead of a regex lookahead. Advancing the
    next search by one character, rather than by the matched trigger length,
    preserves overlapping occurrences such as both `ana` matches in
    `banana`.

    Args:
        trigger:
            Exact string to locate.
        container:
            String in which to locate it.

    Yields:
        Start index of each occurrence.
    """
    start = container.find(trigger)
    while start != -1:
        yield start
        start = container.find(trigger, start + 1)

_left_boundary_reason

_left_boundary_reason(
    *,
    container: str,
    start: int,
    allow_alphanumeric_predecessor: bool,
) -> str | None

Evaluate the left boundary of one occurrence.

Parameters:

Name Type Description Default
container str

Semantic trigger containing the occurrence.

required
start int

Occurrence start index.

required
allow_alphanumeric_predecessor bool

Whether an alphanumeric predecessor is allowed.

required

Returns:

Type Description
str | None

Explanation of a valid boundary, or None when invalid.

Source code in hotstring\core\conflicts.py
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
def _left_boundary_reason(
    *, container: str, start: int, allow_alphanumeric_predecessor: bool
) -> str | None:
    """Evaluate the left boundary of one occurrence.

    Args:
        container:
            Semantic trigger containing the occurrence.
        start:
            Occurrence start index.
        allow_alphanumeric_predecessor:
            Whether an alphanumeric predecessor is allowed.

    Returns:
        Explanation of a valid boundary, or `None` when invalid.
    """
    if start == 0:
        return "the occurrence starts at the beginning"

    preceding_char = container[start - 1]
    if not preceding_char.isalnum():
        return f"the preceding character {preceding_char!r} is non-alphanumeric"
    if allow_alphanumeric_predecessor:
        return (
            f"the preceding character {preceding_char!r} is alphanumeric, "
            "but the hotstring permits an alphanumeric predecessor"
        )
    return None

_right_boundary_reason

_right_boundary_reason(
    *,
    container: str,
    end: int,
    require_ending_character: bool,
    ending_chars: frozenset[str],
) -> str | None

Evaluate the right boundary of one occurrence.

Parameters:

Name Type Description Default
container str

Semantic trigger containing the occurrence.

required
end int

Exclusive occurrence end index.

required
require_ending_character bool

Whether activation requires an ending character.

required
ending_chars frozenset[str]

Effective ending-character set.

required

Returns:

Type Description
str | None

Explanation of a valid boundary, or None when invalid.

Source code in hotstring\core\conflicts.py
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
def _right_boundary_reason(
    *,
    container: str,
    end: int,
    require_ending_character: bool,
    ending_chars: frozenset[str],
) -> str | None:
    """Evaluate the right boundary of one occurrence.

    Args:
        container:
            Semantic trigger containing the occurrence.
        end:
            Exclusive occurrence end index.
        require_ending_character:
            Whether activation requires an ending character.
        ending_chars:
            Effective ending-character set.

    Returns:
        Explanation of a valid boundary, or `None` when invalid.
    """
    if not require_ending_character:
        return "the hotstring does not require an ending character"
    if end == len(container):
        return (
            "the occurrence reaches the end of the containing trigger, so a "
            "subsequently typed ending character can activate it"
        )

    following_char = container[end]
    if following_char in ending_chars:
        return f"the following character {following_char!r} is an ending character"
    return None