Coverage for sources / ictr / printers.py: 97%
56 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-12 01:33 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-12 01:33 +0000
1# vim: set filetype=python fileencoding=utf-8:
2# -*- coding: utf-8 -*-
4#============================================================================#
5# #
6# Licensed under the Apache License, Version 2.0 (the "License"); #
7# you may not use this file except in compliance with the License. #
8# You may obtain a copy of the License at #
9# #
10# http://www.apache.org/licenses/LICENSE-2.0 #
11# #
12# Unless required by applicable law or agreed to in writing, software #
13# distributed under the License is distributed on an "AS IS" BASIS, #
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
15# See the License for the specific language governing permissions and #
16# limitations under the License. #
17# #
18#============================================================================#
21''' Printers, printer factories, and auxiliary functions and types. '''
24import colorama as _colorama
26from . import __
27from . import exceptions as _exceptions
28from . import flavors as _flavors
29from . import records as _records
32_validate_arguments = (
33 __.validate_arguments(
34 globalvars = globals( ),
35 errorclass = _exceptions.ArgumentClassInvalidity ) )
38ColumnsMaxCalculator: __.typx.TypeAlias = __.typx.Annotated[
39 __.typx.Union[
40 __.typx.Optional[ int ],
41 __.cabc.Callable[ [ ], __.typx.Optional[ int ] ],
42 ],
43 __.typx.Doc(
44 ''' Available line length of target character screen.
46 * May be an integer.
47 * May be ``None`` if indeterminable or irrelevant.
48 * May be a callable which takes no arguments and returns ``None``
49 or an integer. This support terminal resizing, for example.
50 ''' ),
51]
54class TextualizationControl( __.immut.DataclassObject ):
55 ''' Contextual data for compositor and introducer factories. '''
57 charset: __.typx.Annotated[
58 __.typx.Optional[ str ],
59 __.typx.Doc(
60 ''' Character set encoding of target.
62 May be ``None`` if indeterminable or irrelevant. ''' ),
63 ] = None
64 colorize: __.typx.Annotated[
65 bool, __.typx.Doc( ''' Colorize textualization? ''' )
66 ] = False
67 columns_max_calculator: ColumnsMaxCalculator = None
69 @property
70 def columns_max( self ) -> __.typx.Optional[ int ]:
71 ''' Available line length (maximum columns) of target.
73 May be ``None`` if indeterminable or irrelevant.
74 '''
75 calculator = self.columns_max_calculator
76 return calculator( ) if callable( calculator ) else calculator
79class Printer(
80 __.immut.DataclassProtocol, __.typx.Protocol,
81 decorators = ( __.typx.runtime_checkable, ),
82):
83 ''' Abstract base class for printers. '''
85 @__.abc.abstractmethod
86 def __call__( self, record: str | _records.Record ) -> None:
87 ''' Prints record to destination. '''
88 raise NotImplementedError
90 @__.abc.abstractmethod
91 def provide_textualization_control(
92 self
93 ) -> __.typx.Optional[ TextualizationControl ]:
94 ''' Provides control object for textualization, if capable. '''
95 raise NotImplementedError
97 # TODO: print (same as __call__)
98 # TODO: print_async
101Printers: __.typx.TypeAlias = __.cabc.Sequence[ Printer ]
102PrinterFactory: __.typx.TypeAlias = (
103 __.cabc.Callable[ [ str, _flavors.Flavor ], Printer ] )
104PrinterFactoryUnion: __.typx.TypeAlias = __.io.TextIOBase | PrinterFactory
105PrinterFactoriesUnion: __.typx.TypeAlias = (
106 __.cabc.Sequence[ PrinterFactoryUnion ] )
109@_validate_arguments
110def count_columns_visual( text: str ) -> int:
111 # Note: If CSI ED ("Erase on Display") or EL ("Erase in Line") sequences
112 # are used within the text, then the count will not be accurate.
113 text_no_ansi = remove_ansi_c1_sequences( text )
114 return __.wcwidth.wcswidth( text_no_ansi )
117@_validate_arguments
118def remove_ansi_c1_sequences( text: str ) -> str:
119 # https://stackoverflow.com/a/14693789/14833542
120 regex = __.re.compile( r'''\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])''' )
121 return regex.sub( '', text )
124@_validate_arguments
125def produce_columns_max_calculator(
126 target: __.io.TextIOBase
127) -> ColumnsMaxCalculator:
128 fileno_revealer = getattr( target, 'fileno', None )
129 if fileno_revealer is None: return None
130 try: fileno = fileno_revealer( )
131 except ( IOError, OSError, __.io.UnsupportedOperation ): return None
132 if not __.os.isatty( fileno ): return None
134 def calculate( ) -> __.typx.Optional[ int ]:
135 try: size = __.shutil.get_terminal_size( fileno )
136 except Exception: return None
137 return size.columns
139 return calculate
142@_validate_arguments
143def produce_printer_factory_default(
144 target: __.io.TextIOBase,
145 force_color: bool = False,
146) -> PrinterFactory:
147 ''' Produces default printer factory associated with a stream.
149 Can optionally force ANSI SGR sequences (terminal color attributes,
150 etc...) on target stream.
151 '''
152 def produce_printer( address: str, flavor: _flavors.Flavor ) -> Printer:
153 from .standard import Printer
154 match __.sys.platform:
155 case 'win32':
156 winansi = _colorama.AnsiToWin32( target ) # pyright: ignore
157 target_ = ( # pragma: no cover
158 winansi.stream if winansi.convert else target )
159 case _: target_ = target
160 return Printer(
161 target = target_, force_color = force_color ) # pyright: ignore
163 return produce_printer
166# def truncate_visual( text: str, columns_max: int ) -> str:
167# lsize = 0
168# for i, c in enumerate( text ):
169# csize = __.wcwidth.wcwidth( c )
170# csize = max( 0, csize ) # control or combining character
171# if lsize + csize > columns_max:
172# # TODO? Add ellipsis.
173# return text[ : i ]
174# lsize += csize
175# return text