mo = Mode::where('name','Ansi')->single(); } public function __get(string $key): mixed { switch ($key) { case 'color_page': return sprintf(" %s[%d;%dm",chr(self::ESC),self::I_HIGH_CODE,self::FG_WHITE_CODE); case 'color_unit': return sprintf(" %s[%d;%dm",chr(self::ESC),self::I_HIGH_CODE,self::FG_GREEN_CODE); default: return parent::__get($key); } } public function attr(array $field): string { return sprintf('%s[%d;%d;%dm',ESC,$field['i'],$field['f'],$field['b']); } /** * This function converts ANSI text into an array of attributes * * We include the attribute for every character, so that if a window is placed on top of this window, the edges * render correctly. * * @param string $contents Our ANSI content to convert * @param int $width Canvas width before we wrap to the next line * @param int $yoffset fields offset when rendered (based on main window) * @param int $xoffset fields offset when rendered (based on main window) * @param int|null $debug Enable debug mode * @return array * @throws \Exception */ public function parse(string $contents,int $width,int $yoffset=0,int $xoffset=0,?int $debug=NULL): array { $result = []; $lines = collect(explode("\r\n",$contents)); if ($debug) dump(['lines'=>$lines]); $i = 0; // Intensity $bg = self::BG_BLACK; // Background color $fg = self::LIGHTGRAY; // Foreground color $attr = $fg + $bg + $i; // Attribute int $default = ['i'=>0,'f'=>self::FG_LIGHTGRAY_CODE,'b'=>self::BG_BLACK_CODE]; $y = 0; // Line $saved_x = NULL; // Cursor saved $saved_y = NULL; // Cursor saved $ansi = $default; // Our current attribute used for input fields while ($lines->count() > 0) { $x = 0; $line = $lines->shift(); $result[$y+1] = []; if ($this->debug) dump(['next line'=>$line,'length'=>strlen($line)]); if (is_numeric($debug) && ($y > $debug)) { dump(['exiting'=>serialize($debug)]); exit(1); } while (strlen($line) > 0) { if ($debug) dump(['y:'=>$y,'attr'=>$attr,'line'=>$line,'length'=>strlen($line)]); if ($x >= $width) { $x = 0; $y++; } /* parse an attribute sequence*/ $m = []; preg_match('/^\x1b\[((\d+)+(;(\d+)+)*)m/U',$line,$m); if (count($m)) { $line = substr($line,strlen(array_shift($m))); // Are values separated by ; $m = array_map(function($item) { return (int)$item; },explode(';',$m[0])); // Sort our numbers sort($m); // Reset if ($m[0] === self::I_CLEAR_CODE) { $bg = self::BG_BLACK; $fg = self::LIGHTGRAY; $i = 0; $ansi = $default; array_shift($m); } // High Intensity if (count($m) && ($m[0] === self::I_HIGH_CODE)) { $i += ((($i === 0) || ($i === self::BLINK)) ? self::HIGH : 0); $ansi['i'] = self::I_HIGH_CODE; array_shift($m); } // Blink if (count($m) && ($m[0] === self::I_BLINK_CODE)) { $i += ((($i === 0) || ($i === self::HIGH)) ? self::BLINK : 0); array_shift($m); } // Foreground if (count($m) && ($m[0] >= self::FG_BLACK_CODE) && ($m[0] <= self::FG_LIGHTGRAY_CODE)) { $ansi['f'] = $m[0]; switch (array_shift($m)) { case self::FG_BLACK_CODE: $fg = self::BLACK; break; case self::FG_RED_CODE: $fg = self::RED; break; case self::FG_GREEN_CODE: $fg = self::GREEN; break; case self::FG_YELLOW_CODE: $fg = self::BROWN; break; case self::FG_BLUE_CODE: $fg = self::BLUE; break; case self::FG_MAGENTA_CODE: $fg = self::MAGENTA; break; case self::FG_CYAN_CODE: $fg = self::CYAN; break; case self::FG_LIGHTGRAY_CODE: $fg = self::LIGHTGRAY; break; } } // Background if (count($m) && ($m[0] >= self::BG_BLACK_CODE) && ($m[0] <= self::BG_LIGHTGRAY_CODE)) { $ansi['b'] = $m[0]; switch (array_shift($m)) { case self::BG_BLACK_CODE: $bg = self::BG_BLACK; break; case self::BG_RED_CODE: $bg = self::BG_RED; break; case self::BG_GREEN_CODE: $bg = self::BG_GREEN; break; case self::BG_BROWN_CODE: $bg = self::BG_BROWN; break; case self::BG_BLUE_CODE: $bg = self::BG_BLUE; break; case self::BG_MAGENTA_CODE: $bg = self::BG_MAGENTA; break; case self::BG_CYAN_CODE: $bg = self::BG_CYAN; break; case self::BG_LIGHTGRAY_CODE: $bg = self::BG_LIGHTGRAY; break; } } $attr = $bg + $fg + $i; continue; } /* parse absolute character position */ $m = []; preg_match('/^\x1b\[(\d*);?(\d*)[Hf]/',$line,$m); if (count($m)) { dump(['Hf'=>$m]); // @todo Remove once validated $line = substr($line,strlen(array_shift($m))); $y = (int)array_shift($m); if (count($m)) $x = (int)array_shift($m)-1; continue; } /* ignore an invalid sequence */ $m = []; preg_match('/^\x1b\[\?7h/',$line,$m); if (count($m)) { $line = substr($line,strlen(array_shift($m))); continue; } /* parse positional sequences */ $m = []; preg_match('/^\x1b\[(\d+)([A-D])/',$line,$m); if (count($m)) { $line = substr($line,strlen(array_shift($m))); switch ($m[1]) { /* parse an up positional sequence */ case 'A': $y -= ($m[0] < 1) ? 0 : $m[0]; break; /* parse a down positional sequence */ case 'B': $y += ($m[0] < 1) ? 0 : $m[0]; break; /* parse a forward positional sequence */ case 'C': $x += ($m[0] < 1) ? 0 : $m[0]; break; /* parse a backward positional sequence */ case 'D': $x -= ($m[0] < 1) ? 0 : $m[0]; break; } continue; } /* parse a clear screen sequence - we ignore them */ $m = []; preg_match('/^\x1b\[2J/',$line,$m); if (count($m)) { $line = substr($line,strlen(array_shift($m))); continue; } /* parse cursor sequences */ $m = []; preg_match('/^\x1b\[([su])/',$line,$m); if (count($m)) { $line = substr($line,strlen(array_shift($m))); switch ($m[0]) { /* parse save cursor sequence */ case 's': $saved_x = $x; $saved_y = $y; break; /* parse restore cursor sequence */ case 'u': $x = $saved_x; $y = $saved_y; break; } continue; } /* parse an input field */ // Input field 'FIELD;valueTYPE;input char' // @todo remove the trailing ESC \ to end the field, just use a control code ^B \x02 (Start of Text) and ^C \x03 $m = []; preg_match('/^\x1b_([A-Z]+;[0-9a-z]+)([;]?.+)?\x1b\\\/',$line,$m); if (count($m)) { $line = substr($line,strlen(array_shift($m))); // We are interested in our field match $f = explode(';',array_shift($m)); // First value is the field name $field = array_shift($f); // Second value is the length/type of the field, nnX nn=size in chars, X=type (lower case) $c = []; preg_match('/([0-9]+)([a-z])/',$xx=array_shift($f),$c); if (! count($c)) { Log::channel('bbs')->alert(sprintf('! IF FAILED PARSING FIELD LENGTH/TYPE [%02dx%02d] (%s)',$y,$x,$xx)); break; } // Third field is the char to use $fieldpad = count($f) ? array_shift($f) : '.'; Log::channel('bbs')->info(sprintf('- IF [%02dx%02d], Field: [%s], Length: [%d], Char: [%s]',$y,$x,$c[2],$c[1],$fieldpad)); // Any remaining fields are junk if (count($f)) Log::channel('bbs')->alert(sprintf('! IGNORING ADDITIONAL IF FIELDS [%02dx%02d] (%s)',$y,$x,join('',$f))); // If we are padding our field with a char, we need to add that back to $line // @todo validate if this goes beyond our width (and if scrolling not enabled) if ($c[1]) $line = str_repeat($fieldpad,$c[1]).$line; $this->fields_input->push(new Field([ 'attribute' => $ansi, 'name' => $field, 'pad' => $fieldpad, 'size' => $c[1], 'type' => $c[2], 'value' => NULL, 'x' => $x+$xoffset, 'y' => $y+$yoffset, ])); } /* parse dynamic value field */ // @todo remove the trailing ESC \ to end the field, just use a control code ie: ^E \x05 (Enquiry) or ^Z \x26 (Substitute) $m = []; preg_match('/^\x1bX([a-zA-Z._:^;]+[0-9]?;-?[0-9^;]+)([;]?[^;]+)?\x1b\\\/',$line,$m); if (count($m)) { $line = substr($line,strlen(array_shift($m))); // We are interested in our field match $f = explode(';',array_shift($m)); $pad = Arr::get($f,2,' '); Log::channel('bbs')->info(sprintf('- DF [%02dx%02d], Field: [%s], Length: [%d], Char: [%s]',$y,$x,$f[0],$f[1],$pad)); // If we are padding our field with a char, we need to add that back to line // @todo validate if this goes beyond our width (and if scrolling not enabled) $line = str_repeat($pad,abs($f[1])).$line; $this->fields_dynamic->push(new Field([ 'name' => $f[0], 'pad' => $pad, 'type' => NULL, 'size' => $f[1], 'value' => NULL, 'x' => $x+$xoffset, 'y' => $y+$yoffset, ])); } /* set character and attribute */ $ch = $line[0]; $line = substr($line,1); /* validate position */ if ($y < 0) $y = 0; if ($x < 0) $x = 0; if ($attr === null) throw new \Exception('Attribute is null?'); $result[$y+1][$x+1] = new Char($ch,$attr); $x++; } // If we got a self::BG_BLACK|self::LIGHTGRAY ESC [0m, but not character, we include it as it resets any background that was going on if (($attr === self::BG_BLACK|self::LIGHTGRAY) && isset($result[$y+1][$x]) && ($result[$y+1][$x]->attr !== $attr)) $result[$y+1][$x+1] = new Char(NULL,$attr); $y++; } return $result; } }