Coverage for sources/ictr/printers.py: 97%
57 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-26 12:43 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-09-26 12:43 +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
79@__.typx.runtime_checkable
80class Printer(
81 __.immut.DataclassProtocol, __.typx.Protocol,
82 class_mutables = __.PROTOCOL_RTC_MUTABLES,
83):
84 ''' Abstract base class for printers. '''
86 @__.abc.abstractmethod
87 def __call__( self, record: str | _records.Record ) -> None:
88 ''' Prints record to destination. '''
89 raise NotImplementedError
91 @__.abc.abstractmethod
92 def provide_textualization_control(
93 self
94 ) -> __.typx.Optional[ TextualizationControl ]:
95 ''' Provides control object for textualization, if capable. '''
96 raise NotImplementedError
98 # TODO: print (same as __call__)
99 # TODO: print_async
102Printers: __.typx.TypeAlias = __.cabc.Sequence[ Printer ]
103PrinterFactory: __.typx.TypeAlias = (
104 __.cabc.Callable[ [ str, _flavors.Flavor ], Printer ] )
105PrinterFactoryUnion: __.typx.TypeAlias = __.typx.TextIO | PrinterFactory
106PrinterFactoriesUnion: __.typx.TypeAlias = (
107 __.cabc.Sequence[ PrinterFactoryUnion ] )
110@_validate_arguments
111def count_columns_visual( text: str ) -> int:
112 # Note: If CSI ED ("Erase on Display") or EL ("Erase in Line") sequences
113 # are used within the text, then the count will not be accurate.
114 text_no_ansi = remove_ansi_c1_sequences( text )
115 return __.wcwidth.wcswidth( text_no_ansi )
118@_validate_arguments
119def remove_ansi_c1_sequences( text: str ) -> str:
120 # https://stackoverflow.com/a/14693789/14833542
121 regex = __.re.compile( r'''\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])''' )
122 return regex.sub( '', text )
125@_validate_arguments
126def produce_columns_max_calculator(
127 target: __.typx.TextIO
128) -> ColumnsMaxCalculator:
129 fileno_revealer = getattr( target, 'fileno', None )
130 if fileno_revealer is None: return None
131 try: fileno = fileno_revealer( )
132 except ( IOError, OSError, __.io.UnsupportedOperation ): return None
133 if not __.os.isatty( fileno ): return None
135 def calculate( ) -> __.typx.Optional[ int ]:
136 try: size = __.shutil.get_terminal_size( fileno )
137 except Exception: return None
138 return size.columns
140 return calculate
143@_validate_arguments
144def produce_printer_factory_default(
145 target: __.typx.TextIO,
146 colorize: __.Absential[ bool ] = __.absent,
147 force_colorize: bool = False,
148) -> PrinterFactory:
149 ''' Produces default printer factory associated with a stream.
151 Can optionally force ANSI SGR sequences (terminal color attributes,
152 etc...) on target stream.
153 '''
154 def produce_printer( address: str, flavor: _flavors.Flavor ) -> Printer:
155 from .standard import Printer
156 match __.sys.platform:
157 case 'win32':
158 winansi = _colorama.AnsiToWin32( target )
159 target_ = ( # pragma: no cover
160 __.typx.cast( __.typx.TextIO, winansi.stream )
161 if winansi.convert else target )
162 case _: target_ = target
163 return Printer(
164 target = target_,
165 colorize = colorize,
166 force_colorize = force_colorize )
168 return produce_printer
171# def truncate_visual( text: str, columns_max: int ) -> str:
172# lsize = 0
173# for i, c in enumerate( text ):
174# csize = __.wcwidth.wcwidth( c )
175# csize = max( 0, csize ) # control or combining character
176# if lsize + csize > columns_max:
177# # TODO? Add ellipsis.
178# return text[ : i ]
179# lsize += csize
180# return text