diff --git a/src/output/Console.php b/src/output/Console.php new file mode 100644 index 0000000..e555126 --- /dev/null +++ b/src/output/Console.php @@ -0,0 +1,240 @@ + self::LEVEL_DEBUG, + "d" => self::LEVEL_DEBUG, + "normal" => self::LEVEL_NORMAL, + "n" => self::LEVEL_NORMAL, + "major" => self::LEVEL_MAJOR, + "m" => self::LEVEL_MAJOR, + ]; + + const TYPE_PREFIXES = [ + self::LEVEL_MAJOR => [ + "section" => [true, "SECTION!", "===", "= ", " =", "==="], + "title" => ["TITLE!", null, "T ", "", "---"], + "desc" => ["DESC!", "> ", ""], + "error" => ["CRITICAL!", "E! ", ""], + "warn" => ["ATTENTION!", "W! ", ""], + "note" => ["IMPORTANT!", "N! ", ""], + "info" => ["IMPORTANT!", "I! ", ""], + ], + self::LEVEL_NORMAL => [ + "section" => [true, "SECTION:", null, ">> ", " <<", "---"], + "title" => ["TITLE:", null, "T ", "", null], + "desc" => ["DESC:", "> ", ""], + "error" => ["ERROR:", "E ", ""], + "warn" => ["WARN:", "W ", ""], + "note" => ["NOTE:", "N ", ""], + "info" => ["INFO:", "I ", ""], + ], + self::LEVEL_DEBUG => [ + "section" => [false, "s", null, ">> ", " <<", null], + "title" => ["t", "t ", ""], + "desc" => [">", "> ", ""], + "error" => ["e", "e ", ""], + "warn" => ["w", "w ", ""], + "note" => ["i", "i ", ""], + "info" => ["D", "D ", ""], + ], + ]; + + const RESULT_PREFIXES = [ + "step" => ["*", "."], + "failure" => ["(FAILURE)", ""], + "success" => ["(SUCCESS)", ""], + "neutral" => [null, null], + ]; + + function __construct(?array $params=null) { + $debug = boolval(cl::get($params, "debug")); + $minLevel = cl::get($params, "min_level"); + if ($minLevel === null) $minLevel = $debug? self::LEVEL_DEBUG: self::LEVEL_NORMAL; + if (!in_array($minLevel, self::VALID_LEVELS)) { + $minLevel = cl::get(self::LEVEL_MAP, $minLevel, $minLevel); + } + if (!in_array($minLevel, self::VALID_LEVELS)) { + throw new Exception("$minLevel: invalid level"); + } + + $this->out = new StdOutput(STDOUT); + $this->err = new StdOutput(STDERR); + $this->minLevel = intval($minLevel); + $this->pending = []; + $this->inSection = false; + $this->titles = null; + $this->actions = null; + } + + /** @var StdOutput la sortie standard */ + protected $out; + + /** @var StdOutput la sortie d'erreur */ + protected $err; + + /** @var int level minimum que doivent avoir les messages pour être affichés */ + protected $minLevel; + + /** @var bool est-on dans une section? */ + protected $inSection; + + /** @var array|string section qui est en attente d'affichage */ + protected $section; + + function section($content, int $level=self::LEVEL_NORMAL): void { + $this->endSection(); + $this->inSection = true; + if ($level < $this->minLevel) return; + $this->section = $content; + } + + protected function printSection() { + if ($this->section !== null) { + $this->section = null; + } + } + + protected function endSection(): void { + $this->inSection = false; + $this->section = null; + } + + /** @var array */ + protected $titles; + + /** @var array */ + protected $currentTitle; + + function title($content, int $level=self::LEVEL_NORMAL): void { + if ($level < $this->minLevel) return; + $this->titles[] = [ + "title" => $content, + "descs" => [], + "print" => true, + ]; + $this->currentTitle =& $this->titles[count($this->titles) - 1]; + } + + function desc($content, int $level=self::LEVEL_NORMAL): void { + if ($level < $this->minLevel) return; + $this->currentTitle["descs"][] = $content; + } + + protected function printTitles(): void { + $this->printSection(); + } + + protected function endTitle(): void { + array_pop($this->titles); + if ($this->titles) { + $this->currentTitle =& $this->titles[count($this->titles) - 1]; + } else { + unset($this->currentTitle); + } + } + + function print($content, int $level=self::LEVEL_NORMAL): void { + if ($level < $this->minLevel) return; + $this->printTitles(); + $this->out->print($content); + } + + /** @var array */ + protected $actions; + + /** @var array */ + protected $currentAction; + + function action($content, int $level=self::LEVEL_NORMAL): void { + $this->actions[] = [ + "level" => $level, + "contents" => [$content], + "result" => null, + "print" => true, + ]; + $this->currentAction =& $this->actions[count($this->actions) - 1]; + } + + function printActions(): void { + $this->printTitles(); + + } + + function step($content): void { + $this->printActions(); + } + + function success($content=null): void { + $this->currentAction["contents"][] = $content; + $this->currentAction["result"] = true; + $this->printActions(); + $this->endAction(); + } + + function failure($content=null): void { + $this->currentAction["contents"][] = $content; + $this->currentAction["result"] = false; + $this->printActions(); + $this->endAction(); + } + + function neutral($content=null): void { + $this->currentAction["contents"][] = $content; + $this->currentAction["result"] = null; + $this->printActions(); + $this->endAction(); + } + + protected function endAction(): void { + array_pop($this->actions); + if ($this->actions) { + $this->currentAction =& $this->actions[count($this->actions) - 1]; + } else { + unset($this->currentAction); + } + } + + function info($content, int $level=self::LEVEL_NORMAL): void { + if ($level < $this->minLevel) return; + $this->printActions(); + } + + function note($content, int $level=self::LEVEL_NORMAL): void { + if ($level < $this->minLevel) return; + $this->printActions(); + } + + function warn($content, int $level=self::LEVEL_NORMAL): void { + if ($level < $this->minLevel) return; + $this->printActions(); + } + + function error($content, int $level=self::LEVEL_NORMAL): void { + if ($level < $this->minLevel) return; + $this->printActions(); + } + + function end(bool $all=false) { + if ($all) { + while ($this->actions) $this->neutral(); + while ($this->titles) $this->endTitle(); + $this->endSection(); + } elseif ($this->actions) { + $this->endAction(); + } elseif ($this->titles) { + $this->endTitle(); + } else { + $this->endSection(); + } + } +} diff --git a/src/output/IMessenger.php b/src/output/IMessenger.php new file mode 100644 index 0000000..1f33b36 --- /dev/null +++ b/src/output/IMessenger.php @@ -0,0 +1,87 @@ + "0", + "bold" => "1", + "faint" => "2", + "underlined" => "4", + "reverse" => "7", + "normal" => "22", + "black" => "30", + "red" => "31", + "green" => "32", + "yellow" => "33", + "blue" => "34", + "magenta" => "35", + "cyan" => "36", + "white" => "37", + "default" => "39", + "black-bg" => "40", + "red-bg" => "41", + "green-bg" => "42", + "yellow-bg" => "43", + "blue-bg" => "44", + "magenta-bg" => "45", + "cyan-bg" => "46", + "white-bg" => "47", + "default-bg" => "49", + ]; + + const COLOR_MAP = [ + "z" => "reset", + "o" => "black", + "r" => "red", + "g" => "green", + "y" => "yellow", + "b" => "blue", + "m" => "magenta", + "c" => "cyan", + "w" => "white", + "O" => "black_bg", + "R" => "red_bg", + "G" => "green_bg", + "Y" => "yellow_bg", + "B" => "blue_bg", + "M" => "magenta_bg", + "C" => "cyan_bg", + "W" => "white_bg", + "@" => "bold", + "-" => "faint", + "_" => "underlined", + "~" => "reverse", + "n" => "normal", + ]; + + /** + * @param resource|null $outf + * @throws Exception si la destination est un fichier et que son ouverture a + * échoué + */ + function __construct($output=null, ?array $params=null) { + if ($output === null) $output = cl::get($params, "output"); + $color = cl::get($params, "color"); + $filterTags = cl::get($params, "filter_tags", true); + $flush = cl::get($params, "flush"); + + if ($output === null) { + $outf = STDOUT; + } elseif ($output === "php://stdout") { + $outf = STDOUT; + } elseif ($output === "php://stderr") { + $outf = STDERR; + } elseif (!is_resource($output)) { + # si $outf est un nom de fichier, vérifier que l'ouverture se fait sans + # erreur. à partir de là, plus aucune gestion d'erreur n'est faite + $outf = @fopen($output, "ab"); + if ($outf === false) { + $error = error_get_last(); + if ($error !== null) $message = $error["message"]; + else $message = "$output: open error"; + throw new Exception($message); + } + if ($flush === null) $flush = true; + } else { + $outf = $output; + } + if ($color === null) $color = stream_isatty($outf); + if ($flush === null) $flush = false; + + $this->outf = $outf; + $this->color = boolval($color); + $this->filterTags = boolval($filterTags); + $this->flush = boolval($flush); + } + + /** @var resource */ + protected $outf; + + /** @var bool faut-il autoriser la sortie en couleur? */ + protected $color; + + function isColor(): bool { + return $this->color; + } + + /** @var bool faut-il enlever les tags dans la sortie? */ + protected $filterTags; + + /** @var bool faut-il flush le fichier après l'écriture de chaque ligne */ + protected $flush; + + function isatty(): bool { + return stream_isatty($this->outf); + } + + private static function replace_colors(array $ms): string { + $colors = []; + foreach (preg_split('/\s+/', $ms[1]) as $color) { + while ($color && !cl::has(self::COLORS, $color)) { + $alias = substr($color, 0, 1); + $colors[] = self::COLOR_MAP[$alias]; + $color = substr($color, 1); + } + if ($color) $colors[] = $color; + } + $text = "\x1B["; + $first = true; + foreach ($colors as $color) { + if (!$color) continue; + if ($first) $first = false; + else $text .= ";"; + $text .= self::COLORS[$color]; + } + $text .= "m"; + return $text; + } + protected function filterContent(string $text): string { + # couleur au début + $text = preg_replace_callback('/]*)>/', [self::class, "replace_colors"], $text); + # reset à la fin + $text = preg_replace('/<\/color>/', "\x1B[0m", $text); + # enlever les tags classiques + if ($this->filterTags) { + $text = preg_replace('/<[^>]*>/', "", $text); + } + return $text; + } + protected function filterColors(string $text): string { + return preg_replace('/\x1B\[.*?m/', "", $text); + } + + protected function fwrite(array $values): void { + $text = implode("", $values); + $text = $this->filterContent($text); + if (!$this->color) $text = $this->filterColors($text); + fwrite($this->outf, $text); + if ($this->flush) fflush($this->outf); + } + + function write(...$values): void { + $this->fwrite($values); + } + + function print(...$values): void { + $values[] = "\n"; + $this->fwrite($values); + } +} diff --git a/src/output/msg/TODO.md b/src/output/TODO.md similarity index 98% rename from src/output/msg/TODO.md rename to src/output/TODO.md index 959bbb5..a8cd04d 100644 --- a/src/output/msg/TODO.md +++ b/src/output/TODO.md @@ -1,4 +1,4 @@ -# msg +# IMessenger * 3 niveaux: DEBUG, NORMAL, MAJOR * plusieurs types de messages: