BASHUI

нажми меня

нажми меня

Какой UI без кнопок? С (т)ранный, поэтому я начал с элемента — кнопка. Идея заключается в том что кнопка (и остальные элементы UI) будет представлена функцией. Функцию можно использовать из коробки. Но удобнее сделать «обёртку»(функцию) с какими-то предустановленными параметрами и уже эту функцию использовать по назначению. Для всех элементов UI я подготовил примеры (demo_*) их можно найти в репе. Вот как выглядит пример для кнопки:

#!/bin/bash
source bashui

mess="RESULT"
name="The Button"
title="Push the button, will get a result..."

butt(){
    # Новая кнопка на основе button из bashui, параметры:
    #1 координата X(колонка)
    #2 координата Y(строка)
    #3 название кнопки
    #4 выполняемая функция
    #5 цвет текста
    #6 цвет рамки
    #7 цвет подложки
    local x=$((COLUMNS/2-(${#name}/2+2)))
    local y=$((LINES/2))
    #       1  2    3       4       5      6       7
    button $x $y "$name" "result" "$wht" "$ylw" "$bblk"
}

# кнопка выполнит эту функцию
result(){
    local x=$((COLUMNS/2-${#mess}/2))
    local y=$((LINES/2+5))
    XY           $x $y  "$mess"
    (sleep 1; XY $x $y "${mess//[[:print:]]/ }") &
}

# все собрано вместе
menu(){
    cursor off          # отключаем курсор
    default_button butt # это необходимо для активации кнопки
    XY $((COLUMNS/2-${#title}/2)) $((LINES/2-2)) "$title"
    butt # рисуем кнопку

    # в цикле опрос клавиатуры и логика
    while true; do
        read_input
        case $_input_ in
              enter ) press_button butt;; # нажат enter, нажимаем кнопку
              escape) return;;            # нажат escape, выход
        esac
    done
}

clear
menu

«Цветовая палитра» для выбора цвета текста, рамки и подложки задана в bashui вот так:

...
#-------------------------+--------------------------------+---------+
#       Text color        |       Background color         |         |
#-----------+-------------+--------------+-----------------+         |
# Base color|Lighter shade| Base color   | Lighter shade   |         |
#-----------+-------------+--------------+-----------------+         |
BLK='\e[30m'; blk='\e[90m'; BBLK='\e[40m'; bblk='\e[100m' #| Black   |
RED='\e[31m'; red='\e[91m'; BRED='\e[41m'; bred='\e[101m' #| Red     |
GRN='\e[32m'; grn='\e[92m'; BGRN='\e[42m'; bgrn='\e[102m' #| Green   |
YLW='\e[33m'; ylw='\e[93m'; BYLW='\e[43m'; bylw='\e[103m' #| Yellow  |
BLU='\e[34m'; blu='\e[94m'; BBLU='\e[44m'; bblu='\e[104m' #| Blue    |
MGN='\e[35m'; mgn='\e[95m'; BMGN='\e[45m'; bmgn='\e[105m' #| Magenta |
CYN='\e[36m'; cyn='\e[96m'; BCYN='\e[46m'; bcyn='\e[106m' #| Cyan    |
WHT='\e[37m'; wht='\e[97m'; BWHT='\e[47m'; bwht='\e[107m' #| White   |
#-------------------------{ Effects }----------------------+---------+
DEF='\e[0m'   #Default color and effects                             |
BLD='\e[1m'   #Bold\brighter                                         |
DIM='\e[2m'   #Dim\darker                                            |
CUR='\e[3m'   #Italic font                                           |
...

Этот код также доступен в отдельной репе bash_color. Попробуем получить результат:

result

result

Иногда необходимо количество кнопок больше чем одна. И надо как-то равномерно расположить их на экране. Чтобы упростить эту задачу я добавил две вспомогательные функции: x_row и y_row, для горизонтального и вертикального рядов кнопок соответственно, пример:

#!/bin/bash
source bashui

defb(){
    # Новая кнопка на основе button из bashui, параметры:
    #1 координата X(колонка)
    #2 координата Y(строка)
    #3 название кнопки
    #4 выполняемая функция
    #5 цвет текста
    #6 цвет рамки
    #7 цвет подложки
    #        1    2      3          4          5      6      7
    button "$1" "$2" "butt $3" "fun_${3,,}" "$wht" "$ylw" "$bblk"
}

# зачистка вывода
clr(){ for i in {5..9}; do XY 15 $i "%$((COLUMNS-15))s"; done; }

but_a(){ defb "$1" "$2" A; } # клонирую
but_b(){ defb "$1" "$2" B; } # кнопку
but_c(){ defb "$1" "$2" C; } # по
but_d(){ defb "$1" "$2" D; } # умолчанию
but_e(){ defb "$1" "$2" E; } # шесть
but_f(){ defb "$1" "$2" F; } # раз

fun_a(){ clr; XY 15 5 "butt A pressed(${_button_[*]})"; } # создаю
fun_b(){ clr; XY 15 5 "butt B pressed(${_button_[*]})"; } # функцию
fun_c(){ clr; XY 15 5 "butt C pressed(${_button_[*]})"; } # для
fun_d(){ clr; XY 15 5 "butt D pressed(${_button_[*]})"; } # выполнения
fun_e(){ clr; XY 15 5 "butt E pressed(${_button_[*]})"; } # каждой
fun_f(){ clr; XY 15 5 "butt F pressed(${_button_[*]})"; } # кнопкой

# ряды кнопок это два массива
ybuttons=(but_a but_b but_c) # вертикальные кнопки 
xbuttons=(but_d but_e but_f) # горизонтальные кнопки

# оба обработчика(все кнопки) собраны в одну функцию для удобства
butts(){
    y_row    2   "${ybuttons[@]}"
    x_row $LINES "${xbuttons[@]}"
}

menu(){
    cursor off
    default_button but_b
    XY 1 1 "$red Esc to exit"

    while true; do
        butts
        read_input
        case $_input_ in
             up     ) select_prev_butt "${ybuttons[@]}";;
             down   ) select_next_butt "${ybuttons[@]}";;
             left   ) select_prev_butt "${xbuttons[@]}";;
             right  ) select_next_butt "${xbuttons[@]}";;
             enter  ) press_button      butts          ;;
             escape ) return                           ;;
        esac
    done
}

clear
menu

Обратите внимание что у кнопок контролируемых через *row функции не установлены явно координаты, значения установлены в "$1" "$2", реальные значения координат передаются в кнопки из *row функций. В качестве аргументов *row функции принимают: позиция на экране (столбец или строка в зависимости от типа функции) и список кнопок. Кнопки (функции) можно просто перечислить так:

x_row 1 but_a but_b but_c

Но тогда придется копипастить список для функций select_(prev|next)_butt что не очень удобно при изменении количества кнопок. Придется редактировать и там тут. Удобней создать массивы для каждого ряда и использовать их в функциях.

Клавиши «вверх», «вниз» выбирают кнопки из вертикального столбика. Клавиши «влево», «вправо» выбирают кнопки из горизонтального ряда. Вместо тысячи слов:

кнопки

кнопки

Строка ввода. Сначала я сделал окно ввода информации ограниченное со всех сторон рамками, но выяснилось что read с некоторыми ключами при нажатии backspace ведет себя с (т)ранно…

read fail

read fail

Пришлось отказаться от окна и сделать простую строку во весь экран, пример оформления:

#!/bin/bash
source bashui

my_reader(){
    # Новое окно ввода на основе reader из bashui, параметры:
    #1 строка на которой появится окно ввода
    #2 максимальное кол-во символов
    #3 имя окна ввода
    #4 имя переменной в которую будет записан текст
    #5 текст по умолчанию
    #5 цвет текста
    #6 цвет рамки
    #7 цвет подложки
    #      1  2          3         4    5      6      7      8
    reader 10 20 'test data input' td "$td" "$wht" "$ylw" "$bblk"
}

td='initial text'
clear
my_reader
clear
bye "$td"

ввод информации

ввод информации

Ну и самое интересное — список. Это окно с произвольным количеством колонок в котором построчно отображаются какие-то данные. Количество колонок задается в диапазоне от 1 до N, где N = сколько_влезет_в_окно_терминала и определяется методом научного тыка. Выбор элементов осуществляется при помощи клавиш курсора, pgUp/Down и горячих клавиш. Каждому элементу первого столбца в таблице присваивается горячая клавиша соответствующая первому символу. Вот пример создания меню из списка и нескольких кнопок:

#!/bin/bash
source bashui

defb(){
    # Новая кнопка на основе button из bashui, параметры:
    #1 координата X(колонка)
    #2 координата Y(строка)
    #3 название кнопки
    #4 выполняемая функция
    #5 цвет текста
    #6 цвет рамки
    #7 цвет подложки
    #        1    2      3          4          5      6      7
    button "$1" "$2" "butt $3" "fun_${3,,}" "$wht" "$ylw" "$bblk"
}

clr(){ for i in {1..4}; do XY 1 $i "%${COLUMNS}s"; done; } # clear output area
but_d(){ defb "$1" "$2" D; }
but_e(){ defb "$1" "$2" E; }
but_f(){ defb "$1" "$2" F; }

fun_d(){ clr; XY 1 1 "butt D; target: $_target_"; }
fun_e(){ clr; XY 1 1 "butt E; description: ${_target_[1]}"; }
fun_f(){ clr; XY 1 1 "butt F; all items of target:"
              printf -v text -- '%s\n' "${_target_[@]}"
              XY 1 2  "$text"; }
clear
w=$((COLUMNS-10))
my_items(){
    # Новый список на основе items, параметры:
    #1 координата X(колонка)
    #2 координата Y(строка)
    #3 ширина окна
    #4 высота окна, мин. 5
    #5 кол-во колонок, N или в % от ширины
    #6 название списка
    #7 цвет текста
    #8 цвет рамки
    #9 цвет подложки
    local mess="Columns via number(${bred}Esc to continue$bblk)"
    #     1  2  3  4      5        6      7       8      9    data
    items 10 5 $w 15      3     "$mess" "$wht" "$ylw" "$bblk" "$@"
}
my_items2(){
    local mess="Columns via percent of Width(${bred}Esc to exit$bblk)"
    #     1  2  3  4      5        6      7       8      9    data
    items 10 5 $w 15 '30 55 15' "$mess" "$wht" "$ylw" "$bblk" "$@"
}

data=(               # массив с тестовыми данными
    #-------------{ first line - column descriptions }--------------------
$red'Item name'        $blu'Item description'                 $grn'Status'
    #-----------------------{ the data }----------------------------------
    'first'        $BLD$ylw'Long description text'                'true'
    'second'               'Description 2'                        'O_o'
    'third'                'description 3'                        'false'
    'fourth'         "${red}Long ${grn}description ${blu}text"    'true'
    ''                     ''                                     ''
$ylw'fifth'                'Description 2'                        'O_o'
    'sixth'                'description 3'                        'false'
    'midle'            $grn'Long description text'                'true'
    'long name row2'       'Description 2'                        'O_o'
    ''                     ''                                      ''
    'row 3'                'description 3'                        'false'
    'row1'             $blu'Long description text'                'true'
    'row1'                 'Long description text'                'true'
    'last'                 'Description 2'                        'O_o'
)

xbuttons=(but_d but_e but_f)
butts (){ x_row $LINES "${xbuttons[@]}"; }
ilist (){ my_items     "${data[@]}"    ; }
ilist2(){ my_items2    "${data[@]}"    ; }
menu  (){
    cursor off
    default_button but_d
    while true; do
        $1; butts
        read_input
        case $_input_ in
             up     ) select_prev_item "${data[@]}"    ;;
             down   ) select_next_item "${data[@]}"    ;;
             left   ) select_prev_butt "${xbuttons[@]}";;
             right  ) select_next_butt "${xbuttons[@]}";;
             enter  ) press_button         butts       ;;
             escape ) return;;
             pgup   ) select_prev_item  --fast   "${data[@]}";;
             pgdown ) select_next_item  --fast   "${data[@]}";;
             *      ) select_by_hot_key $_input_ "${data[@]}";;
        esac
    done
}

menu ilist  # Columns via number
menu ilist2 # Columns via percent

В примере создается список из трех стобцов, поэтому данные в массиве data оформлены в виде таблицы с тремя столбцами, чтобы визуально сразу представить как данные будут отображаться в списке. Первые 3(по количеству столбцов) элемента из массива данных, будут использованы в качестве названий стобцов и не отображаются в списке. Элементы можно «раскрашивать» разными цветами, добавляя соответствующие цветовые коды в начало элемента данных. Предусмотрено два способа установки ширины столбца:

  • автоматический, вы указываете только количество столбцов, ширина столбцов в этом случае определяется как ширина_окна/количество_столбцов и одинакова для всех столбцов

    менюшка, равномерное распределение

    менюшка, равномерное распределение

  • % от ширины окна, в этом случае в 5-й аргумент необходимо передать не цифру количества столбцов, а строку, содержащую несколько (по количеству столбцов) значений в %, сумма которых должна составить 100%, например '30 55 15'

    менюшка, распределение в % от ширины

    менюшка, распределение в % от ширины

Основным является первый столбец, но можно получить информацию из всех столбцов и строить рабочий процесс соответствующим образом. Полазим по менюшке:

меню

меню

В гифке продемонстрированы все типы перемещения: курсором, pgUp/Down и через горячие кнопки. А вот как реализован опрос клавиатуры:

read_input(){
    read -rsN1 _input_
    case ${_input_,,} in
            # One symbol keys support
                ' '*) _input_=space;;
              $'\t'*) _input_=tab  ;;
              $'\n'*) _input_=enter;;
            # escape sequences additional check
            $'\u1b'*) read -rsN4 -t 0.001 _input_
                      case ${_input_,,} in
                                   # arrows, pgUp/Down and escape support
                                   *a*) _input_=up    ;;
                                   *b*) _input_=down  ;;
                                   *d*) _input_=left  ;;
                                   *c*) _input_=right ;;
                                   *5*) _input_=pgup  ;;
                                   *6*) _input_=pgdown;;
                                    '') _input_=escape;;
                      esac;;
                   # the rest, return 1 symbol in lowercase
                   *) _input_=${_input_,,}
                      _input_=${_input_:0:1};;
    esac
}

Я опробовал этот bashui’вый интерфейс на одном из своих проектов sshto, посмотрите что из этого получилось demo_sshto

bashuisshto список хостов

bashuisshto список хостов

bashuisshto список команд

bashuisshto список команд

Проект находится на ранней стадии развития, абсолютно всё может поменяться как в лучшую так и в худшую сторону неоднократно, смело тащите в прод!)

Потрогать мой bashui можно тут.

кстати, что касается …hui (18+)

Разное касается, лично я предпочитаю класическую схему. Но иногда выдаю вот такое вот, для своей bash игры piu-piu я добавил режим penis’а О_о

Активируется он так:

penis=big ./piu-piu

В этом режиме вертолет заменяется на гигантский фалоимитатор с пропеллером. Вместо бонусов таблетки виагры от которых он увеличивается в размерах и стреляет эм, известно чем. Выглядит всё это непотребство как-то так:

shake'em

shake’em

Режим кооператива и дуэль тоже оху… там все еще хуже О_о

Творите, выдумывайте, пробуйте!)

Лайки, пальцы.

© Habrahabr.ru