xulbux 1.6.4__py3-none-any.whl → 1.6.6__py3-none-any.whl

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.

Potentially problematic release.


This version of xulbux might be problematic. Click here for more details.

xulbux/xx_console.py CHANGED
@@ -1,31 +1,18 @@
1
1
  """
2
- Functions for logging and other small actions within the console:
3
- - `Console.get_args()`
4
- - `Console.user()`
5
- - `Console.is_admin()`
6
- - `Console.pause_exit()`
7
- - `Console.cls()`
8
- - `Console.log()`
9
- - `Console.debug()`
10
- - `Console.info()`
11
- - `Console.done()`
12
- - `Console.warn()`
13
- - `Console.fail()`
14
- - `Console.exit()`
15
- - `Console.confirm()`
16
- - `Console.restricted_input()`
17
- - `Console.pwd_input()`\n
18
- ------------------------------------------------------------------------------------------------------
2
+ Functions for logging and other small actions within the console.\n
3
+ ----------------------------------------------------------------------------------------------------------
19
4
  You can also use special formatting codes directly inside the log message to change their appearance.
20
- For more detailed information about formatting codes, see the the `xx_format_codes` description.
5
+ For more detailed information about formatting codes, see the the `xx_format_codes` module documentation.
21
6
  """
22
7
 
23
- from ._consts_ import DEFAULT, CHARS
24
- from .xx_format_codes import FormatCodes
8
+ from ._consts_ import COLOR, CHARS
9
+ from .xx_format_codes import FormatCodes, _COMPILED
25
10
  from .xx_string import String
26
- from .xx_color import *
11
+ from .xx_color import Color, rgba, hexa
27
12
 
28
- from contextlib import suppress
13
+ from prompt_toolkit.key_binding.key_bindings import KeyBindings
14
+ from typing import Optional
15
+ import prompt_toolkit as _prompt_toolkit
29
16
  import pyperclip as _pyperclip
30
17
  import keyboard as _keyboard
31
18
  import getpass as _getpass
@@ -35,8 +22,33 @@ import sys as _sys
35
22
  import os as _os
36
23
 
37
24
 
25
+ # YAPF: disable
26
+ class _ConsoleWidth:
27
+ def __get__(self, obj, owner=None):
28
+ return _os.get_terminal_size().columns
29
+
30
+ class _ConsoleHeight:
31
+ def __get__(self, obj, owner=None):
32
+ return _os.get_terminal_size().lines
33
+
34
+ class _ConsoleSize:
35
+ def __get__(self, obj, owner=None):
36
+ size = _os.get_terminal_size()
37
+ return (size.columns, size.lines)
38
+
39
+ class _ConsoleUser:
40
+ def __get__(self, obj, owner=None):
41
+ return _os.getenv("USER") or _os.getenv("USERNAME") or _getpass.getuser()
42
+ # YAPF: enable
43
+
44
+
38
45
  class Console:
39
46
 
47
+ w: int = _ConsoleWidth()
48
+ h: int = _ConsoleHeight()
49
+ wh: tuple[int, int] = _ConsoleSize()
50
+ usr: str = _ConsoleUser()
51
+
40
52
  @staticmethod
41
53
  def get_args(find_args: dict) -> dict[str, dict[str, any]]:
42
54
  args = _sys.argv[1:]
@@ -54,18 +66,6 @@ class Console:
54
66
  results[arg_key] = {"exists": exists, "value": value}
55
67
  return results
56
68
 
57
- def w() -> int:
58
- return getattr(_shutil.get_terminal_size(), "columns", 80)
59
-
60
- def h() -> int:
61
- return getattr(_shutil.get_terminal_size(), "lines", 24)
62
-
63
- def wh() -> tuple[int, int]:
64
- return Console.w(), Console.h()
65
-
66
- def user() -> str:
67
- return _os.getenv("USER") or _os.getenv("USERNAME") or _getpass.getuser()
68
-
69
69
  @staticmethod
70
70
  def pause_exit(
71
71
  pause: bool = False,
@@ -84,6 +84,7 @@ class Console:
84
84
  if exit:
85
85
  _sys.exit(exit_code)
86
86
 
87
+ @staticmethod
87
88
  def cls() -> None:
88
89
  """Will clear the console in addition to completely resetting the ANSI formats."""
89
90
  if _shutil.which("cls"):
@@ -94,33 +95,45 @@ class Console:
94
95
 
95
96
  @staticmethod
96
97
  def log(
97
- title: str,
98
+ title: Optional[str] = None,
98
99
  prompt: object = "",
100
+ format_linebreaks: bool = True,
99
101
  start: str = "",
100
102
  end: str = "\n",
101
103
  title_bg_color: hexa | rgba = None,
102
104
  default_color: hexa | rgba = None,
105
+ _console_tabsize: int = 8,
103
106
  ) -> None:
104
107
  """Will print a formatted log message:
105
108
  - `title` -⠀the title of the log message (e.g. `DEBUG`, `WARN`, `FAIL`, etc.)
106
109
  - `prompt` -⠀the log message
110
+ - `format_linebreaks` -⠀whether to format (indent after) the line breaks or not
107
111
  - `start` -⠀something to print before the log is printed
108
- - `end` -⠀something to print after the log is printed (e.g. `\\n\\n`)
112
+ - `end` -⠀something to print after the log is printed (e.g. `\\n`)
109
113
  - `title_bg_color` -⠀the background color of the `title`
110
114
  - `default_color` -⠀the default text color of the `prompt`\n
111
- --------------------------------------------------------------------------------
112
- The log message supports special formatting codes. For more detailed
113
- information about formatting codes, see `xx_format_codes` class description."""
115
+ -----------------------------------------------------------------------------------
116
+ The log message can be formatted with special formatting codes. For more detailed
117
+ information about formatting codes, see `xx_format_codes` module documentation."""
118
+ title = "" if title is None else title.strip().upper()
119
+ title_len, tab_len = len(title) + 4, _console_tabsize - ((len(title) + 4) % _console_tabsize)
114
120
  title_color = "_color" if not title_bg_color else Color.text_color_for_on_bg(title_bg_color)
115
- if title:
121
+ if format_linebreaks:
122
+ prompt_lst = (String.split_count(l, Console.w - (title_len + tab_len)) for l in str(prompt).splitlines())
123
+ prompt_lst = (item for lst in prompt_lst for item in (lst if isinstance(lst, list) else [lst]))
124
+ prompt = f"\n{' ' * title_len}\t".join(prompt_lst)
125
+ else:
126
+ prompt = str(prompt)
127
+ if title == "":
116
128
  FormatCodes.print(
117
- f'{start} [bold][{title_color}]{f"[BG:{title_bg_color}]" if title_bg_color else ""} {title.upper()}: [_]\t{f"[{default_color}]" if default_color else ""}{str(prompt)}[_]',
129
+ f'{start} {f"[{default_color}]" if default_color else ""}{str(prompt)}[_]',
118
130
  default_color=default_color,
119
131
  end=end,
120
132
  )
121
133
  else:
122
134
  FormatCodes.print(
123
- f'{start} {f"[{default_color}]" if default_color else ""}{str(prompt)}[_]',
135
+ f'{start} [bold][{title_color}]{f"[BG:{title_bg_color}]" if title_bg_color else ""} {title} [_]'
136
+ + f'\t{f"[{default_color}]" if default_color else ""}{prompt}[_]',
124
137
  default_color=default_color,
125
138
  end=end,
126
139
  )
@@ -129,108 +142,154 @@ class Console:
129
142
  def debug(
130
143
  prompt: object = "Point in program reached.",
131
144
  active: bool = True,
132
- start: str = "\n",
133
- end: str = "\n\n",
134
- title_bg_color: hexa | rgba = DEFAULT.color["yellow"],
135
- default_color: hexa | rgba = DEFAULT.text_color,
145
+ format_linebreaks: bool = True,
146
+ start: str = "",
147
+ end: str = "\n",
148
+ title_bg_color: hexa | rgba = COLOR.yellow,
149
+ default_color: hexa | rgba = COLOR.text,
136
150
  pause: bool = False,
137
151
  exit: bool = False,
138
152
  ) -> None:
139
153
  """A preset for `log()`: `DEBUG` log message with the options to pause
140
- at the message and exit the program after the message was printed."""
154
+ at the message and exit the program after the message was printed.
155
+ If `active` is false, no debug message will be printed."""
141
156
  if active:
142
- Console.log("DEBUG", prompt, start, end, title_bg_color, default_color)
157
+ Console.log("DEBUG", prompt, format_linebreaks, start, end, title_bg_color, default_color)
143
158
  Console.pause_exit(pause, exit)
144
159
 
145
160
  @staticmethod
146
161
  def info(
147
162
  prompt: object = "Program running.",
148
- start: str = "\n",
149
- end: str = "\n\n",
150
- title_bg_color: hexa | rgba = DEFAULT.color["blue"],
151
- default_color: hexa | rgba = DEFAULT.text_color,
163
+ format_linebreaks: bool = True,
164
+ start: str = "",
165
+ end: str = "\n",
166
+ title_bg_color: hexa | rgba = COLOR.blue,
167
+ default_color: hexa | rgba = COLOR.text,
152
168
  pause: bool = False,
153
169
  exit: bool = False,
154
170
  ) -> None:
155
171
  """A preset for `log()`: `INFO` log message with the options to pause
156
172
  at the message and exit the program after the message was printed."""
157
- Console.log("INFO", prompt, start, end, title_bg_color, default_color)
173
+ Console.log("INFO", prompt, format_linebreaks, start, end, title_bg_color, default_color)
158
174
  Console.pause_exit(pause, exit)
159
175
 
160
176
  @staticmethod
161
177
  def done(
162
178
  prompt: object = "Program finished.",
163
- start: str = "\n",
164
- end: str = "\n\n",
165
- title_bg_color: hexa | rgba = DEFAULT.color["teal"],
166
- default_color: hexa | rgba = DEFAULT.text_color,
179
+ format_linebreaks: bool = True,
180
+ start: str = "",
181
+ end: str = "\n",
182
+ title_bg_color: hexa | rgba = COLOR.teal,
183
+ default_color: hexa | rgba = COLOR.text,
167
184
  pause: bool = False,
168
185
  exit: bool = False,
169
186
  ) -> None:
170
187
  """A preset for `log()`: `DONE` log message with the options to pause
171
188
  at the message and exit the program after the message was printed."""
172
- Console.log("DONE", prompt, start, end, title_bg_color, default_color)
189
+ Console.log("DONE", prompt, format_linebreaks, start, end, title_bg_color, default_color)
173
190
  Console.pause_exit(pause, exit)
174
191
 
175
192
  @staticmethod
176
193
  def warn(
177
194
  prompt: object = "Important message.",
178
- start: str = "\n",
179
- end: str = "\n\n",
180
- title_bg_color: hexa | rgba = DEFAULT.color["orange"],
181
- default_color: hexa | rgba = DEFAULT.text_color,
195
+ format_linebreaks: bool = True,
196
+ start: str = "",
197
+ end: str = "\n",
198
+ title_bg_color: hexa | rgba = COLOR.orange,
199
+ default_color: hexa | rgba = COLOR.text,
182
200
  pause: bool = False,
183
201
  exit: bool = False,
184
202
  ) -> None:
185
203
  """A preset for `log()`: `WARN` log message with the options to pause
186
204
  at the message and exit the program after the message was printed."""
187
- Console.log("WARN", prompt, start, end, title_bg_color, default_color)
205
+ Console.log("WARN", prompt, format_linebreaks, start, end, title_bg_color, default_color)
188
206
  Console.pause_exit(pause, exit)
189
207
 
190
208
  @staticmethod
191
209
  def fail(
192
210
  prompt: object = "Program error.",
193
- start: str = "\n",
194
- end: str = "\n\n",
195
- title_bg_color: hexa | rgba = DEFAULT.color["red"],
196
- default_color: hexa | rgba = DEFAULT.text_color,
211
+ format_linebreaks: bool = True,
212
+ start: str = "",
213
+ end: str = "\n",
214
+ title_bg_color: hexa | rgba = COLOR.red,
215
+ default_color: hexa | rgba = COLOR.text,
197
216
  pause: bool = False,
198
217
  exit: bool = True,
199
218
  reset_ansi=True,
200
219
  ) -> None:
201
220
  """A preset for `log()`: `FAIL` log message with the options to pause
202
221
  at the message and exit the program after the message was printed."""
203
- Console.log("FAIL", prompt, start, end, title_bg_color, default_color)
222
+ Console.log("FAIL", prompt, format_linebreaks, start, end, title_bg_color, default_color)
204
223
  Console.pause_exit(pause, exit, reset_ansi=reset_ansi)
205
224
 
206
225
  @staticmethod
207
226
  def exit(
208
227
  prompt: object = "Program ended.",
209
- start: str = "\n",
210
- end: str = "\n\n",
211
- title_bg_color: hexa | rgba = DEFAULT.color["magenta"],
212
- default_color: hexa | rgba = DEFAULT.text_color,
228
+ format_linebreaks: bool = True,
229
+ start: str = "",
230
+ end: str = "\n",
231
+ title_bg_color: hexa | rgba = COLOR.magenta,
232
+ default_color: hexa | rgba = COLOR.text,
213
233
  pause: bool = False,
214
234
  exit: bool = True,
215
235
  reset_ansi=True,
216
236
  ) -> None:
217
237
  """A preset for `log()`: `EXIT` log message with the options to pause
218
238
  at the message and exit the program after the message was printed."""
219
- Console.log("EXIT", prompt, start, end, title_bg_color, default_color)
239
+ Console.log("EXIT", prompt, format_linebreaks, start, end, title_bg_color, default_color)
220
240
  Console.pause_exit(pause, exit, reset_ansi=reset_ansi)
221
241
 
242
+ @staticmethod
243
+ def log_box(
244
+ *values: object,
245
+ start: str = "",
246
+ end: str = "\n",
247
+ box_bg_color: str | hexa | rgba = "green",
248
+ default_color: hexa | rgba = "#000",
249
+ w_padding: int = 2,
250
+ w_full: bool = False,
251
+ ) -> None:
252
+ """Will print a box, containing a formatted log message:
253
+ - `*values` -⠀the box content (each value is on a new line)
254
+ - `start` -⠀something to print before the log box is printed
255
+ - `end` -⠀something to print after the log box is printed (e.g. `\\n`)
256
+ - `box_bg_color` -⠀the box's background color
257
+ - `default_color` -⠀the default text color of the `*values`
258
+ - `w_padding` -⠀the horizontal padding (in chars) to the box content
259
+ - `w_full` -⠀whether to make the box be the full console width or not\n
260
+ -----------------------------------------------------------------------------------
261
+ The box content can be formatted with special formatting codes. For more detailed
262
+ information about formatting codes, see `xx_format_codes` module documentation."""
263
+ lines = [line.strip() for val in values for line in val.splitlines()]
264
+ unfmt_lines = [FormatCodes.remove_formatting(line) for line in lines]
265
+ max_line_len = max(len(line) for line in unfmt_lines)
266
+ pad_w_full = (Console.w - (max_line_len + (2 * w_padding))) if w_full else 0
267
+ lines = [
268
+ f"[bg:{box_bg_color}]{' ' * w_padding}{line}" + " " *
269
+ ((w_padding + max_line_len - len(unfmt)) + pad_w_full) + "[_bg]" for line, unfmt in zip(lines, unfmt_lines)
270
+ ]
271
+ pady = " " * (Console.w if w_full else max_line_len + (2 * w_padding))
272
+ FormatCodes.print(
273
+ f"{start}[bg:{box_bg_color}]{pady}[_bg]\n"
274
+ + _COMPILED["formatting"].sub(lambda m: f"{m.group(0)}[bg:{box_bg_color}]", "\n".join(lines))
275
+ + f"\n[bg:{box_bg_color}]{pady}[_bg]",
276
+ default_color=default_color,
277
+ sep="\n",
278
+ end=end,
279
+ )
280
+
222
281
  @staticmethod
223
282
  def confirm(
224
283
  prompt: object = "Do you want to continue?",
225
- start="\n",
284
+ start="",
226
285
  end="\n",
227
- default_color: hexa | rgba = DEFAULT.color["cyan"],
286
+ default_color: hexa | rgba = COLOR.cyan,
228
287
  default_is_yes: bool = True,
229
288
  ) -> bool:
230
289
  """Ask a yes/no question.\n
231
- -------------------------------------------------------------------------------
232
- The question can be formatted with special formatting codes. For more detailed
233
- information about formatting codes, see the `xx_format_codes` description."""
290
+ ---------------------------------------------------------------------------------------
291
+ The prompt can be formatted with special formatting codes. For more detailed
292
+ information about formatting codes, see the `xx_format_codes` module documentation."""
234
293
  confirmed = input(
235
294
  FormatCodes.to_ansi(
236
295
  f'{start} {str(prompt)} [_|dim](({"Y" if default_is_yes else "y"}/{"n" if default_is_yes else "N"}): )',
@@ -244,21 +303,24 @@ class Console:
244
303
  @staticmethod
245
304
  def restricted_input(
246
305
  prompt: object = "",
306
+ start="",
307
+ end="\n",
308
+ default_color: hexa | rgba = COLOR.cyan,
247
309
  allowed_chars: str = CHARS.all,
248
310
  min_len: int = None,
249
311
  max_len: int = None,
250
312
  mask_char: str = None,
251
313
  reset_ansi: bool = True,
252
- ) -> str | None:
314
+ ) -> Optional[str]:
253
315
  """Acts like a standard Python `input()` with the advantage, that you can specify:
254
316
  - what text characters the user is allowed to type and
255
317
  - the minimum and/or maximum length of the users input
256
318
  - optional mask character (hide user input, e.g. for passwords)
257
319
  - reset the ANSI formatting codes after the user continues\n
258
- -----------------------------------------------------------------------------------
320
+ ---------------------------------------------------------------------------------------
259
321
  The input can be formatted with special formatting codes. For more detailed
260
- information about formatting codes, see the `xx_format_codes` description."""
261
- FormatCodes.print(prompt, end="", flush=True)
322
+ information about formatting codes, see the `xx_format_codes` module documentation."""
323
+ FormatCodes.print(start + prompt, default_color=default_color, end="")
262
324
  result = ""
263
325
  select_all = False
264
326
  last_line_count = 1
@@ -266,22 +328,17 @@ class Console:
266
328
 
267
329
  def update_display(console_width: int) -> None:
268
330
  nonlocal select_all, last_line_count, last_console_width
269
- lines = String.split_count(
270
- str(prompt) + (mask_char * len(result) if mask_char else result),
271
- console_width,
272
- )
331
+ lines = String.split_count(str(prompt) + (mask_char * len(result) if mask_char else result), console_width)
273
332
  line_count = len(lines)
274
333
  if (line_count > 1 or line_count < last_line_count) and not last_line_count == 1:
275
334
  if last_console_width > console_width:
276
335
  line_count *= 2
277
- for _ in range(
278
- line_count
279
- if line_count < last_line_count and not line_count > last_line_count
280
- else (line_count - 2 if line_count > last_line_count else line_count - 1)
281
- ):
336
+ for _ in range(line_count if line_count < last_line_count and not line_count > last_line_count else (
337
+ line_count - 2 if line_count > last_line_count else line_count - 1)):
282
338
  _sys.stdout.write("\033[2K\r\033[A")
283
339
  prompt_len = len(str(prompt)) if prompt else 0
284
- prompt_str, input_str = lines[0][:prompt_len], (
340
+ prompt_str = lines[0][:prompt_len]
341
+ input_str = (
285
342
  lines[0][prompt_len:] if len(lines) == 1 else "\n".join([lines[0][prompt_len:]] + lines[1:])
286
343
  ) # SEPARATE THE PROMPT AND THE INPUT
287
344
  _sys.stdout.write(
@@ -292,7 +349,7 @@ class Console:
292
349
  def handle_enter():
293
350
  if min_len is not None and len(result) < min_len:
294
351
  return False
295
- FormatCodes.print("[_]" if reset_ansi else "", flush=True)
352
+ FormatCodes.print(f"[_]{end}" if reset_ansi else end, default_color=default_color)
296
353
  return True
297
354
 
298
355
  def handle_backspace_delete():
@@ -301,7 +358,7 @@ class Console:
301
358
  result, select_all = "", False
302
359
  elif result and event.name == "backspace":
303
360
  result = result[:-1]
304
- update_display(Console.w())
361
+ update_display(Console.w)
305
362
 
306
363
  def handle_paste():
307
364
  nonlocal result, select_all
@@ -310,25 +367,18 @@ class Console:
310
367
  filtered_text = "".join(char for char in _pyperclip.paste() if allowed_chars == CHARS.all or char in allowed_chars)
311
368
  if max_len is None or len(result) + len(filtered_text) <= max_len:
312
369
  result += filtered_text
313
- update_display(Console.w())
370
+ update_display(Console.w)
314
371
 
315
372
  def handle_select_all():
316
373
  nonlocal select_all
317
374
  select_all = True
318
- update_display(Console.w())
319
-
320
- def handle_copy():
321
- nonlocal select_all
322
- with suppress(KeyboardInterrupt):
323
- select_all = False
324
- update_display(Console.w())
325
- _pyperclip.copy(result)
375
+ update_display(Console.w)
326
376
 
327
377
  def handle_character_input():
328
378
  nonlocal result
329
379
  if (allowed_chars == CHARS.all or event.name in allowed_chars) and (max_len is None or len(result) < max_len):
330
380
  result += event.name
331
- update_display(Console.w())
381
+ update_display(Console.w)
332
382
 
333
383
  while True:
334
384
  event = _keyboard.read_event()
@@ -341,8 +391,8 @@ class Console:
341
391
  handle_paste()
342
392
  elif event.name == "a" and _keyboard.is_pressed("ctrl"):
343
393
  handle_select_all()
344
- elif event.name == "c" and _keyboard.is_pressed("ctrl") and select_all:
345
- handle_copy()
394
+ elif event.name == "c" and _keyboard.is_pressed("ctrl"):
395
+ raise KeyboardInterrupt
346
396
  elif event.name == "esc":
347
397
  return None
348
398
  elif event.name == "space":
@@ -351,16 +401,54 @@ class Console:
351
401
  handle_character_input()
352
402
  else:
353
403
  select_all = False
354
- update_display(Console.w())
404
+ update_display(Console.w)
355
405
 
356
406
  @staticmethod
357
407
  def pwd_input(
358
408
  prompt: object = "Password: ",
409
+ start="",
410
+ end="\n",
411
+ default_color: hexa | rgba = COLOR.cyan,
359
412
  allowed_chars: str = CHARS.standard_ascii,
360
413
  min_len: int = None,
361
414
  max_len: int = None,
362
- _reset_ansi: bool = True,
415
+ reset_ansi: bool = True,
363
416
  ) -> str:
364
417
  """Password input (preset for `Console.restricted_input()`)
365
418
  that always masks the entered characters with asterisks."""
366
- return Console.restricted_input(prompt, allowed_chars, min_len, max_len, "*", _reset_ansi)
419
+ return Console.restricted_input(prompt, start, end, default_color, allowed_chars, min_len, max_len, "*", reset_ansi)
420
+
421
+ @staticmethod
422
+ def multiline_input(
423
+ prompt: object = "",
424
+ start="",
425
+ end="\n",
426
+ default_color: hexa | rgba = COLOR.cyan,
427
+ show_keybindings=True,
428
+ input_prefix=" ⤷ ",
429
+ reset_ansi=True,
430
+ ) -> str:
431
+ """An input where users can input (and paste) text over multiple lines.\n
432
+ -----------------------------------------------------------------------------------
433
+ - `prompt` -⠀the input prompt
434
+ - `start` -⠀something to print before the input
435
+ - `end` -⠀something to print after the input (e.g. `\\n`)
436
+ - `default_color` -⠀the default text color of the `prompt`
437
+ - `show_keybindings` -⠀whether to show the special keybindings or not
438
+ - `input_prefix` -⠀the prefix of the input line
439
+ - `reset_ansi` -⠀whether to reset the ANSI codes after the input or not
440
+ -----------------------------------------------------------------------------------
441
+ The input prompt can be formatted with special formatting codes. For more detailed
442
+ information about formatting codes, see `xx_format_codes` module documentation."""
443
+ kb = KeyBindings()
444
+
445
+ @kb.add("c-d", eager=True) # CTRL+D
446
+ def _(event):
447
+ event.app.exit(result=event.app.current_buffer.document.text)
448
+
449
+ FormatCodes.print(start + prompt, default_color=default_color)
450
+ if show_keybindings:
451
+ FormatCodes.print("[dim][[b](CTRL+D)[dim] : end of input][_dim]")
452
+ input_string = _prompt_toolkit.prompt(input_prefix, multiline=True, wrap_lines=True, key_bindings=kb)
453
+ FormatCodes.print("[_]" if reset_ansi else "", end=end[1:] if end.startswith("\n") else end)
454
+ return input_string