keyhandler = $handler; return $this; } /** * Update the menu items * * @param array $items * @return self */ public function setItems(array $items): self { $this->items = $items; return $this; } /** * Set the menu title * * @param string $title * @return self */ public function setTitle(string $title): self { $this->title = $title; return $this; } /** * Display the menu * * @param int $left * @param int $top * @param int $width * @param int $height * @param string|int|null $value * @return mixed */ public function display(int $left, int $top, int $width, int $height = 0, string|int|null $value = null): mixed { //$this->index = 0; $keys = array_keys($this->items); $this->index = array_search($value, $keys) ?: 0; [$w,$h] = $this->terminal->getSize(); if ($height == 0) { $height = min($h - 5, count($this->items) + 2); } if ($left == 0) { $left = round(($w - $width) / 2); } if ($top == 0) { $top = round(($h - $height) / 2); } $this->redraw($left, $top, $width, $height); $showing = true; while ($showing) { while (null === ($ch = $this->terminal->readKey())) { usleep(10000); } if (is_callable($this->keyhandler)) { $ret = call_user_func($this->keyhandler, $ch); if ($ret === true) { return false; } } if (mb_strlen($ch) == 1) { if ($ch == "\x03") { $showing = false; } if ($ch == "\r") { return array_keys($this->items)[$this->index]; } } else { switch ($ch) { case "k{UP}": $this->index--; if ($this->index < 0) $this->index = count($this->items) - 1; $this->redraw($left, $top, $width, $height); break; case "k{DOWN}": $this->index++; if ($this->index >= count($this->items)) $this->index = 0; $this->redraw($left, $top, $width, $height); break; case "k{PGUP}": $this->index -= $height - 2; if ($this->index < 0) $this->index = count($this->items) - 1; $this->redraw($left, $top, $width, $height); break; case "k{PGDN}": $this->index += $height - 2; if ($this->index >= count($this->items)) $this->index = 0; $this->redraw($left, $top, $width, $height); break; case "k{HOME}": $this->index = 0; if ($this->index >= count($this->items)) $this->index = 0; $this->redraw($left, $top, $width, $height); break; case "k{END}": $this->index = count($this->items) - 1; if ($this->index >= count($this->items)) $this->index = 0; $this->redraw($left, $top, $width, $height); break; } } } return null; } /** * Redraw menu * * @param int $left * @param int $top * @param int $width * @param int $height * @return void */ private function redraw(int $left, int $top, int $width, int $height) { $visibleItems = $height - 2; // scroll if index is out of view if ($this->index < $this->scroll) { $this->scroll = $this->index; } if ($this->index - $visibleItems >= $this->scroll) { $this->scroll = $this->index - $visibleItems + 1; } $scrollTop = $this->scroll; $scrollBottom = $scrollTop + $visibleItems - 1; $thumbTop = round(($visibleItems - 1) / count($this->items) * $scrollTop); $thumbBottom = round(($visibleItems) / count($this->items) * $scrollBottom); $tleft = (int)round(($width / 2) - ((mb_strlen($this->title) + 2) / 2)); // draw head echo "\e[40;37m"; $this->terminal ->writeAt($left, $top + 0, self::U_CORNER_ROUNDED_TOPLEFT.str_repeat(self::U_EDGE_HORIZONTAL,$width - 2).self::U_CORNER_ROUNDED_TOPRIGHT) ->writeAt($left + $tleft, $top + 0, " \e[1m{$this->title}\e[22m ") //->writeAt($left, $top + 1, self::U_EDGE_VERTICAL.str_repeat(" ",$width - 2).self::U_EDGE_VERTICAL) //->writeAt($left + 2, $top + 1, "\e[1m" . $this->title . "\e[22m") //->writeAt($left, $top + 2, self::U_EDGE_VERTICAL_RIGHT.str_repeat(self::U_EDGE_HORIZONTAL,$width - 2).self::U_EDGE_VERTICAL_LEFT) ->writeAt($left, $top + $height - 1, self::U_CORNER_BOTTOMLEFT.str_repeat(self::U_EDGE_HORIZONTAL,$width - 2).self::U_CORNER_BOTTOMRIGHT) ; $keys = array_keys($this->items); for ($n = 0; $n < $visibleItems; $n++) { $key = $keys[$n + $this->scroll]??null; $item = ($key ? ($this->items[$key]) : null); if ($item === "---") { $item = "\e[2m".str_repeat("\u{2500}", $width - 2)."\e[22m"; } elseif (is_null($item)) { $item = str_repeat(" ", $width - 2); } else { $item = mb_substr($item, 0, $width); $padlen = $width - 3 - $this->itemlen($item); $item = " " . $item . ($padlen>0?str_repeat(" ", $padlen):"") . "\e[40;37m"; $item = (($n + $this->scroll == $this->index)?"\e[37;44m":"\e[40;37m") . $item; } if ($n >= $thumbTop && $n <= $thumbBottom) { $scrollbar = "\e[97;1m".self::U_EDGE_VERTICAL_THUMB."\e[40;37;22m"; } else { $scrollbar = "\e[37;2m".self::U_EDGE_VERTICAL_SCROLL."\e[40;37;22m"; } $this->terminal ->writeAt($left, $top + 1 + $n, self::U_EDGE_VERTICAL.$item.$scrollbar); } echo "\e[0m"; } /** * Calculate item length without markup * * @param string $item * @return int */ private function itemlen(string $item): int { $item = preg_replace('<\e\[.+?m>', '', $item); return mb_strlen($item); } }