chromatic-python 0.2.0__tar.gz → 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {chromatic_python-0.2.0/chromatic_python.egg-info → chromatic_python-0.2.1}/PKG-INFO +3 -3
  2. chromatic_python-0.2.1/chromatic/_version.py +21 -0
  3. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic/color/core.py +238 -193
  4. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic/color/core.pyi +50 -46
  5. chromatic_python-0.2.1/chromatic/color/iterators.py +166 -0
  6. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic/color/palette.py +34 -161
  7. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic/color/palette.pyi +15 -39
  8. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic/demo.py +2 -2
  9. {chromatic_python-0.2.0 → chromatic_python-0.2.1/chromatic_python.egg-info}/PKG-INFO +3 -3
  10. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic_python.egg-info/SOURCES.txt +1 -0
  11. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/pyproject.toml +10 -8
  12. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/tests/test_color_str.py +49 -26
  13. chromatic_python-0.2.0/chromatic/_version.py +0 -16
  14. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/.gitattributes +0 -0
  15. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/.gitignore +0 -0
  16. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/LICENSE +0 -0
  17. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/README.md +0 -0
  18. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic/__init__.py +0 -0
  19. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic/_typing.py +0 -0
  20. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic/ascii/__init__.py +0 -0
  21. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic/ascii/_array.py +0 -0
  22. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic/ascii/_curses.py +0 -0
  23. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic/ascii/_glyph_proc.py +0 -0
  24. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic/color/__init__.py +0 -0
  25. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic/color/colorconv.py +0 -0
  26. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic/data/__init__.py +0 -0
  27. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic/data/__init__.pyi +0 -0
  28. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic/data/fonts/IBM_VGA_437_8x16.ttf +0 -0
  29. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic/data/fonts/consolas.ttf +0 -0
  30. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic/data/images/butterfly.jpg +0 -0
  31. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic/data/images/escher.png +0 -0
  32. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic/data/images/goblin_virus.png +0 -0
  33. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic/data/images/hotdog.jpg +0 -0
  34. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic_python.egg-info/dependency_links.txt +0 -0
  35. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic_python.egg-info/requires.txt +0 -0
  36. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/chromatic_python.egg-info/top_level.txt +0 -0
  37. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/logo/logo.ANS +0 -0
  38. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/logo/logo.PNG +0 -0
  39. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/setup.cfg +0 -0
  40. {chromatic_python-0.2.0 → chromatic_python-0.2.1}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: chromatic-python
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: ANSI art image processing and colored terminal text
5
5
  Author: crypt0lith
6
6
  License: MIT License
@@ -27,7 +27,6 @@ License: MIT License
27
27
  Project-URL: Homepage, https://github.com/crypt0lith/chromatic
28
28
  Keywords: ansi,ascii,art,font,image,terminal,parser
29
29
  Classifier: Programming Language :: Python :: 3.12
30
- Classifier: License :: OSI Approved :: MIT License
31
30
  Classifier: Operating System :: OS Independent
32
31
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
33
32
  Classifier: Typing :: Typed
@@ -42,6 +41,7 @@ Requires-Dist: pillow~=10.4.0
42
41
  Requires-Dist: scikit-image~=0.25.0rc1
43
42
  Requires-Dist: scikit-learn~=1.5.2
44
43
  Requires-Dist: scipy~=1.14.1
44
+ Dynamic: license-file
45
45
 
46
46
  ![image](/logo/logo.PNG)
47
47
 
@@ -0,0 +1,21 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
6
+ TYPE_CHECKING = False
7
+ if TYPE_CHECKING:
8
+ from typing import Tuple
9
+ from typing import Union
10
+
11
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
12
+ else:
13
+ VERSION_TUPLE = object
14
+
15
+ version: str
16
+ __version__: str
17
+ __version_tuple__: VERSION_TUPLE
18
+ version_tuple: VERSION_TUPLE
19
+
20
+ __version__ = version = '0.2.1'
21
+ __version_tuple__ = version_tuple = (0, 2, 1)
@@ -2,24 +2,23 @@ __all__ = [
2
2
  'CSI',
3
3
  'Color',
4
4
  'ColorStr',
5
+ 'SGR_RESET',
5
6
  'SgrParameter',
6
7
  'SgrSequence',
7
8
  'ansicolor24Bit',
8
9
  'ansicolor4Bit',
9
10
  'ansicolor8Bit',
11
+ 'color_chain',
10
12
  'colorbytes',
11
13
  'get_ansi_type',
12
- 'hsl_gradient',
13
14
  'randcolor',
14
- 'rgb2ansi_color_esc',
15
- 'rgb_luma_transform',
16
- 'SGR_RESET',
15
+ 'rgb2ansi_escape',
17
16
  ]
18
17
 
19
- import math
20
18
  import operator as op
21
19
  import os
22
20
  import random
21
+ import sys
23
22
  from collections import Counter
24
23
  from collections.abc import Buffer
25
24
  from copy import deepcopy
@@ -53,7 +52,6 @@ from .colorconv import *
53
52
  from .._typing import (
54
53
  AnsiColorAlias,
55
54
  ColorDictKeys,
56
- Float3Tuple,
57
55
  Int3Tuple,
58
56
  RGBVectorLike,
59
57
  is_matching_typed_dict,
@@ -218,7 +216,7 @@ class colorbytes(bytes):
218
216
 
219
217
  fmt: AnsiColorType = cls if cls is not colorbytes else DEFAULT_ANSI
220
218
  try:
221
- inst = bytes.__new__(fmt, rgb2ansi_color_esc(fmt, *rgb.copy().popitem()))
219
+ inst = bytes.__new__(fmt, rgb2ansi_escape(fmt, *rgb.copy().popitem()))
222
220
  except TypeError:
223
221
  print(vars())
224
222
  raise
@@ -232,7 +230,7 @@ class colorbytes(bytes):
232
230
  ) from None
233
231
  if (is_subtype := cls is not colorbytes) and type(__ansi) is cls:
234
232
  return cast(AnsiColorFormat, __ansi)
235
- match __ansi.removeprefix(CSI).removesuffix(b'm').split(b';'):
233
+ match _unwrap_ansi_escape(__ansi):
236
234
  case [color]:
237
235
  typ = ansicolor4Bit
238
236
  k, rgb = _ANSI16C_I2KV[int(color)]
@@ -247,7 +245,7 @@ class colorbytes(bytes):
247
245
  case _:
248
246
  raise ValueError
249
247
  if typ is not cls:
250
- __ansi = rgb2ansi_color_esc(
248
+ __ansi = rgb2ansi_escape(
251
249
  cls if is_subtype else typ, mode=cast(ColorDictKeys, k), rgb=rgb
252
250
  )
253
251
  inst = bytes.__new__(typ, __ansi)
@@ -352,7 +350,7 @@ class ansicolor24Bit(colorbytes):
352
350
 
353
351
 
354
352
  _SUPPORTS_256 = frozenset(
355
- {
353
+ [
356
354
  'ANSICON',
357
355
  'COLORTERM',
358
356
  'ConEmuANSI',
@@ -361,7 +359,7 @@ _SUPPORTS_256 = frozenset(
361
359
  'TERMINAL_EMULATOR',
362
360
  'TERM_PROGRAM',
363
361
  'WT_SESSION',
364
- }
362
+ ]
365
363
  )
366
364
 
367
365
 
@@ -385,11 +383,8 @@ def is_vt_proc_enabled():
385
383
  return True
386
384
 
387
385
 
388
- def get_term_ansi_default():
389
- return ansicolor8Bit if is_vt_proc_enabled() else ansicolor4Bit
390
-
386
+ DEFAULT_ANSI = ansicolor8Bit if is_vt_proc_enabled() else ansicolor4Bit
391
387
 
392
- DEFAULT_ANSI = get_term_ansi_default()
393
388
  _ANSI_COLOR_TYPES = frozenset(colorbytes.__subclasses__())
394
389
 
395
390
 
@@ -401,6 +396,75 @@ def _is_ansi_type(typ: type):
401
396
  return False
402
397
 
403
398
 
399
+ @lru_cache
400
+ def _sgr_re_pattern():
401
+ import re
402
+
403
+ def group(*choices: *tuple[str, ...]):
404
+ return '(?:' + '|'.join(choices) + ')'
405
+
406
+ def params(*choices: *tuple[str, ...]):
407
+ return ';'.join(choices)
408
+
409
+ u8_no_leading_zeros = group(r'25[0-5]', r'2[0-4]\d', r'1\d\d', r'[1-9]\d', r'\d')
410
+ pat = group(
411
+ group(
412
+ params(
413
+ '[3-4]8',
414
+ group(
415
+ params('2', *(u8_no_leading_zeros for _ in range(3))),
416
+ params('5', u8_no_leading_zeros),
417
+ ),
418
+ )
419
+ ),
420
+ group('10', '9', '4', '3') + '[1-7]',
421
+ group('10', '9', '[1-6]') + '0',
422
+ '6[1-3]',
423
+ '5[2-5]',
424
+ '[2-4]9',
425
+ '28',
426
+ '2[0-6]',
427
+ r'1\d',
428
+ r'\d',
429
+ )
430
+ return re.compile(rf'\x1b\[{pat[:-1]}(?:;{pat})*)?m')
431
+
432
+
433
+ def _split_ansi_escape(__s: str) -> Optional[list[tuple['SgrSequence', str]]]:
434
+ out = []
435
+ i = 0
436
+ for m in _sgr_re_pattern().finditer(__s):
437
+ text = __s[i : (j := m.start())]
438
+ if i != j:
439
+ out.append(text)
440
+ ansi = _unwrap_ansi_escape(__s[j : (i := m.end())].encode())
441
+ if any(ansi):
442
+ out.append(SgrSequence(map(int, ansi)))
443
+ if i + 1 < len(__s):
444
+ out.append(__s[i:])
445
+ if not any(isinstance(x, SgrSequence) for x in out):
446
+ return
447
+ n = len(out)
448
+ tmp = []
449
+ for idx, x in enumerate(out):
450
+ if idx + 1 < n and type(x) is type(out[idx + 1]):
451
+ out[idx + 1] = x + out[idx + 1]
452
+ else:
453
+ tmp.append(x)
454
+ out = tmp
455
+ if out and len(out) % 2 != 0:
456
+ out.append({SgrSequence: str, str: SgrSequence}[type(out[-1])]())
457
+ return [(a, b) if isinstance(a, SgrSequence) else (b, a) for a, b in zip(out[::2], out[1::2])]
458
+
459
+
460
+ def _unwrap_ansi_escape(__b: bytes):
461
+ return __b.removeprefix(CSI).removesuffix(b'm').split(b';')
462
+
463
+
464
+ def _concat_ansi_escape(__it: Iterable[bytes]):
465
+ return b'\x1b[%sm' % b';'.join(__it)
466
+
467
+
404
468
  AnsiColorFormat: TypeAlias = ansicolor4Bit | ansicolor8Bit | ansicolor24Bit
405
469
  AnsiColorType: TypeAlias = type[AnsiColorFormat]
406
470
  AnsiColorParam: TypeAlias = AnsiColorAlias | AnsiColorType
@@ -437,7 +501,7 @@ def get_ansi_type(typ):
437
501
  ) from None
438
502
 
439
503
 
440
- def rgb2ansi_color_esc(ret_format, mode, rgb):
504
+ def rgb2ansi_escape(ret_format, mode, rgb):
441
505
  ret_format = get_ansi_type(ret_format)
442
506
  assert len(rgb) == 3, 'length of RGB value is not 3'
443
507
  try:
@@ -522,8 +586,7 @@ class SgrParamWrapper:
522
586
  return hash(self._value_)
523
587
 
524
588
  def __eq__(self, other):
525
- cls, other_cls = map(type, (self, other))
526
- if cls is other_cls or issubclass(other_cls, bytes):
589
+ if type(self) is type(other) or isinstance(other, bytes):
527
590
  return hash(self) == hash(other)
528
591
  return NotImplemented
529
592
 
@@ -534,10 +597,8 @@ class SgrParamWrapper:
534
597
  return f"{type(self).__name__}({self._value_})"
535
598
 
536
599
  def is_same_kind(self, other):
537
- if self == other:
538
- return True
539
600
  try:
540
- return next(_iter_sgr(other)) == self._value_
601
+ return self == other or self._value_ == next(_iter_sgr(other))
541
602
  except (TypeError, StopIteration, RuntimeError):
542
603
  return False
543
604
 
@@ -548,9 +609,6 @@ class SgrParamWrapper:
548
609
  return isinstance(self._value_, colorbytes)
549
610
 
550
611
 
551
- # SgrParamWrapper.__name__ = SgrParameter.__name__.lower()
552
-
553
-
554
612
  @lru_cache
555
613
  def _get_sgr_bitmask[_T: (bytes, bytearray, Buffer)](__x: _T) -> list[int]:
556
614
  """Return a list of integers from a bytestring of ANSI SGR parameters.
@@ -575,7 +633,9 @@ def _get_sgr_bitmask[_T: (bytes, bytearray, Buffer)](__x: _T) -> list[int]:
575
633
  return allocated
576
634
 
577
635
 
578
- def _iter_normalized_sgr(__iter) -> Iterator[AnsiColorFormat | int]:
636
+ def _iter_normalized_sgr[
637
+ _T: (Buffer, SgrParamWrapper, int)
638
+ ](__iter: Buffer | Iterable[_T]) -> Iterator[AnsiColorFormat | int]:
579
639
  if isinstance(__iter, Buffer):
580
640
  yield from _get_sgr_bitmask(__iter)
581
641
  else:
@@ -635,13 +695,13 @@ def _gen_colorbytes(__iter: Iterable[int]) -> Iterator[bytes | AnsiColorFormat]:
635
695
  break
636
696
 
637
697
 
638
- def _iter_sgr(__x):
698
+ def _iter_sgr[_T: (Buffer, int)](__x: _T | Iterable[_T]):
639
699
  if isinstance(__x, int):
640
700
  __x = [__x]
641
701
  return _gen_colorbytes(_iter_normalized_sgr(__x))
642
702
 
643
703
 
644
- class SgrSequence:
704
+ class SgrSequence(Sequence[SgrParamWrapper]):
645
705
 
646
706
  def append(self, __value):
647
707
  if __value not in _SGR_PARAM_VALUES:
@@ -677,9 +737,11 @@ class SgrSequence:
677
737
  if self.is_color():
678
738
  return next((v for v in self if v.is_color() and __key in v._value_.rgb_dict), None)
679
739
 
680
- def index(self, value):
740
+ def index(self, value, start: SupportsIndex = 0, stop: SupportsIndex = sys.maxsize):
681
741
  try:
682
- return next(i for i, p in enumerate(self) if p.is_same_kind(value))
742
+ return op.index(start) + next(
743
+ i for i, p in enumerate(self[start:stop]) if p.is_same_kind(value)
744
+ )
683
745
  except StopIteration:
684
746
  raise ValueError(f"{value!r} not in sequence") from None
685
747
 
@@ -722,7 +784,7 @@ class SgrSequence:
722
784
 
723
785
  def __add__(self, other):
724
786
  if type(self) is type(other):
725
- return SgrSequence([*self, *other])
787
+ return SgrSequence(x for xs in (self, other) for x in xs)
726
788
  if isinstance(other, str):
727
789
  return str(self) + other
728
790
 
@@ -738,7 +800,7 @@ class SgrSequence:
738
800
  def __bytes__(self):
739
801
  if self._bytes_ is None:
740
802
  if self._sgr_params_:
741
- self._bytes_ = b'\x1b[%sm' % b';'.join(self.values())
803
+ self._bytes_ = _concat_ansi_escape(self.values())
742
804
  else:
743
805
  self._bytes_ = bytes()
744
806
  return self._bytes_
@@ -795,10 +857,10 @@ class SgrSequence:
795
857
  return SgrSequence(self._sgr_params_ + other._sgr_params_)
796
858
 
797
859
  def __init__(self, __iter=None, *, ansi_type=None) -> None:
798
- cls = type(self)
799
- if type(__iter) is cls:
860
+ if type(self) is type(__iter):
861
+ __iter: SgrSequence
800
862
  other = __iter.__copy__()
801
- for attr in cls.__slots__:
863
+ for attr in type(self).__slots__:
802
864
  setattr(self, attr, getattr(other, attr))
803
865
  return
804
866
 
@@ -866,14 +928,17 @@ class SgrSequence:
866
928
  self._has_bright_colors_ = False
867
929
  self._sgr_params_ = [self._sgr_params_.pop()]
868
930
  self._rgb_dict_ = {}
869
- self._bytes_ = b'\x1b[%sm' % b';'.join(map(bytes, self._sgr_params_))
931
+ self._bytes_ = _concat_ansi_escape(map(bytes, self._sgr_params_))
870
932
 
871
933
  def __iter__(self):
872
934
  return iter(self._sgr_params_)
873
935
 
936
+ def __len__(self):
937
+ return len(self._sgr_params_)
938
+
874
939
  def __radd__(self, other):
875
940
  if type(self) is type(other):
876
- return SgrSequence([*other, *self])
941
+ return SgrSequence(x for xs in (other, self) for x in xs)
877
942
  if isinstance(other, str):
878
943
  return other + str(self)
879
944
  raise TypeError(
@@ -916,13 +981,13 @@ class SgrSequence:
916
981
  def rgb_dict(self, __value: tuple[AnsiColorType, dict[ColorDictKeys, Optional[Color]]]) -> None:
917
982
  ansi_type, color_dict = __value
918
983
  for k, v in color_dict.items():
984
+ if self._rgb_dict_.get(k):
985
+ try:
986
+ self.pop(self.index(self.get_color(k)))
987
+ except ValueError as e:
988
+ e.add_note(repr(self))
989
+ raise e
919
990
  if v is not None:
920
- if self._rgb_dict_.get(k):
921
- try:
922
- self.pop(self.index(self.get_color(k)))
923
- except ValueError as e:
924
- e.add_note(repr(self))
925
- raise e
926
991
  color_bytes = ansi_type.from_rgb({k: v})
927
992
  self._rgb_dict_ |= color_bytes._rgb_dict_
928
993
  self._sgr_params_.append(SgrParamWrapper(color_bytes))
@@ -995,7 +1060,7 @@ def _solve_color_spec[
995
1060
  e = ValueError('too many arguments' if len(out) >= 2 else 'args contain non-RGB values')
996
1061
  context = ('invalid color spec', str(e))
997
1062
  raise ValueError(': '.join(filter(None, context))) from None
998
- return SgrSequence([ansi_type.from_rgb({k: v}) for k, v in out.items()])
1063
+ return SgrSequence(ansi_type.from_rgb({k: v}) for k, v in out.items())
999
1064
 
1000
1065
 
1001
1066
  def _get_color_str_vars(
@@ -1534,159 +1599,139 @@ class ColorStr(str):
1534
1599
  return {k: v.rgb for k, v in self._color_dict_.items()}
1535
1600
 
1536
1601
 
1537
- def hsl_gradient(
1538
- start: Int3Tuple | Float3Tuple,
1539
- stop: Int3Tuple | Float3Tuple,
1540
- step: SupportsIndex,
1541
- num: SupportsIndex = None,
1542
- ncycles: int | float = float('inf'),
1543
- replace_idx: tuple[SupportsIndex | Iterable[SupportsIndex], Iterator[Color]] = None,
1544
- dtype: type[Color] | Callable[[Int3Tuple], int] = Color,
1545
- ):
1546
- replace_idx, rgb_iter = _resolve_replacement_indices(replace_idx)
1547
- while abs(float(step)) < 1:
1548
- step *= 10
1549
- color_vec = _init_gradient_color_vec(num, start, step, stop)
1550
- color_iter = iter(color_vec)
1551
- type_map: dict[type[Color | int], ...] = {Color: lambda x: x.rgb, int: lambda x: hex2rgb(x)}
1552
- get_rgb_iter_idx: Callable[[Color | int, SupportsIndex], int] = lambda x, ix: rgb2hsl(
1553
- type_map[type(x)](x)
1554
- )[ix]
1555
- next_rgb_iter = None
1556
- prev_output = None
1557
- while ncycles > 0:
1558
- try:
1559
- cur_iter = next(color_iter)
1560
- if cur_iter != prev_output:
1561
- for idx in replace_idx:
1562
- try:
1563
- next_rgb_iter = next(rgb_iter)
1564
- cur_iter = list(cur_iter)
1565
- cur_iter[idx] = get_rgb_iter_idx(next_rgb_iter, idx)
1566
- except StopIteration:
1567
- raise GeneratorExit
1568
- except KeyError:
1569
- raise TypeError(
1570
- f"Expected iterator to return "
1571
- f"{repr(Color.__qualname__)} or {repr(int.__qualname__)}, "
1572
- f"got {repr(type(next_rgb_iter).__qualname__)} instead"
1573
- ) from None
1574
- output = hsl2rgb(cast(Float3Tuple, cur_iter))
1575
- if callable(dtype):
1576
- output = dtype(output)
1577
- yield output
1578
- prev_output = cur_iter
1579
- except StopIteration:
1580
- ncycles -= 1
1581
- color_vec.reverse()
1582
- color_iter = iter(color_vec)
1583
- except GeneratorExit:
1584
- break
1602
+ def _color_str_to_mask(cs: ColorStr) -> tuple[SgrSequence, str]:
1603
+ return cs._sgr_, cs.base_str
1585
1604
 
1586
1605
 
1587
- def _resolve_replacement_indices(
1588
- replace_idx: tuple[SupportsIndex | Sequence[SupportsIndex], Iterator[Color]] = None,
1589
- ):
1590
- if replace_idx is not None:
1591
- replace_idx, rgb_iter = replace_idx
1592
- if not isinstance(rgb_iter, Iterator):
1593
- raise TypeError(
1594
- f"Expected 'replace_idx[1]' to be an iterator, got {type(rgb_iter).__name__} "
1595
- f"instead"
1606
+ class color_chain:
1607
+
1608
+ def extend(self, other):
1609
+ if isinstance(other, color_chain):
1610
+ self._masks_.extend(other._masks_[:])
1611
+ elif isinstance(other, ColorStr):
1612
+ self._masks_.append(_color_str_to_mask(other))
1613
+ elif isinstance(other, str):
1614
+ self._masks_.append((SgrSequence(), other))
1615
+ else: #
1616
+ raise TypeError
1617
+
1618
+ @classmethod
1619
+ def from_masks(cls, masks, ansi_type=None):
1620
+ if isinstance(masks, Sequence) and all(
1621
+ isinstance(x, tuple)
1622
+ and len(x) == 2
1623
+ and isinstance(x[0], SgrSequence)
1624
+ and isinstance(x[1], str)
1625
+ for x in masks
1626
+ ):
1627
+ return cls._from_masks_unchecked(masks, get_ansi_type(ansi_type or DEFAULT_ANSI))
1628
+ raise TypeError
1629
+
1630
+ @classmethod
1631
+ def _from_masks_unchecked(cls, masks, ansi_type):
1632
+ inst = object.__new__(cls)
1633
+ prev_fg = prev_bg = None
1634
+ inst._masks_ = []
1635
+ for sgr, s in masks:
1636
+ if prev_fg is not None and prev_fg == sgr.fg:
1637
+ sgr.rgb_dict = (ansi_type, {'fg': None})
1638
+ if prev_bg is not None and prev_bg == sgr.bg:
1639
+ sgr.rgb_dict = (ansi_type, {'bg': None})
1640
+ inst._masks_.append((sgr, s))
1641
+ prev_fg, prev_bg = sgr.fg, sgr.bg
1642
+ inst._ansi_type_ = ansi_type
1643
+ return inst
1644
+
1645
+ def __add__(self, other):
1646
+ if isinstance(other, (color_chain, ColorStr)):
1647
+ other_masks: tuple[tuple[SgrSequence, str], ...] = (
1648
+ other.masks if isinstance(other, color_chain) else (_color_str_to_mask(other),)
1596
1649
  )
1597
- if not isinstance(replace_idx, Sequence):
1598
- replace_idx = {replace_idx}
1650
+ if self._masks_ and other_masks:
1651
+ match [
1652
+ (self._masks_[-1][0].fg, self._masks_[-1][0].bg),
1653
+ (other_masks[0][0].fg, other_masks[0][0].bg),
1654
+ ]:
1655
+ case [(None, tuple() as bg), (tuple() as fg, None)] | (
1656
+ [(tuple() as fg, None), (None, tuple() as bg)]
1657
+ ):
1658
+ return self._from_masks_unchecked(
1659
+ [
1660
+ *self._masks_[:-1],
1661
+ (
1662
+ color_chain(fg=fg, bg=bg)._masks_.pop()[0],
1663
+ self._masks_[-1][1] + other_masks[0][1],
1664
+ ),
1665
+ *other_masks[1:],
1666
+ ],
1667
+ ansi_type=self._ansi_type_,
1668
+ )
1669
+ case _:
1670
+ return self._from_masks_unchecked(
1671
+ self.masks + other_masks, ansi_type=self._ansi_type_
1672
+ )
1673
+ elif isinstance(other, str):
1674
+ if len(self._masks_) > 0:
1675
+ return self._from_masks_unchecked(
1676
+ [*self.masks[:-1], (self._masks_[-1][0], self._masks_[-1][1] + other)],
1677
+ ansi_type=self._ansi_type_,
1678
+ )
1679
+ return self._from_masks_unchecked(
1680
+ [*self.masks, (SgrSequence(), other)], ansi_type=self._ansi_type_
1681
+ )
1682
+ return NotImplemented
1683
+
1684
+ def __call__(self, __obj=None):
1685
+ return "%s%s" % (self, __obj)
1686
+
1687
+ def __iadd__(self, other):
1688
+ self.extend(other)
1689
+ return self
1690
+
1691
+ def __init__(self, **kwargs):
1692
+ self._ansi_type_ = get_ansi_type(kwargs.get('ansi_type', DEFAULT_ANSI))
1693
+ if kwargs.get('sgr_params') is not None:
1694
+ sgr = SgrSequence(kwargs.get('sgr_params'))
1599
1695
  else:
1600
- replace_idx = set(replace_idx)
1601
- valid_idx_range = range(3)
1602
- if any(idx_diff := replace_idx.difference(valid_idx_range)):
1603
- raise ValueError(f"Invalid replacement indices: {idx_diff}")
1604
- if replace_idx == set(valid_idx_range):
1605
- raise ValueError(f"All 3 indexes selected for replacement: {replace_idx=}")
1606
- else:
1607
- rgb_iter = None
1608
- replace_idx = []
1609
- return replace_idx, rgb_iter
1610
-
1611
-
1612
- def _init_gradient_color_vec(
1613
- num: SupportsIndex,
1614
- start: Int3Tuple | Float3Tuple,
1615
- step: SupportsIndex,
1616
- stop: Int3Tuple | Float3Tuple,
1617
- ):
1618
- def convert_bounds(rgb: Int3Tuple):
1619
- if all(0 <= n <= 255 for n in rgb):
1620
- return rgb2hsl(rgb)
1621
- raise ValueError
1622
-
1623
- start, stop = tuple(map(convert_bounds, (start, stop)))
1624
- start_h, start_s, start_l = start
1625
- stop_h, stop_s, stop_l = stop
1626
- if num:
1627
- num_samples = num
1628
- else:
1629
- abs_h = abs(stop_h - start_h)
1630
- h_diff = min(abs_h, 360 - abs_h)
1631
- dist = math.sqrt(h_diff**2 + (stop_s - start_s) ** 2 + (stop_l - start_l) ** 2)
1632
- num_samples = max(int(dist / float(step)), 1)
1633
- color_vec = [np.linspace(*bounds, num=num_samples, dtype=float) for bounds in zip(start, stop)]
1634
- color_vec = list(zip(*color_vec))
1635
- return color_vec
1636
-
1637
-
1638
- def rgb_luma_transform(
1639
- rgb: Int3Tuple,
1640
- start: SupportsIndex = None,
1641
- num: SupportsIndex = 50,
1642
- step: SupportsIndex = 1,
1643
- cycle: bool | Literal['wave'] = False,
1644
- ncycles: int | float = float('inf'),
1645
- gradient: Int3Tuple = None,
1646
- dtype: type[Color] = None,
1647
- ) -> Iterator[Int3Tuple | int | Color]:
1648
- if dtype is None:
1649
- ret_type = tuple
1650
- elif issubclass(dtype, int):
1651
- ret_type = lambda x: dtype(rgb2hex(x))
1652
- is_cycle = bool(cycle is not False)
1653
- is_oscillator = cycle == 'wave'
1654
- if is_oscillator:
1655
- ncycles *= 2
1656
- h, s, luma = rgb2hsl(rgb)
1657
- luma_linspace = [*np.linspace(start=0, stop=1, num=num)][::step]
1658
- if start:
1659
- start = min(max(float(start), 0), 1)
1660
- luma = min(luma_linspace, key=lambda x: abs(x - start))
1661
- start_idx = luma_linspace.index(luma)
1662
- remaining_indices = luma_linspace[start_idx:]
1663
- luma_iter = iter(remaining_indices)
1664
- else:
1665
- luma_iter = iter(luma_linspace)
1696
+ sgr = SgrSequence()
1697
+ v: Int3Tuple | Color | int
1698
+ for k in kwargs.keys() & {'fg', 'bg'}:
1699
+ if (v := kwargs[k]) is None:
1700
+ continue
1701
+ elif isinstance(v, int):
1702
+ v = hex2rgb(v)
1703
+ sgr += SgrSequence(self._ansi_type_.from_rgb({k: v}))
1704
+ self._masks_ = [(sgr, '')]
1666
1705
 
1667
- def _generator():
1668
- nonlocal luma_iter, ncycles
1669
- if step == 0:
1670
- yield rgb
1671
- return
1672
- prev_output = None
1673
- while ncycles > 0:
1674
- try:
1675
- output = hsl2rgb((h, s, next(luma_iter)))
1676
- if output != prev_output:
1677
- yield ret_type(output)
1678
- prev_output = output
1679
- except StopIteration as STOP_IT:
1680
- if not is_cycle:
1681
- raise STOP_IT
1682
- ncycles -= 1
1683
- if is_oscillator:
1684
- luma_linspace.reverse()
1685
- luma_iter = iter(luma_linspace)
1686
-
1687
- if gradient is not None:
1688
- _gradient = hsl_gradient(
1689
- start=rgb, stop=gradient, step=step, num=num, replace_idx=(2, _generator())
1706
+ def __radd__(self, other):
1707
+ if isinstance(other, ColorStr):
1708
+ return color_chain._from_masks_unchecked(
1709
+ (_color_str_to_mask(other),) + self.masks, ansi_type=other.ansi_format
1710
+ )
1711
+ elif isinstance(other, str):
1712
+ if (parsed := _split_ansi_escape(other)) is not None:
1713
+ return color_chain._from_masks_unchecked(
1714
+ parsed + self._masks_[:], ansi_type=self._ansi_type_
1715
+ )
1716
+ else:
1717
+ return color_chain._from_masks_unchecked(
1718
+ [(SgrSequence(), other), *self.masks], ansi_type=self._ansi_type_
1719
+ )
1720
+ return NotImplemented
1721
+
1722
+ def __repr__(self):
1723
+ return "{.__name__}([{!s}], ansi_type={.__name__!r})".format(
1724
+ type(self),
1725
+ ', '.join('(%s, %r)' % (bytes(sgr), s) for sgr, s in self._masks_),
1726
+ self._ansi_type_,
1690
1727
  )
1691
- return iter(_gradient)
1692
- return iter(_generator())
1728
+
1729
+ def __str__(self):
1730
+ return ''.join(
1731
+ str(ColorStr(base_str, color_spec=sgr, ansi_type=self._ansi_type_, no_reset=True))
1732
+ for sgr, base_str in self.masks
1733
+ )
1734
+
1735
+ @property
1736
+ def masks(self):
1737
+ return tuple(self._masks_)