нажми меня
нажми меня

BASHUI - это BASH + UI, а не то что вы подумали.

Начиная работать над sshto я решил не переизобретать велосипед, вернее не переизобретать велосипед целиком а только некоторые его части и в качестве "рамы с педалями" использовал dialog. Это значительно ускорило разработку, но идея написать свой UI на баше с блекджеком и всем остальным ни на секунду не покидала мой воспалённый мозг. Звёзды сошлись, и я решил воплотить этот проект в жизнь(в bash).

Какой 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

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

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

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

Комментарии (43)


  1. Tirarex
    16.11.2023 18:22
    +25

    Говоря на тему интерфейсов в CLI хочется приложить эту ссылку https://github.com/GiorgosXou/TUIFIManager


    1. vaniacer Автор
      16.11.2023 18:22
      +1

      Интеренсно, спасибо.


  1. gatoazul
    16.11.2023 18:22
    +15

    По-моему кто-то ломится в открытую дверь.

    man dialog


    1. domix32
      16.11.2023 18:22

      No manual entry for dialog

      Ну, вот например Debian отвечает. FreeBSD на удивление откликнулась.


      1. gatoazul
        16.11.2023 18:22
        +2

        sudo apt install dialog


    1. vaniacer Автор
      16.11.2023 18:22

      Кто-то не научился нормально оформлять статьи( Про dialog написано в преамбуле.


      1. gatoazul
        16.11.2023 18:22

        То-то я недоумевал, что статья с полуслова начинается.


        1. vaniacer Автор
          16.11.2023 18:22

          Новая схема оформления туго заходит, раньше с катом как-то проще было. И покрутив колесико вверх можно было предисловие перечитать. А сейчас получается если человек с мобилы смотрит или из "читают сейчас" то предисловие пропадает...


  1. zhuser
    16.11.2023 18:22
    +17

    Наверное 100% программистов в начале своего увлечения кодингом писали ascii-gui. Прекрасное увлечение, во-первых -- покогает структурировать модульность. Во-вторых -- смиряет с мыслью, что всё уже написано до нас...


    1. ImagineTables
      16.11.2023 18:22
      +6

      Кто из нас в детстве не писал свой Нортон-Командер…


      1. JQuery567
        16.11.2023 18:22
        +1

        не писал тот, у кого детство было не на ассемблере и Turbo Vision :-)


      1. aschensekt
        16.11.2023 18:22

        Именно этим я решил заняться сейчас как раз для того чтобы лишний раз поучиться писать на плюсах))


    1. DikSoft
      16.11.2023 18:22

      Наверное 100% программистов в начале ..

      .. а ещё часть писала для них свои генераторы готовых интерфейсов. )

      Проходил через такое на dBase III а потом на Clipper 87. ) После чего свалил на Clipper Tools и FastPro от SoftScribe.


    1. Aleus1249355
      16.11.2023 18:22
      +2

      Я среди них :-)

      Буквально на второй год изучения программирования на плюсах. С цветами и озвучиванием кнопок ????


  1. texder
    16.11.2023 18:22
    +3

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


    1. vaniacer Автор
      16.11.2023 18:22

      Наздоровье, не вы один)


  1. 13werwolf13
    16.11.2023 18:22
    +2

    люблю tui интерфейсы, хотя в работе чаще удобнее оказывается банальный cli.
    люблю bash
    люблю интересные статьи на хабре

    автор, респект, интересное!


    1. vaniacer Автор
      16.11.2023 18:22

      Спасибо!)


  1. Breathe_the_pressure
    16.11.2023 18:22
    +4

    Dos Navigator?


    1. sappience
      16.11.2023 18:22
      +2

      Turbo Vision


    1. JQuery567
      16.11.2023 18:22

      а не накорябать ли по мотивам Dos Navigatora Bash Нафигатор? :-) Нет уж, лучше на python! :-)


    1. vaniacer Автор
      16.11.2023 18:22

      Bash Navigator же)


  1. amkartashov
    16.11.2023 18:22
    +4

    башуй, пока молодой


    1. vaniacer Автор
      16.11.2023 18:22

      Старый башуй, прод не уронит)


      1. khajiit
        16.11.2023 18:22

        … но и глубоко не отладит?


        1. vaniacer Автор
          16.11.2023 18:22

          Просто наладит.


  1. PyReader
    16.11.2023 18:22
    +2

    Не Баш, а Питон, но безумно мощная вещь, почти все внутренние тулы пишу на нем https://github.com/Textualize/textual


    1. vaniacer Автор
      16.11.2023 18:22

      Интересно, спасибо.


  1. denim
    16.11.2023 18:22
    +6

    Прочитал с удовольствием. Пожалуйста не сокращайте message до mess и button до butt, гораздо лучше будет msg и btn, ваш вариант звучит слегка эммм неоднозначно ????


    1. FreeNickname
      16.11.2023 18:22
      +5

      Учитывая приписку про альтернативный режим игры автора, возможно, так и задумано :)


    1. domix32
      16.11.2023 18:22
      +3

      Это же общее состояние скриптования на шелл.


    1. vaniacer Автор
      16.11.2023 18:22
      +1

      Bashui тоже какбэ намекает;) А еще когда я python'а своего расчехляю люблю сокращать dictionary как dick 8---


  1. emusic
    16.11.2023 18:22

    А я подумал, что это для BASIC... :)


    1. JQuery567
      16.11.2023 18:22

      bashic :-)


    1. vaniacer Автор
      16.11.2023 18:22

      Было бы BASICUI O_o


      1. emusic
        16.11.2023 18:22

        Нет. :) В названиях того, что относилось к BASIC, обычно использовалось сокращение "bas". :)


        1. vaniacer Автор
          16.11.2023 18:22

          Забавно, да, для basic'а идеально подошло бы)


  1. domix32
    16.11.2023 18:22
    +1

    Ещё есть модно-молодёжное gum.


    1. vaniacer Автор
      16.11.2023 18:22
      +1

      Да, именно так и выглядит, интересно, спасибо.


  1. Self_Perfection
    16.11.2023 18:22
    +2

    Смотрю на код и думаю: "а не автор ли piu-piu-sh написал сие?". И точно! Стиль кода узнаваемый.

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


    1. vaniacer Автор
      16.11.2023 18:22

      Про продакшон шутка конечно)


  1. bblkovo
    16.11.2023 18:22

    Лучше бы автор sshto доделал на предмет мульти файлов и использовании записей типа host dev-*

    User dev

    Host dev1

    Hostname dev-mon

    А то такие записи не считывает


    1. vaniacer Автор
      16.11.2023 18:22

      Это опенсорс, шлите пулреквесты...