move_hello.gif

همانطور که می‌بینید با کلید‌های جهت می‌توانیم عبارت Hello World را جا‌به‌جا کنیم.
#!/usr/bin/env bash 

shopt -s checkwinsize || return 1; (:;:)
خط دوم اسکریپت کلکی هوشمندانه برای چک کردن سایز ترمینال و ست کردن متغیر‌های COLUMNS و LINES است. این متغیرها هنگام تعامل کاربر با ترمینال ست می‌شوند و به همین خاطر استفاده از ‌آن‌ها در اسکریپت به کمی زیرکی نیاز دارد.

با استفاده از یک No Op ترمینال رو گول می‌زنیم تا توهم تعامل بزنه (😂) و متغیرها رو ست کنه. می‌تونستیم از

/usr/bin/true

هم استفاده کنیم ولی خیلی کار جالبی نیست!

عبارت (:;:) در اصل هیچ‌کاری انجام نمی‌دهد اما باعث می‌شود تا ترمینال دو متغیر مورد نیاز را به شل گزارش دهد. درباره‌ noop بیشتر بخوانید

در قسمت بعدی مختصات میانه‌ی ترمینال رو محاسبه می‌کنیم. این کار ضروری نیست. می‌توانستیم به سادگی x و y رو به COLUMNS و LINES اختصاص بدیم یا حتا مستقیما از COLUMNS و LINES استفاده کنیم. دقت کنید که در نحو ریاضی بش، یعنی در Arithmetic Syntax ،نیازی به استفاده از سیجیل $ برای متغیرها نداریم.

x=$(( COLUMNS / 2 ))
y=$(( LINES / 2 ))
با استفاده از کد انسی می‌توانیم عبارت خود را در مکان مشخصی در ترمینال چاپ کنیم.
printf "\e[10;20H Hello"
مختصات ۱۰ و ۲۰ مکان نشانگر هنگام چاپ را مشخص می‌کند. این کد را به شکل زیر نیز می‌توان نوشت:
printf "\e[%d;%dH Hello" 10 20
این نحو مرسوم نوشتار printf است. d% یک format specifier یا همان placeholder برای مقداری است که بعد از آن معین می‌شود. d% به معنی digit یا عدد صحیح است. برای اطلاعات بیشتر، `man printf` و `man 3 printf`را ببینید.

پس تا اینجای کار کد ما به شکل زیر است:

#!/usr/bin/env bash 

shopt -s checkwinsize || return 1; (:;:)

x=$(( COLUMNS / 2 ))
y=$(( LINES / 2 ))

printf "\e[%d;%dH Hello" $y $x

در قسمت بعدی کلید‌های جهت را تعریف می‌کنیم. برای این‌کار از `cat -v` استفاده می کنیم تا کد ASCII کلید‌ها را یاد بگیریم.

img

به ترتیب از چپ، بالا پایین چپ راست.

برای خواندن یک متغیر از STDIN طبق معمول از read استفاده می‌کنیم.

بنابراین برای دنبال کردن کلید‌هایی که توسط کاربر فشرده می‌شوند به شیوه‌ی زیر عمل می‌کنیم:

read -rsn3 key

فلگ‌های rsn3 به ترتیب برای خواندن بک‌اسلش‌ها، سکوت خروجی و خواندن مشخصا ۳ حرف از ورودی استفاده می‌شوند. برای اطلاعات بیشتر، man read را ببینید.

حالا با ترکیب یک لوپ و یک کیس مقادیر x و y را تغییر داده، ترمینال را پاک کرده و عبارت را مجدد چاپ می‌کنیم تا حرکت عبارت میسر شود.

while :; do

    read -rsn3 key
    clear

    case "$key" in
        $'\e[A') ((y--));;
        $'\e[B') ((y++));;
        $'\e[C') ((x++));;
        $'\e[D') ((x--));;
    esac

	printf "\e[%d;%dH Hello" $y $x
done

نحو خاص ;: به معنی true است.

تمام کد تا اینجا:

#!/usr/bin/env bash 

shopt -s checkwinsize || return 1; (:;:)

x=$(( COLUMNS / 2 ))
y=$(( LINES / 2 ))

while :; do

		printf "\e[%d;%dH Hello" $y $x
    
		read -rsn3 key
    clear

    case "$key" in
        $'\e[A') ((y--));;
        $'\e[B') ((y++));;
        $'\e[C') ((x++));;
        $'\e[D') ((x--));;
    esac
done

دقت کنید که در صورتی‌ که printf بعد از case قرار بگیرد، تا دریافت کلیدی از کاربر چیزی چاپ نخواهد شد.

move_hello_simple.gif

همانطور که در گیف می‌بینید، تفاوت‌هایی بین گیف اول و نتیجه‌ی فعلی کد ما وجود دارد.
  • نشانگر پنهان نشده است
  • ترمینال به موقع پاک نمی‌شود
  • محتوای پیشین ترمینال حفظ نمی‌شود
برای پنهان کردن نشانگر از یک کد انسی استفاده می‌کنیم:
printf "\e[?25l"

دستور انسی بالا به شکل echo -ne "\033[?25l" یا printf "\033[?25l"نیز می‌توانست نوشته شود. ( این قاعده بر باقی دستورات هم صدق می‌کند)

هردوی e\ و 033\ کد اسکی ۲۷ (ASCII 27) را به ترمینال دیکته می‌کنند. e\ یک escape sequence است که معنی ASCII 27 می‌دهد و 033\ نوشتار اوکتال همان است.

برای حفظ محتوای پیشین ترمینال از قابلیت Alt Screen استفاده می‌کنیم. این قابلیت که با یک کد انسی فعال می‌شود باعث می‌شود تا برنامه‌ی ما در یک بافر جداگانه اجرا شود. این همان ویژگی است که برنامه‌هایی همچون ویم از آن استفاده می‌کنند.

printf "\x1b[?1049h"
برای ظاهر کردن نشانگر و بازگشتن به بافر اصلی به ترتیب از دستورات انسی زیر استفاده می‌کنیم:
printf "\e[?25l"
printf "\x1b[?1049h"
بنابراین می توانیم دو تابع برای اعمال تنظیمات لازم پیش از اجرای قسمت اصلی کد، و برای بازگرداندن حالت پیشین ترمینال داشته باشیم.
setupTerm() {
    printf "\x1b[?1049h"
    printf "\e[?25l"
}

restoreTerm() {
    printf "\x1b[?1049l"
    printf "\x1B[?25h"
}
همچنین برای منظم کردن کد، برای چاپ عبارت نیز یک تابع می‌سازیم:
printHello() {
    printf '\e[%d;%dH Hello World' $y $x
}
حالا کافیست توابع را به نوبت و در جای درست صدا بزنیم تا کار خود را انجام دهند.
#!/usr/bin/env bash 

shopt -s checkwinsize || return 1; (:;:)

x=$(( COLUMNS / 2 ))
y=$(( LINES / 2 ))

setupTerm() {
    printf "\x1b[?1049h"
    printf "\e[?25l"
}

restoreTerm() {
    printf "\x1b[?1049l"
    printf "\x1B[?25h"
}

printHello() {
    printf '\e[%d;%dH Hello World' $y $x
}

setupTerm

trap 'restoreTerm;exit' SIGINT

printHello

while :; do

    read -rsn3 key
    clear

    case "$key" in
        $'\e[A') ((y--));;
        $'\e[B') ((y++));;
        $'\e[C') ((x++));;
        $'\e[D') ((x--));;
    esac

    printHello
done

برای اینکه تابع restoreTerm هنگام خروج و دریافت SIGINT یعنی Ctrl-c اجرا شود، از trap استفاده می‌کنیم. برای اطلاعات بیشتر man trap ببنید.




مطالب این سایت بصورت مداوم به‌روز‌رسانی می‌شوند. برای دنبال کردن مطالب این سایت را بوکمارک کنید.

جهت حمایت مالی از پروژه، در صورتی که در ایران هستید، روی دکمه‌ی برام قهوه بخر کلیک کنید.

جهت ارتباط با نگارنده از طریق تلگرام یا ایمیل اقدام کنید.

Telegram Protonmail