diff --git a/src/CliProgram/TApplicationErrorHandling.php b/src/CliProgram/TApplicationErrorHandling.php
new file mode 100644
index 0000000..f1554fd
--- /dev/null
+++ b/src/CliProgram/TApplicationErrorHandling.php
@@ -0,0 +1,361 @@
+handleError($output, $errorLevel, $message, $file, $line);
+ }
+ );
+ set_exception_handler(
+ function (Throwable $exception) use ($output): void {
+ $this->handleException($output, $exception);
+ }
+ );
+ }
+
+ /**
+ * Handle an error
+ *
+ * @param OutputInterface $output The console output
+ * @param int $errorLevel The error level
+ * @param string $message The message
+ * @param string $file The error file
+ * @param int $line The error line
+ *
+ * @return bool Was the error handled ?
+ */
+ protected function handleError (OutputInterface $output, int $errorLevel, string $message, string $file, int $line): bool {
+ //region Check if this error level should be handled
+ if (!(error_reporting() & $errorLevel)) {
+ return false;
+ }
+ //endregion
+
+ //region Get the rendering output and tag, based on the error level
+ switch ($errorLevel) {
+ case E_ERROR:
+ case E_CORE_ERROR:
+ case E_COMPILE_ERROR:
+ case E_USER_ERROR:
+ case E_RECOVERABLE_ERROR:
+ case E_PARSE:
+ $renderOutput = CliHelper::getErrorOutput($output);
+ $renderTag = 'error';
+ break;
+
+ case E_WARNING:
+ case E_CORE_WARNING:
+ case E_COMPILE_WARNING:
+ case E_USER_WARNING:
+ case E_DEPRECATED;
+ case E_USER_DEPRECATED:
+ $renderOutput = $output;
+ $renderTag = 'comment';
+ break;
+
+ case E_NOTICE:
+ case E_STRICT:
+ default:
+ $renderOutput = $output;
+ $renderTag = null;
+ break;
+ }
+ //endregion
+ //region Get the "title" for the error level
+ $title = match ($errorLevel) {
+ E_ERROR => 'PHP Error',
+ E_CORE_ERROR => 'PHP Core Error',
+ E_COMPILE_ERROR => 'PHP Compilation Error',
+ E_USER_ERROR => 'User Error',
+ E_RECOVERABLE_ERROR => 'Recoverable Error',
+ E_PARSE => 'Parse Error',
+ E_WARNING => 'PHP Warning',
+ E_CORE_WARNING => 'PHP Core Warning',
+ E_COMPILE_WARNING => 'PHP Compilation Warning',
+ E_USER_WARNING => 'User Warning',
+ E_DEPRECATED => 'PHP Deprecated',
+ E_USER_DEPRECATED => 'User Deprecated',
+ E_NOTICE => 'PHP Notice',
+ E_STRICT => 'PHP Strict Notice',
+ default => 'Error',
+ };
+ //endregion
+
+ //region Get the traces
+ $traces = [];
+ $firstTraceFound = false;
+ foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $trace) {
+ //region Skip the firsts traces, until we find the real file and line
+ if (!$firstTraceFound) {
+ $traceFile = $trace['file'] ?? '';
+ $traceLine = $trace['line'] ?? '';
+ if ($traceFile === $file && $traceLine === $line) {
+ $firstTraceFound = true;
+ }
+ continue;
+ }
+ //endregion
+ $traces[] = $trace;
+ }
+
+ //region Add the current file and line to the trace
+ array_unshift($traces, [
+ 'function' => '',
+ 'file' => $file ? : 'n/a',
+ 'line' => $line ? : 'n/a',
+ 'args' => [],
+ ]);
+ //endregion
+ //endregion
+
+ //region Render the error
+ $this->renderError(
+ $renderOutput,
+ $title,
+ $message,
+ $file,
+ $line,
+ $traces,
+ $renderTag
+ );
+ //endregion
+ return true;
+ }
+ /**
+ * Handle an exception
+ *
+ * @param OutputInterface $output The console output
+ * @param Throwable $exception The exception
+ *
+ * @return void
+ */
+ protected function handleException (OutputInterface $output, Throwable $exception): void {
+ do {
+ $traces = $exception->getTrace();
+ array_unshift($traces, [
+ 'function' => '',
+ 'file' => $exception->getFile() ? : 'n/a',
+ 'line' => $exception->getLine() ? : 'n/a',
+ 'args' => [],
+ ]);
+
+ $message = MbstringExtended::trim($exception->getMessage());
+ if ($message === '' || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
+ $class = get_debug_type($exception);
+ $title = sprintf('%s%s', $class, 0 !== ($code = $exception->getCode()) ? ' (' . $code . ')' : '');
+ }
+ else {
+ $title = '';
+ }
+
+ $this->renderError(
+ $output,
+ $title,
+ $message,
+ $exception->getFile(),
+ $exception->getLine(),
+ $traces
+ );
+ } while ($exception = $exception->getPrevious());
+ }
+
+ /**
+ * Render an error
+ *
+ * @param OutputInterface $output The application output
+ * @param string $title The error title
+ * @param string $message The error message
+ * @param string $file The error file
+ * @param int $line The error line
+ * @param array{file:string,line:int,class:string,type:string,function:string} $traces The error traces
+ * @param string|null $renderTag The redering tag, or null for no tag
+ *
+ * @return void
+ */
+ public function renderError (
+ OutputInterface $output,
+ string $title,
+ string $message,
+ string $file,
+ int $line,
+ array $traces,
+ ?string $renderTag = 'error'
+ ): void {
+ $title = MbstringExtended::trim($title);
+ $message = MbstringExtended::trim($message);
+
+ if ($title !== '') {
+ $title = sprintf(' [%s] ', $title);
+ $len = Helper::width($title);
+ }
+ else {
+ $len = 0;
+ }
+
+ if (str_contains($message, "@anonymous\0")) {
+ $message = preg_replace_callback(
+ '/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/',
+ function ($match) {
+ return class_exists($match[0], false)
+ ? (get_parent_class($match[0])
+ ? : key(class_implements($match[0]))
+ ? : 'class'
+ ) . '@anonymous'
+ : $match[0];
+ },
+ $message
+ );
+ }
+
+ $width = PHP_INT_MAX;
+ if (property_exists($this, 'terminal') && $this->terminal instanceof Terminal && $this->terminal->getWidth()) {
+ $width = $this->terminal->getWidth() - 1;
+ }
+
+ $lines = [];
+ foreach ($message !== '' ? preg_split('/\r?\n/', $message) : [] as $messageLine) {
+ foreach ($this->tApplicationErrorHandling__splitStringByWidth($messageLine, $width - 4) as $messageSubline) {
+ // pre-format lines to get the right string length
+ $lineLength = Helper::width($messageSubline) + 4;
+ $lines[] = [$messageSubline, $lineLength];
+
+ $len = max($lineLength, $len);
+ }
+ }
+
+ $messages = [];
+ if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
+ $messages[] = sprintf('%s', OutputFormatter::escape(sprintf('In %s line %s:', basename($file) ? : 'n/a', $line ? : 'n/a')));
+ }
+ $messages[] = $emptyLine = $this->tApplicationErrorHandling__wrapInTag(str_repeat(' ', $len), $renderTag);
+ if ($title !== '') {
+ $messages[] = $this->tApplicationErrorHandling__wrapInTag(
+ sprintf(
+ '%s%s',
+ $title,
+ str_repeat(' ', max(0, $len - Helper::width($title)))
+ ),
+ $renderTag
+ );
+ }
+ foreach ($lines as $line) {
+ $messages[] = $this->tApplicationErrorHandling__wrapInTag(
+ sprintf(
+ ' %s %s',
+ OutputFormatter::escape($line[0]),
+ str_repeat(' ', $len - $line[1])
+ ),
+ $renderTag
+ );
+ }
+ $messages[] = $emptyLine;
+ $messages[] = '';
+
+ $output->writeln($messages, OutputInterface::VERBOSITY_QUIET);
+
+ if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) {
+ $output->writeln('Exception trace:', OutputInterface::VERBOSITY_QUIET);
+
+ foreach ($traces as $trace) {
+ $class = $trace['class'] ?? '';
+ $type = $trace['type'] ?? '';
+ $function = $trace['function'] ?? '';
+ $file = $trace['file'] ?? 'n/a';
+ $line = $trace['line'] ?? 'n/a';
+
+ $output->writeln(
+ sprintf(
+ ' %s%s at %s:%s',
+ $class,
+ $function
+ ? $type . $function . '()'
+ : '',
+ $file,
+ $line
+ ),
+ OutputInterface::VERBOSITY_QUIET
+ );
+ }
+
+ $output->writeln('', OutputInterface::VERBOSITY_QUIET);
+ }
+ }
+
+ /**
+ * Split a string by width
+ *
+ * str_split is not suitable for multi-byte characters, we should use preg_split to get char array properly.
+ *
additionally, array_slice() is not enough as some character has doubled width.
+ *
we need a function to split string not by character count but by string width
+ *
+ * @param string $string The string to split
+ * @param int $width The width of each line
+ *
+ * @return string[] The split string
+ */
+ private function tApplicationErrorHandling__splitStringByWidth (string $string, int $width): array {
+ if (false === $encoding = mb_detect_encoding($string, null, true)) {
+ return str_split($string, $width);
+ }
+
+ $utf8String = mb_convert_encoding($string, 'utf8', $encoding);
+ $lines = [];
+ $line = '';
+
+ $offset = 0;
+ while (preg_match('/.{1,10000}/u', $utf8String, $match, 0, $offset)) {
+ $offset += strlen($match[0]);
+
+ foreach (preg_split('//u', $match[0]) as $char) {
+ // test if $char could be appended to current line
+ if (mb_strwidth($line . $char, 'utf8') <= $width) {
+ $line .= $char;
+ continue;
+ }
+ // if not, push current line to array and make new line
+ $lines[] = str_pad($line, $width);
+ $line = $char;
+ }
+ }
+
+ $lines[] = count($lines) ? str_pad($line, $width) : $line;
+
+ mb_convert_variables($encoding, 'utf8', $lines);
+
+ return $lines;
+ }
+ /**
+ * Wrap a text in a tag
+ *
+ * @param string $string The text to wrap
+ * @param string|null $tag The tag to wrap the text in, or null to not wrap the text
+ *
+ * @return string The wrapped text
+ */
+ private function tApplicationErrorHandling__wrapInTag (string $string, ?string $tag): string {
+ if ($tag === null) {
+ return $string;
+ }
+ return sprintf('<%s>%s%s>', $tag, $string, $tag);
+ }
+}
\ No newline at end of file