'address#', 'd'=>'%date', ]; // Fields that are editable private $fieldoptions = [ 'a'=>['edit'=>TRUE], // Address 'p'=>['edit'=>TRUE], // Password 'u'=>['edit'=>TRUE], // User ]; // @todo Move this to the database private $header = RED.'T'.BLUE.'E'.GREEN.'S'.YELLOW.'T'.MAGENTA.'!'; public function __construct(\App\Models\Frame $o,string $msg=NULL) { dump(__METHOD__); $this->frame = $o; $this->output = $this->hasFlag('clear') ? CLS : HOME; // If we have a message to display on the bottom line. if ($msg) $this->output .= UP.$msg.HOME; $startline = 0; if (! $this->hasFlag('ip')) { // Set the page header: CUG/Site Name | Page # | Cost $this->output .= $this->render_header($this->header). $this->render_page($this->frame->frame,$this->frame->index). $this->render_cost($this->frame->cost); $startline = 1; } // Calculate fields and render output. $this->fields($startline); } /** * Render the frame * * @return null|string */ public function __toString() { return $this->output; } /** * Convert the frame from Binary to Output * Look for fields within the frame. * * @param int $startline */ public function fields($startline=0,$fieldchar='.') { $infield = FALSE; // In a field $fieldtype = NULL; // Type of field $fieldlength = 0; // Length of field $this->fields = collect(); // Fields in this frame. if ($startline) $this->output .= str_repeat(DOWN,$startline); // $fieldadrline = 1; // Scan the frame for a field start for ($y=$startline;$y<=$this->frame_length;$y++) { // Fields can only be on a single line $fieldx = $fieldy = FALSE; for ($x=0;$x<$this->frame_width;$x++) { $posn = $y*40+$x; // If the frame is not big enough, fill it with spaces. $byte = ord(isset($this->frame->content{$posn}) ? $this->frame->content{$posn} : ' ')%128; // Check for start-of-field if ($byte == ord(ESC)) { // Esc designates start of field (Esc-K is end of edit) $infield = TRUE; $fieldlength = 1; $fieldtype = ord(substr($this->frame->content,$posn+1,1))%128; $this->output .= $fieldchar; } else { if ($infield) { if ($byte == $fieldtype) { $fieldlength++; $byte = ord($fieldchar); // Replace field with $fieldchar. if ($fieldx === FALSE) { $fieldx = $x; $fieldy = $y; } // Is this a magic field? // @todo For page redisplay *00, we should show entered contents - for refresh *09 we should show updated contents if (array_get($this->fieldmap,chr($fieldtype)) ) { $field = $this->fieldmap[chr($fieldtype)]; //dump(['infield','byte'=>$byte,'fieldtype'=>$fieldtype,'field'=>$field,'strpos'=>strpos($field,'#')]); /* // address field has many lines. increment when hit on first character. if ($fieldlength == 1 && strpos($field,'#') !== false) { $field = str_replace('#',$fieldadrline,$field); dump(['field'=>$field,'fieldadrline'=>$fieldadrline,'fieldadrline'=>$fieldadrline]); $fieldadrline++; } */ // Replace field with Date if ($field == '%date') { // Drop the last dot and replace it. if ($fieldlength == 2) { $datetime = date('D d M H:ia'); $this->output = rtrim($this->output,$fieldchar); $this->output .= $datetime{0}; } if ($fieldlength > 1 AND $fieldlength <= strlen($datetime)) $byte = ord($datetime{$fieldlength-1}); } // @todo user data /* else if (isset($user[$field])) { if ($fieldlength <= strlen($user[$field])) { $byte = ord($user[$field]{$fieldlength-1}); } } /*else // pre-load field contents. PAM or *00 ? if (isset($fields[what]['value'])) { */ } } else { $this->fields->push(new FrameFields([ 'type'=>chr($fieldtype), 'length'=>$fieldlength, 'x'=>$fieldx-1, // Adjust for the ESC char 'y'=>$fieldy, ])); Log::debug(sprintf('Frame: %s, Field found at [%s,%s], Type: %s, Length: %s',$this->page(),$fieldx-1,$fieldy,$fieldtype,$fieldlength)); $infield = FALSE; $fieldx = $fieldy = FALSE; } } } // truncate end of lines @todo havent validated this code or used it? if (isset($pageflags['tru']) && substr($this->frame->content,$posn,40-$x) === str_repeat(' ',40-$x)) { $this->output .= CR . LF; break; } if (! $infield OR $fieldlength > 1) $this->output .= ($byte < 32) ? ESC.chr($byte+64) : chr($byte); } } } /** * Returns the current frame. */ public function frame() { return $this->frame->frame; } /** * Return the current field configuration */ public function getField(int $id) { return $this->fields->get($id); } /** * Get a specific key of the field options that passes a filter test * * @param string $type * @param int $after * @return mixed */ public function getFieldId($type='edit',$after=0) { return $this->fields ->search(function($item,$key) use ($type,$after) { return $key >= $after AND $this->isFieldEditable($item->type); }); } /** * Return the flag for this page * * CLEAR: Clear Screen before rendering. * * @param $flag * @return bool */ public function hasFlag($flag) { return $this->frame->hasFlag($flag); } /** * Return the frame DB id * * @return mixed */ public function id() { return $this->frame->id; } /** * Current frame index * * @return mixed */ public function index() { return $this->frame->index; } /** * Return the next index */ public function index_next() { return chr(ord($this->frame->index)+1); } /** * Determine if a field is editable * * @param string $field * @return mixed */ public function isFieldEditable(string $field) { return array_get(array_get($this->fieldoptions,$field),'edit',FALSE); } /** * Return the Page Number */ public function page(bool $as_array=FALSE) { return $as_array ? ['frame'=>$this->frame->frame,'index'=>$this->frame->index] : $this->frame->page; } /** * Return the next page number. * * @param bool $as_array * @return mixed */ public function pagenext(bool $as_array=FALSE) { return $as_array ? ['frame'=>$this->frame->frame,'index'=>$this->index_next()] : $this->frame->frame.$this->index_next(); } /** * Render the cost of the frame * * @param int $cost * @return string * @throws \Exception */ private function render_cost(int $cost) { if ($cost > 999) throw new \Exception('Price too high'); if ($cost > 100) $color = RED; elseif ($cost > 0) $color = YELLOW; else $color = GREEN; return sprintf($color.'% '.$this->cost_length.'.0f%s',$cost,$this->cost_unit); } /** * Render the Site Header * * @param string $header * @return bool|string */ private function render_header(string $header) { $filler = ($this->strlenv($header) < $this->header_length) ? str_repeat(' ',$this->header_length-$this->strlenv($header)) : ''; return substr($header.$filler,0,$this->header_length+substr_count($this->header,ESC)); } /** * Render the Frame Number * * @param int $num * @param string $frame * @return string * @throws \Exception */ private function render_page(int $num,string $frame) { if ($num > 999999999) throw new \Exception('Page Number too big',500); if (strlen($frame) !== 1) throw new \Exception('Frame invalid',500); return sprintf(WHITE.'% '.$this->pagenum_length.'.0f%s',$num,$frame); } /** * Get the route for the key press * * @param string $read */ public function route(string $read) { // @todo return FALSE; } /** * Calculate the length of text * * ESC characters are two chars, and need to be counted as one. * * @param $text * @return int */ function strlenv($text) { return strlen($text)-substr_count($text,ESC); } public static function testFrame() { // Simulate a DB load $o = new \App\Models\Frame; $o->content = ''; $o->flags = ['ip']; $o->type = 'a'; $o->frame = 999; $o->index = 'a'; // Header $o->content .= substr(R_RED.'T'.R_BLUE.'E'.R_GREEN.'S'.R_YELLOW.'T-12345678901234567890',0,20). R_WHITE.'999999999a'.R_RED.sprintf('%07.0f',999).'u'; $o->content .= str_repeat('+-',18).' '.R_RED.'01'; $o->content .= 'Name: '.ESC.str_repeat('u',5).str_repeat('+-',14); $o->content .= 'Date: '.ESC.str_repeat('d',25).str_repeat('+-',4); $o->content .= 'Address: '.ESC.str_repeat('a',19).' '.str_repeat('+-',5); $o->content .= ' : '.ESC.str_repeat('a',19).' '.str_repeat('+-',5); return $o; } /** * Return the Frame Type */ public function type() { return $this->frame->type(); } }