Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Nice prompt [Bash specific]

54 views
Skip to first unread message

Kaz Kylheku

unread,
Aug 3, 2023, 2:32:05 PM8/3/23
to
I "redesigned" my shell prompt yesterday.

I now have the current date/time, host name and working directory
(possibly shortened) in a scroll-protected status line at the bottom of
the terminal.

The prompt is just a dollar sign (PS1='\$ ') which is very refreshing.

Here is the code. I have this in a ~/.bash_functions file that gets
sourced.

Using the PROMPT_COMMAND hook mechanism, before every command, we
update the status area and scroll-protect it. Before that, we check whether the
LINES or COLUMNS has changed, in which case we call prepare_terminal
to reserve the bottom row for ourselves, and make sure the cursor is
off thatline. This happens on the first call to update_status_line
also, since old_lines and old_cols are zero.

Whenever a command has a termination status other than successful, we
print the line !<n>! where <n> is the status before the prompt.
Logic in update_status_line ensures that repetitions of this are suppressed
if the user hits enter on an empty line.

prepare_terminal()
{
stty rows $((LINES - 1))
printf "\n\033[1A"
old_lines=$LINES
old_cols=$COLUMNS
}

old_cmdno=0
old_lines=0
old_cols=0

update_status_line()
{
local exit=$?
local getcmdno='\#'
local cmdno=${getcmdno@P}
local esc=$(printf "\033")
local pwd=$PWD
local dots=

[ $LINES -eq $old_lines -a $COLUMNS -eq $old_cols ] || prepare_terminal

while true; do
[ "${pwd#/*/}" == "$pwd" ] && break
local status="$esc[7m$(date +%m-%d/%H:%M)$esc[m $HOSTNAME $dots$pwd"
local status_nohl="$(date +%m-%d/%H:%M) $HOSTNAME $dots$pwd"
[ ${#status_nohl} -le $COLUMNS ] && break
pwd=${pwd#/}
pwd=/${pwd#*/}
dots='...'
done

[ ${#status_nohl} -gt $COLUMNS ] && return

printf "${esc}7$esc[%s;1H$esc[K%s$esc[1;%sr${esc}8" $((LINES + 1)) "$status" $
LINES
if [ $exit -ne 0 -a $cmdno -ne $old_cmdno ] ; then
printf "!%s!\n" $exit
fi
old_cmdno=$cmdno
}

PROMPT_COMMAND='update_status_line'


--
TXR Programming Language: http://nongnu.org/txr
Cygnal: Cygwin Native Application Library: http://kylheku.com/cygnal
Mastodon: @Kazi...@mstdn.ca

Janis Papanagnou

unread,
Aug 4, 2023, 11:55:19 AM8/4/23
to
On 03.08.2023 20:31, Kaz Kylheku wrote:
> I "redesigned" my shell prompt yesterday.
>
> [...]

Hi Kaz, I wanted to try out your script and have two questions...

My bash doesn't seem to support the @P in cmdno=${getcmdno@P}
what does it do?

At certain places you use in printf contexts \033 or define a
variable esc to carry that value. In Kornshell I use \E or \e
for ANSI escapes, and my bash also supports that. Is there in
bash with PROMPT_COMMAND some technical necessity to use $esc ?

Janis

Kaz Kylheku

unread,
Aug 4, 2023, 1:27:02 PM8/4/23
to
On 2023-08-04, Janis Papanagnou <janis_pap...@hotmail.com> wrote:
> On 03.08.2023 20:31, Kaz Kylheku wrote:
>> I "redesigned" my shell prompt yesterday.
>>
>> [...]
>
> Hi Kaz, I wanted to try out your script and have two questions...

Hi Janis,

Thanks for trying it; if you get it working 100%, you will like it,
I'm sure. The idea is to set the prompt to just PS=1'\$ ' which
is clean and refreshing, and feels like old Unix. All the junk
like showing the date, time, host name and current directory, goes into
a scroll-protected status line at the bottom.

> My bash doesn't seem to support the @P in cmdno=${getcmdno@P}
> what does it do?

This is a newer feature (Bash 4.4+)? It specifies prompt-like
expansion.

The variable getcmdno contains a code that is normally used
in the PS1 prompt. @{getcmdno@P} handles that variable as if it
were PS1, expanding that code.

The only reason I used this was to gain access to the current prompt
line number. I don't think there is any other way to get to it. The line
number lets us distinguish whether the user is just hitting Enter on an
empty line:

$
$
$ _

or actually issuing new commands.

When the user issues an empty line, the value of $? observed by the
PROMPT_COMMAND hook remains the same. I wanted the failed status like
!1! to be printed only once, like this:

$ false
!1!
$
$
$ _

and not:

$ false
!1!
$
!1!
$
!1!
$ _

which is achieved by observing no change in the prompt number.

So it is all for aesthetics.

Someone who doesn't mind that, or doesn't want the termination
status, or does something else with it can remove all that
code.

> At certain places you use in printf contexts \033 or define a
> variable esc to carry that value. In Kornshell I use \E or \e
> for ANSI escapes, and my bash also supports that. Is there in
> bash with PROMPT_COMMAND some technical necessity to use $esc ?

I initially had a hard-coded escape.

By defining it as a named term, we can do this:

esc=ESC # for debugging

The performance hit of the extra substitutions is not an issue.

Also, the reason the following variables are defined the way they are is
to support reloading of the code.

old_cmdno=${old_cmdno-0}
old_lines=${old_lines-0}
old_cols=${old_cols-0}

If the variables already exist, their values are left alone, so that
update_statusS_line will not call prepare_terminal, which will steal
another line of display, which is an annoying behavior.

In Common Lisp, (defvar variable value) will not evaluate the value
expression or assign the variable if the variable already exists. This
supports reloading of modules without disturbing their global variables.
(You use defparameter, which always assigns, if you want that.)

Kaz Kylheku

unread,
Aug 4, 2023, 1:36:18 PM8/4/23
to
On 2023-08-03, Kaz Kylheku <864-11...@kylheku.com> wrote:
> while true; do
> [ "${pwd#/*/}" == "$pwd" ] && break
> local status="$esc[7m$(date +%m-%d/%H:%M)$esc[m $HOSTNAME $dots$pwd"
> local status_nohl="$(date +%m-%d/%H:%M) $HOSTNAME $dots$pwd"
> [ ${#status_nohl} -le $COLUMNS ] && break
> pwd=${pwd#/}
> pwd=/${pwd#*/}
> dots='...'
> done

Erratum: the break test at the top of the loop body should be moved to the
bottom:

while true; do
local status="$esc[7m$(date +%m-%d/%H:%M)$esc[m $HOSTNAME $dots$pwd"
local status_len=$((${#status} - ${#status_esc}))
[ $status_len -le $COLUMNS ] && break
pwd=${pwd#/}
pwd=/${pwd#*/}
dots='...'
[ "${pwd#/*/}" == "$pwd" ] && break
done

This causes an issue reported by a user on Mastodon, that
when you change to a directory like / or /home, the status
line disappears. We need the loop body executed unconditionally
at least to calculate a status line, and then to stop iterating
if the path cannot be reduced further after that.

There is one more check after the loop for a status line that cannot fit
the current $COLUMNS with, in which case it is turned off.

Keith Thompson

unread,
Aug 4, 2023, 2:54:18 PM8/4/23
to
Janis Papanagnou <janis_pap...@hotmail.com> writes:
[...]
> Hi Kaz, I wanted to try out your script and have two questions...
>
> My bash doesn't seem to support the @P in cmdno=${getcmdno@P}
> what does it do?

What version of bash are you using? This feature was introduced in bash
4.4, released in 2016.

From "bash info param":

'${PARAMETER@OPERATOR}'
The expansion is either a transformation of the value of PARAMETER
or information about PARAMETER itself, depending on the value of
OPERATOR. Each OPERATOR is a single letter:
[...]
'P'
The expansion is a string that is the result of expanding the
value of PARAMETER as if it were a prompt string (*note
Controlling the Prompt::).

--
Keith Thompson (The_Other_Keith) Keith.S.T...@gmail.com
Will write code for food.
void Void(void) { Void(); } /* The recursive call of the void */

Chris Elvidge

unread,
Aug 4, 2023, 4:04:50 PM8/4/23
to
Yes, nice, but you lose a line on the terminal (but who cares?). If you
use Esc]2; you can update the terminal title. However, no highlights.

Should PROMPT_COMMAND= be PROMPT_COMMAND[0]= for modernity?

As it happens, mine looks like this:
PROMPT_COMMAND=(
[0]="echo -ne \"\\033]11;#204A87\\007\""
[1]="echo -ne \"\\033]2;\${USER}@\${HOSTNAME} (\${IP}) : (\${KERNEL},
\${BASHVR}) : \$(pwd) : \${LAST_COMMAND}\\007\""
)

Cheers

--

Chris Elvidge, England
I AM NOT THE NEW DALAI LAMA

Kaz Kylheku

unread,
Aug 4, 2023, 4:56:29 PM8/4/23
to
On 2023-08-04, Chris Elvidge <ch...@mshome.net> wrote:
> Yes, nice, but you lose a line on the terminal (but who cares?). If you
> use Esc]2; you can update the terminal title. However, no highlights.

Yes; putting stuff into the title is an old trick.

That stuff is far away from where my attention is, usually close
to the prompt.

Secondly, it's sometimes occupied. Right now I'm in screen session; when
you swap screens, screen updates the title.
(Though, that doesn't mean we can't clobber it, too.)

> Should PROMPT_COMMAND= be PROMPT_COMMAND[0]= for modernity?

I guess so; if there is a stack/list of them now, we want to
just be one entry. But probably [0] is already occupied?
We want to add to the array, not clobber an arbitrary index.

Kaz Kylheku

unread,
Aug 4, 2023, 11:30:46 PM8/4/23
to
On 2023-08-03, Kaz Kylheku <864-11...@kylheku.com> wrote:
> I "redesigned" my shell prompt yesterday.

I have discovered an issue. When I run the "top" utility from procps-ng,
and then exit, stupidly puts the cursor into the scroll-protected area,
for whatever reason.

I implemented a countermeasure. As the last interaction with the
terminal, after we print the status line, and restore the cursor
position, we ask the terminal the X, Y position. If it is beyond the
last line, then we move the cursor to the first colum of the last line.

To read the X, Y position, we issue ESC[6h, and then parse the response
which is like ESC[<y>;<x>]R. Bash's read function lets us do this
nicely: read -s -d R <var>. Read until the delimiter R, silently
without echoing.

I will probably optimize things. Now that we are getting the ecurrent
position, we don't have to use the ESC 7 and ESC 8 trick for saving
and restoring position. We can just get the position once,
do the status line and then before restoring the position, check
for a valid line number.

For now it's like this:

old_cmdno=${old_cmdno-0}
old_lines=${old_lines-0}
old_cols=${old_cols-0}

prepare_terminal()
{
stty rows $((LINES - 1))
printf "\n\033[1A"
old_lines=$LINES
old_cols=$COLUMNS
}

get_current_line()
{
local esc=$(printf "\033")
local response
printf "$esc[6n" > /dev/tty
read -s -d R response < /dev/tty
local IFS="[;"
set -- $response
printf "%s\n" "$2"
}

update_status_line()
{
local exit=$?
local getcmdno='\#'
local cmdno=${getcmdno@P}
local esc=$(printf "\033")
local pwd=$PWD
local dots=

[ $LINES -eq $old_lines -a $COLUMNS -eq $old_cols ] || prepare_terminal

local status_esc="$esc[7m$esc[m"

while true; do
local status="$esc[7m$(date +%m-%d/%H:%M)$esc[m $HOSTNAME $dots$pwd"
local status_len=$((${#status} - ${#status_esc}))
[ $status_len -le $COLUMNS ] && break
pwd=${pwd#/}
pwd=/${pwd#*/}
dots='...'
[ "${pwd#/*/}" == "$pwd" ] && break
done

status_len=$((${#status} - ${#status_esc}))

[ $status_len -gt $COLUMNS ] && status=

printf "${esc}7$esc[%s;1H$esc[K%s$esc[1;%sr${esc}8" $((LINES + 1)) "$status" $LINES
if [ $exit -ne 0 -a $cmdno -ne $old_cmdno ] ; then
printf "!%s!\n" $exit
fi

local curln=$(get_current_line)

if [ $curln > $LINES ]; then
printf "$esc[%s;1H" $LINES
fi

old_cmdno=$cmdno
}

PROMPT_COMMAND='update_status_line'

Kaz Kylheku

unread,
Aug 4, 2023, 11:33:48 PM8/4/23
to
On 2023-08-05, Kaz Kylheku <864-11...@kylheku.com> wrote:
> On 2023-08-03, Kaz Kylheku <864-11...@kylheku.com> wrote:
>> I "redesigned" my shell prompt yesterday.
>
> I have discovered an issue. When I run the "top" utility from procps-ng,
> and then exit, stupidly puts the cursor into the scroll-protected area,
> for whatever reason.

The fix has the unexpected tiny benefit that when you quit top, the top
output left on the screen no longer scrolls up by one line The cursor is
placed over the last line of the top output, and you can still see the
first line of the display.

Normally, top occupies the entire screen. When you quit, it puts the
cursor on the next line after that, so the display scrolls by one line.

Now it's putting the cursor into the status line which we adjust to the
previous line, and so no scrolling takes place.

Kaz Kylheku

unread,
Aug 5, 2023, 1:51:04 AM8/5/23
to
On 2023-08-05, Kaz Kylheku <864-11...@kylheku.com> wrote:
> if [ $curln > $LINES ]; then

You want to make this $curln -gt $LINES, if you don't want to keep
creating a file called 52 (or however many lines you have).

Kaz Kylheku

unread,
Aug 5, 2023, 2:28:42 AM8/5/23
to
On 2023-08-03, Kaz Kylheku <864-11...@kylheku.com> wrote:
> I "redesigned" my shell prompt yesterday.

This now lives here:

https://www.kylheku.com/cgit/kabapro/tree/bash-prompt.sh

Kaz Kylheku

unread,
Aug 5, 2023, 4:10:33 AM8/5/23
to
On 2023-08-03, Kaz Kylheku <864-11...@kylheku.com> wrote:
> I "redesigned" my shell prompt yesterday.

LOL, I got the clock in the status line to update spontaneously.

$ trap update_status_line ALRM
$ while true; do kill -ALRM $$ ; sleep 15 ; done &
[1] 19410

Background process sends SIGALRM every 15 seconds; status
line updates.

Janis Papanagnou

unread,
Aug 5, 2023, 6:31:20 AM8/5/23
to
On 05.08.2023 10:10, Kaz Kylheku wrote:
> On 2023-08-03, Kaz Kylheku <864-11...@kylheku.com> wrote:
>> I "redesigned" my shell prompt yesterday.
>
> LOL, I got the clock in the status line to update spontaneously.
>
> $ trap update_status_line ALRM
> $ while true; do kill -ALRM $$ ; sleep 15 ; done &
> [1] 19410
>
> Background process sends SIGALRM every 15 seconds; status
> line updates.

I think this extension introduced a bug; I started a bash shell,
sourced the script, exited the bash instance, and on a new start
and sourcing I get every 15s (even after leaving bash completely)
./bash-prompt-V0: line 73: kill: (4118) - No such process
I got rid by the timer background process only after a 'pkill bash'
(couldn't identify the actual PID under my own or init's UID).

I suppose it needs at least a process cleanup before termination?

Janis

PS: Does bash support (as known from ksh) process bound timers?

Janis Papanagnou

unread,
Aug 5, 2023, 9:21:56 AM8/5/23
to
On 03.08.2023 20:31, Kaz Kylheku wrote:
> I "redesigned" my shell prompt yesterday.
> [...]

Yet another question...

> printf "${esc}7$esc[%s;1H$esc[K%s$esc[1;%sr${esc}8" $((LINES + 1)) "$status" $LINES

What does $esc[1;%sr (with $LINES expanded for %s) do?
Say, \033[1;24r
(It seems I cannot find the ANSI control character 'r'.)

Janis

Janis Papanagnou

unread,
Aug 5, 2023, 10:10:22 AM8/5/23
to
On 04.08.2023 19:26, Kaz Kylheku wrote:
>
> Thanks for trying it; if you get it working 100%, you will like it,
> I'm sure. The idea is to set the prompt to just PS=1'\$ ' which
> is clean and refreshing, and feels like old Unix. All the junk
> like showing the date, time, host name and current directory, goes into
> a scroll-protected status line at the bottom.

I can already say that I like the idea! Formerly we already had "all
the junk" in a separate line by adding a \n before the $ prompt. The
fixed (and protected) status line is certainly a step forward. When
(or if) I write my ksh variant - bash is not my standard shell -, I
want to make further changes; the attributes I like to be displayed
are of course different, but I also prefer to change the cursor
position for the command input to be constantly at the bottom of the
screen. - The short time special case that the first commands are
entered at the top of the screen and only if the screen gets filled
it moves downwards until it permanently reaches the bottom of the
screen is somewhat strange if you think about it; after the first
or second command entered my screen is anyway full and the cursor
at the bottom, so why not keep it static where it usually is. For
my taste that would be another little step forwards.

I want to strive to this layout...

+-----
|
| Scroll area (Input and Output)
|
:
:
|
| Prompt (Input)
| Status (scroll protected)
+-----

(That faintly reminds me the old mainframe times.)

Janis

Janis Papanagnou

unread,
Aug 5, 2023, 10:22:49 AM8/5/23
to
I suppose I found the answer...

| The basics are: configure your device's shell to set scroll
| region once the command is accepted from user (equivalent of
| printf "\033[1,24r") -- and reset it once it completes --
| printf "\033[r".
[Stackexchange]

...presuming the differences (';' vs. ',') are not changing semantics.

>
> Janis
>

Kaz Kylheku

unread,
Aug 5, 2023, 11:36:53 AM8/5/23
to
On 2023-08-05, Janis Papanagnou <janis_pap...@hotmail.com> wrote:
> On 05.08.2023 10:10, Kaz Kylheku wrote:
>> On 2023-08-03, Kaz Kylheku <864-11...@kylheku.com> wrote:
>>> I "redesigned" my shell prompt yesterday.
>>
>> LOL, I got the clock in the status line to update spontaneously.
>>
>> $ trap update_status_line ALRM
>> $ while true; do kill -ALRM $$ ; sleep 15 ; done &
>> [1] 19410
>>
>> Background process sends SIGALRM every 15 seconds; status
>> line updates.
>
> I think this extension introduced a bug; I started a bash shell,
> sourced the script, exited the bash instance, and on a new start
> and sourcing I get every 15s (even after leaving bash completely)
> ./bash-prompt-V0: line 73: kill: (4118) - No such process

I looked for this problem proactively but didn't see it! Looks
like it has to be handled. A simple hack would be to terminate if
the kill fails. The ugly error should go to /dev/null.

I noticed another problem. In a fresh login shell, if I do a reverse
search with Ctrl-R for something, and cancel the search with Ctrl-C,
the background process dies. Orphaning the process so that it becomes
the child of PID 1 might fix it. It needs to be removed from the
TTY session so that it doesn't get signals.

Kaz Kylheku

unread,
Aug 5, 2023, 11:40:52 AM8/5/23
to
On 2023-08-05, Janis Papanagnou <janis_pap...@hotmail.com> wrote:
The documented syntax requires a semicolon. If it works with the
comma, it's some exstension; it doesn't on the terminal I'm using.

Kaz Kylheku

unread,
Aug 5, 2023, 11:45:24 AM8/5/23
to
On 2023-08-05, Janis Papanagnou <janis_pap...@hotmail.com> wrote:
> On 04.08.2023 19:26, Kaz Kylheku wrote:
>>
>> Thanks for trying it; if you get it working 100%, you will like it,
>> I'm sure. The idea is to set the prompt to just PS=1'\$ ' which
>> is clean and refreshing, and feels like old Unix. All the junk
>> like showing the date, time, host name and current directory, goes into
>> a scroll-protected status line at the bottom.
>
> I can already say that I like the idea! Formerly we already had "all
> the junk" in a separate line by adding a \n before the $ prompt. The
> fixed (and protected) status line is certainly a step forward.

I like being able to cut and paste commands in examples without
the chore of editing out the long prompts down to '$'.

> (or if) I write my ksh variant - bash is not my standard shell -, I
> want to make further changes; the attributes I like to be displayed
> are of course different, but I also prefer to change the cursor
> position for the command input to be constantly at the bottom of the
> screen.

The logic is already there; it's executed when the cursor intrudes
into the status line.

We just have to execute that logic whenever the prior cursor position is
not exactly one line before the status line.

Maybe just executing the move unconditionally would also work,
in which case saving the previous position can be dropped.

Janis Papanagnou

unread,
Aug 5, 2023, 7:47:02 PM8/5/23
to
On 05.08.2023 17:40, Kaz Kylheku wrote:
> On 2023-08-05, Janis Papanagnou <janis_pap...@hotmail.com> wrote:
>> On 05.08.2023 15:21, Janis Papanagnou wrote:
>> [ANSI escape codes, Stackexchange quote]
>>
>> ...presuming the differences (';' vs. ',') are not changing semantics.
>
> The documented syntax requires a semicolon. If it works with the
> comma, it's some exstension; it doesn't on the terminal I'm using.

If it doesn't work it was probably just a typo of that poster.

Janis

Kaz Kylheku

unread,
Aug 5, 2023, 9:58:18 PM8/5/23
to
On 2023-08-05, Kaz Kylheku <864-11...@kylheku.com> wrote:
Renamed. It's not a prompt but a status line.
So it's called Basta! (BAsh STAtus).

https://www.kylheku.com/cgit/basta/tree/

Janis Papanagnou

unread,
Aug 7, 2023, 9:57:31 PM8/7/23
to
On 05.08.2023 17:45, Kaz Kylheku wrote:
> On 2023-08-05, Janis Papanagnou <janis_pap...@hotmail.com> wrote:
> [...]
>> (or if) I write my ksh variant - bash is not my standard shell -, I
>> want to make further changes; the attributes I like to be displayed
>> are of course different, but I also prefer to change the cursor
>> position for the command input to be constantly at the bottom of the
>> screen.
>
> The logic is already there; it's executed when the cursor intrudes
> into the status line.
>
> We just have to execute that logic whenever the prior cursor position is
> not exactly one line before the status line.
>
> Maybe just executing the move unconditionally would also work,
> in which case saving the previous position can be dropped.

I've written some experimental code using some Kornshell mechanisms,
so it differs from yours. (I've adopted/adapted your name though; I
like "basta" and named mine accordingly "kosta", kornshell status :-)
One thing I used (that I think doesn't exist in bash) are discipline
functions; I use them to update changes in rows/columns, and trigger
prompt PS1 access. And I have a few own details. Yet I've a lot to
add to that basic frame (path shortening, coloring, maybe composing
status lines, ...), it certainly can be shortened, and needs testing.
But I'm anyway just playing around with your status line idea...

Janis


function LINES.set { .kosta.rows=${.sh.value} ; .kosta.update ;}
function COLUMNS.set { .kosta.cols=${.sh.value} ;}
function PS1.get { .kosta.compose ;}

namespace kosta
{
typeset -i rows=${LINES} cols=${COLUMNS}
typeset -i prompt_line=$((rows-1))
typeset -i status_line=$((rows))

function update
{
prompt_line=$((rows-1))
status_line=$((rows))
}

function compose
{
prev_rc=$?
PS1='(!)$ '
status="${USER}@${HOSTNAME}:${PWD/${HOME}\//\~/} ${SHLVL_P}"

printf '\E[%d;1H\E[K' status_line # goto(row,column);
clear-to-eol;
if (( prev_rc > 255 )) # signal
then printf 'SIG %d (%s)! ' prev_rc-256 $( kill -l $((prev_rc)) )
elif (( prev_rc > 0 )) # error
then printf 'ERR %d! ' prev_rc
fi
printf '%s' "${status}"
printf '\E[1;%dr' prompt_line # scroll-region(from,to);
printf '\E[%d;1H' prompt_line # goto(row,column);
}

} # namespace kosta

Kaz Kylheku

unread,
Aug 8, 2023, 1:35:12 AM8/8/23
to
On 2023-08-08, Janis Papanagnou <janis_pap...@hotmail.com> wrote:
> On 05.08.2023 17:45, Kaz Kylheku wrote:
>> On 2023-08-05, Janis Papanagnou <janis_pap...@hotmail.com> wrote:
>> [...]
>>> (or if) I write my ksh variant - bash is not my standard shell -, I
>>> want to make further changes; the attributes I like to be displayed
>>> are of course different, but I also prefer to change the cursor
>>> position for the command input to be constantly at the bottom of the
>>> screen.
>>
>> The logic is already there; it's executed when the cursor intrudes
>> into the status line.
>>
>> We just have to execute that logic whenever the prior cursor position is
>> not exactly one line before the status line.
>>
>> Maybe just executing the move unconditionally would also work,
>> in which case saving the previous position can be dropped.
>
> I've written some experimental code using some Kornshell mechanisms,
> so it differs from yours. (I've adopted/adapted your name though; I
> like "basta" and named mine accordingly "kosta", kornshell status :-)

Great!

See, you can still innovate in the area of 1970's ECMA/ANSI terminal
control combined with 1980's Unix shells.

Kaz Kylheku

unread,
Aug 9, 2023, 3:25:45 PM8/9/23
to
On 2023-08-06, Kaz Kylheku <864-11...@kylheku.com> wrote:
> On 2023-08-05, Kaz Kylheku <864-11...@kylheku.com> wrote:
>> On 2023-08-03, Kaz Kylheku <864-11...@kylheku.com> wrote:
>>> I "redesigned" my shell prompt yesterday.
>>
>> This now lives here:
>>
>> https://www.kylheku.com/cgit/kabapro/tree/bash-prompt.sh
>
> Renamed. It's not a prompt but a status line.
> So it's called Basta! (BAsh STAtus).
>
> https://www.kylheku.com/cgit/basta/tree/

I'm seeing a weird glitch with Gnome terminal.

I have multiple terminal tabs open, each running a Bash with a Basta
status line.

When I switch from one tab to another, I see a spontaneous cursor
movement and scrolling a few seconds later.

What is happening is that the value of $LINES has spontaneously changed.
The basta.update_status function notices this and handles it like a
resize.

No resize has taken place; I'm just switching tabs.

This is some Gnome Terminal B.S. Probably, whenever a tab is selected to
be current, it is sending a SIGWINCH to the TTY session.

I'm guessing, they are doing it just in case the application window had
changed since the last time that tab was shown, whether it actually
changed or not.

Anyway, resize will just have to be handled more smoothly, so there
is no glitch.

Another, unrelated issue is that the background signal for updating the
screen can occur in the middle of an command line editing action. The
escape sequences get bungled.

I suspect my latest public changes already have the effect of mostly
mitigating it. I refactored the update function into two smaller ones,
and the trap handler only calls one of the two. That one no longer
interrogates the current position of the cursor, and so it isn't
processing input from the TTY that could be fooled by user input. I'm
keeping an eye on this issue to see how much of a problem it still is,
if any.

Kaz Kylheku

unread,
Aug 9, 2023, 4:14:34 PM8/9/23
to
On 2023-08-09, Kaz Kylheku <864-11...@kylheku.com> wrote:
> Anyway, resize will just have to be handled more smoothly, so there
> is no glitch.

... and I fixed it. Part the fix is to catch the window change
signal: "trap basta.update_status WINCH"!

This way, after a resize, we immediately restore the status line
and the scroll-protected region; the user has no chance to run
a command with the scroll-protection off where their output
then runs into the status line, which then scrolls up.

Janis Papanagnou

unread,
Aug 29, 2023, 7:08:09 PM8/29/23
to
On 08.08.2023 03:57, Janis Papanagnou wrote:
> [...a Kornshell variant of "basta"...]
>
> namespace kosta
> {
> typeset -i rows=${LINES} cols=${COLUMNS}

When installing the module to be sourced during .profile/.kshrc
initialization I noticed that these two shell variables aren't
yet defined at that instance of time (which at least initially
produced some weird output once before the first hit of <Enter>).
Therefore I changed that in the code to

typeset -i rows cols
stty size | read rows cols

> [...]
> } # namespace kosta
>

Janis

Kaz Kylheku

unread,
Aug 29, 2023, 7:42:08 PM8/29/23
to
On 2023-08-29, Janis Papanagnou <janis_pap...@hotmail.com> wrote:
> On 08.08.2023 03:57, Janis Papanagnou wrote:
>> [...a Kornshell variant of "basta"...]
>>
>> namespace kosta
>> {
>> typeset -i rows=${LINES} cols=${COLUMNS}
>
> When installing the module to be sourced during .profile/.kshrc
> initialization I noticed that these two shell variables aren't
> yet defined at that instance of time (which at least initially

Yes. I discovered this too. A while ago, in fact.
Then I forgot about it, so that a bug recently crept in. I
I changed a certain global variable initialization to this:

basta_scroll_lines=${basta_scroll_lines-$LINES}

which left the variable with a blank value when initializing out
of .bashrc!

I pushed out a fix for this just this morning!

However, LINES is available by the Bash calls the prompt hook.
So the bulk of the initialization is done at that time.

Speaking of globals, I initializee all global variables conditionally:

basta_old_cmdno=${basta_old_cmdno-0}
basta_old_lines=${basta_old_lines-0}
basta_old_cols=${basta_old_cols-0}

basta_scroll_lines=${basta_scroll_lines-0}
basta_prev_reserved_rows=${basta_prev_reserved_rows-}

basta_status_alarm_pid=${basta_status_alarm_pid-}

This allows for hot reloading without clobbering the variables.

In Common Lisp there is a defvar macro operator which defines
a variable if it doesn't already exist. If a value is specified
it assigns that value. If the variable already exists, it does
not assign the value. E.g.

(defvar *obj-count* 0)

If you reload the code, it will not reset your *obj-count* to zero.

If you want that you use:

(defparameter *obj-count* 0)

whose second argument is mandatory and always assigned to the
variable.

> typeset -i rows cols
> stty size | read rows cols

I no longer trust the size, except on first invocation.

In the first-time, once-only initialization code that is done by the
prompt hook when it detects that startup, I trust that the $LINES
variable corresponds to the usable area of the terminal, not
necessary the whole thing. I then query the terminal to determine
its size: position the cursor to 999,999 and then inquire about
the current position, to get ther row number and that's the size.
From that, it is inferred whethere or not there is already a protected
status line; we put our status line above any protected status lines.

On subsequent terminal resize operations, the terminal is always
queried for the size, rather than believing that $LINES or the tty
driver has the correct size.

Janis Papanagnou

unread,
Aug 29, 2023, 7:48:51 PM8/29/23
to
On 30.08.2023 01:42, Kaz Kylheku wrote:
> On 2023-08-29, Janis Papanagnou <janis_pap...@hotmail.com> wrote:
> [...]
>> typeset -i rows cols
>> stty size | read rows cols
>
> I no longer trust the size, except on first invocation.

This is only called initially once in my ksh-code, later I use the
discipline function to update the variable on LINES changes:

function LINES.set { .kosta.rows=${.sh.value} ; .kosta.update ;}

(That's Kornshell-specific, of course.)

Janis

> [...]


Kaz Kylheku

unread,
Aug 29, 2023, 8:02:14 PM8/29/23
to
On 2023-08-29, Janis Papanagnou <janis_pap...@hotmail.com> wrote:
> On 30.08.2023 01:42, Kaz Kylheku wrote:
>> On 2023-08-29, Janis Papanagnou <janis_pap...@hotmail.com> wrote:
>> [...]
>>> typeset -i rows cols
>>> stty size | read rows cols
>>
>> I no longer trust the size, except on first invocation.
>
> This is only called initially once in my ksh-code, later I use the
> discipline function to update the variable on LINES changes:
>
> function LINES.set { .kosta.rows=${.sh.value} ; .kosta.update ;}
>
> (That's Kornshell-specific, of course.)

By the way, likewise, my code check $LINES and $COLUMNS to detect
a change:


[ $LINES -eq $basta_old_lines -a \
$COLUMNS -eq $basta_old_cols ] || basta.prepare_term

but beyond that basta.prepare_term function won't rely on $LINES; the
terminal is queried to determine its size, from which we subtract the
number of lines needing to be protected, and set LINES accordingly,
punching that into the TTY with stty.

I found that not trusting LINES eliminated the rare glitches I was still
seeing where the scrolling region got set wrong, clobbering the status.

A good change also was not doing the cursor position check when there is
pending input in the TTY. In Bash we can use the read function to detect
whether input is pending. If input from the user is pending, we can't
be starting a transaction to query the terminal; we would have to read
and discard the user's input to get to the termina's response.

Skipping the check is likely okay because most of the time the cursor
does not stray into the status line.

If the user has run some program that has wrecked the scroll region,
*and* is rapidly typing ahead, then we won't catch the bad cursor
position.

I think I have it debugged close to as well as it can be; I put
almost 30 commits into this thing since I started the git repo.

Janis Papanagnou

unread,
Aug 30, 2023, 6:46:57 AM8/30/23
to
On 30.08.2023 02:02, Kaz Kylheku wrote:
> [...]
> I think I have it debugged close to as well as it can be; I put
> almost 30 commits into this thing since I started the git repo.

It's amazing how many things (including edge and corner cases)
have to be considered to make a piece of software bulletproof,
even for a function that appears so simple at first glance as
a shell status line. :-)

Janis

Janis Papanagnou

unread,
Sep 8, 2023, 10:04:17 AM9/8/23
to
On 03.08.2023 20:31, Kaz Kylheku wrote:
> [...]
> local status="$esc[7m$(date +%m-%d/%H:%M)$esc[m $HOSTNAME $dots$pwd"
> local status_nohl="$(date +%m-%d/%H:%M) $HOSTNAME $dots$pwd"
> [ ${#status_nohl} -le $COLUMNS ] && break
> [...]

One more question; is there any neat way to get the "net" length of a
string that carries ANSI control characters (without duplicating the
code as above)?

Initially I wanted to add [ANSI-escape] colors to my "kosta" variant
(e.g. somthing like <red>errors<blue>shlvl<green>path<yellow>...),
but lacking a good idea I abstained from supporting that feature.

Janis

Richard Harnden

unread,
Sep 8, 2023, 2:13:10 PM9/8/23
to
I was also having problems with printf and ansi/utf8.

Don't have a ksh solution, but a short bit of C ... maybe it's useful.

eg:

$ R_ARROW=$(/bin/echo -en "\xe2\x86\x92")
$ RED=$(/bin/echo -ne "\x1b[38;5;1m")
$ GREEN=$(/bin/echo -ne "\x1b[38;5;2m")
$ BOLD=$(/bin/echo -ne "\x1b[1m")
$ RESET=$(/bin/echo -ne "\x1b[0m")

$ TEST="${GREEN}This is a ${R_ARROW} ${BOLD}${RED}test!${RESET}"

$ echo ${#TEST}
43

It isn't 43 - there are only 17 printable characters, so printf gets
confused:

$ printf "%s%50s%s\n" ">>" "${TEST}" "<<"
>> This is a → test!<<


$ printf "%s%s%s\n" ">>" "$(str_pad "${TEST}" 50)" "<<"
>> This is a → test!<<

or left justifed:
$ printf "%s%s%s\n" ">>" "$(str_pad "${TEST}" -50)" "<<"
>>This is a → test! <<

Anyway ...

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

static int str_length(const char *s)
{
int length = 0;

while ( *s )
{
/* ansi escapes don't use any glyphs */
if ( *s == 0x1b )
{
s++;

while ( *s )
{
if ( islower(*s) )
break;

s++;
}

s++;
continue;
}


if ( (*s & 0xc0) != 0x80 ) /* don't count uft8 continuation
bytes */
length++;

s++;
}

return length;
}

int main(int argc, char *argv[])
{
int pad;
char *endptr;
int len;
int p;

if ( argc != 3 )
{
fprintf(stderr, "Usage: %s <str> <pad>\n", argv[0]);
return EXIT_FAILURE;
}

if ( argc == 2 && ( strcmp(argv[1], "-?") == 0 || strcmp(argv[1],
"--help") == 0 || strcmp(argv[1], "--help") == 0 ) )
{
printf("Usage: %s <str> <pad>\n", argv[0]);
return 0;
}

pad = strtol(argv[2], &endptr, 10);

if ( *endptr != '\0' )
{
fprintf(stderr, "%s: \"%s\" is not a number!\n", argv[0], argv[2]);
return EXIT_FAILURE;
}

if ( abs(pad) > 200 )
{
fprintf(stderr, "%s: pad, %d, is out of range. [-200..200]\n",
argv[0], pad);
return EXIT_FAILURE;
}

len = str_length(argv[1]);

if ( pad == 0 || len >= abs(pad) )
pad = 0;
else
p = abs(pad) - len;

if ( pad > 0 )
printf("%*c", p, ' ');

printf("%s", argv[1]);

if ( pad < 0 )
printf("%*c", p, ' ');

return 0;
}




Kaz Kylheku

unread,
Sep 8, 2023, 10:53:32 PM9/8/23
to
On 2023-09-08, Janis Papanagnou <janis_pap...@hotmail.com> wrote:
> On 03.08.2023 20:31, Kaz Kylheku wrote:
>> [...]
>> local status="$esc[7m$(date +%m-%d/%H:%M)$esc[m $HOSTNAME $dots$pwd"
>> local status_nohl="$(date +%m-%d/%H:%M) $HOSTNAME $dots$pwd"
>> [ ${#status_nohl} -le $COLUMNS ] && break
>> [...]
>
> One more question; is there any neat way to get the "net" length of a
> string that carries ANSI control characters (without duplicating the
> code as above)?

There could be ways, like using a function like $(ansi "$esc[m").

Let's recall that the code is Bash specific.

Bash has dynamically scoped local variables, allowing us to cleanly
control the behavior of functions with flags, without resorting
to blatant global variables.

local ansi_enable=y

such that $(ansi ...) will output its argument, rather than
consume it and output nothing. When the scope terminates
the prior value of ansi_enable is restored automatically.

Now the thing is that I have a code smell: the double invocation
of date! In status_nohl, I'm calling date a second time, only
to get the format for the purposee of counting length.

I would prefer some solution in which we mark up the ANSI stuff
somehow like:

@@$esc [m@@.

For the purposes of rendering the codes, we filter out the expanded
version of @@$esc[m@@ (and everything similar) into just $esc[m.

For the purposes of calculating size, we filter @@$esc[m@@ into
nothing.

Bash should be able to do this ${var/this/that} substitution;
I will look into it.

--
TXR Programming Language: http://nongnu.org/txr
Cygnal: Cygwin Native Application Library: http://kylheku.com/cygnal
Mastodon: @Kazi...@mstdn.ca
NOTE: If you use Google Groups, I don't see you, unless you're whitelisted.

Kaz Kylheku

unread,
Sep 8, 2023, 11:03:26 PM9/8/23
to
On 2023-09-08, Janis Papanagnou <janis_pap...@hotmail.com> wrote:
> On 03.08.2023 20:31, Kaz Kylheku wrote:
>> [...]
>> local status="$esc[7m$(date +%m-%d/%H:%M)$esc[m $HOSTNAME $dots$pwd"
>> local status_nohl="$(date +%m-%d/%H:%M) $HOSTNAME $dots$pwd"
>> [ ${#status_nohl} -le $COLUMNS ] && break
>> [...]
>
> One more question; is there any neat way to get the "net" length of a
> string that carries ANSI control characters (without duplicating the
> code as above)?

In the current version of the code, I see I had abandoned the
above approach. I have only one string which is rendered.
There is still duplication, but only of the escapes:

local status_esc="$esc[7m$esc[m"

while true; do
local status="$esc[7m$(date +%m-%d/%H:%M)$esc[m $HOSTNAME $dots$pwd"
local status_len=$((${#status} - ${#status_esc}))
[ $status_len -le $COLUMNS ] && break
pwd=${pwd#/}
pwd=/${pwd#*/}
dots='...'
[ "${pwd#/*/}" == "$pwd" ] && break
done

status_esc is a string consisting of all the escape material
which occurs in status, so has to be maintained in parallel.

While that sucks, it sucks less than repeating the $(date ...)
and $HOSTNAME and other details.

In the above loop, I should be using a cached sample of the
$(date ...) too.

Janis Papanagnou

unread,
Sep 8, 2023, 11:23:04 PM9/8/23
to
On 08.09.2023 20:13, Richard Harnden wrote:
> On 08/09/2023 15:04, Janis Papanagnou wrote:
>> On 03.08.2023 20:31, Kaz Kylheku wrote:
>>> [...]
>>> local status="$esc[7m$(date +%m-%d/%H:%M)$esc[m $HOSTNAME
>>> $dots$pwd"
>>> local status_nohl="$(date +%m-%d/%H:%M) $HOSTNAME $dots$pwd"
>>> [ ${#status_nohl} -le $COLUMNS ] && break
>>> [...]
>>
>> One more question; is there any neat way to get the "net" length of a
>> string that carries ANSI control characters (without duplicating the
>> code as above)?
>>
>> Initially I wanted to add [ANSI-escape] colors to my "kosta" variant
>> (e.g. somthing like <red>errors<blue>shlvl<green>path<yellow>...),
>> but lacking a good idea I abstained from supporting that feature.
>
> I was also having problems with printf and ansi/utf8.
>
> Don't have a ksh solution, but a short bit of C ... maybe it's useful.
>
> eg:
> [snip samples ]
>
> Anyway ...
> [ snip C-code ]

I'm looking for any ideas or insights, so thanks for your idea and
C-code.

I see that you seem to have used a heuristic, triggering with Esc
and terminating with any lower-case character. - This is (almost)
exactly what I tried in shell with patterns (operating on text
that uses an \E escape literal for Esc)...

plain=${var//\\E\[*([!a-z])[a-z]} ; len=${#plain}

I had used "Esc[" (for my cases) but "Esc]" might also be relevant
for a more general application of the pattern (an extension to both
brackets is straightforward, of course). I had included the brace
because I wasn't sure whether an Esc might occur in other contexts
as well alone. (I wasn't sure, though, whether such patterns are
just a hack or sufficient for all ANSI escapes and not conflicting
with other applications.)

The C-code reminds me that any clumsy solution could also be hidden
by creating a shell built-in from a C-function. (But that relocates
the task just to another place.)

Instead of a C program another option could also be a sed-script.
(Performance should not be an issue here.)

Initially I was hoping for a _standard_ tool (like col(1), a tool
to filter reverse line feeds from input), since shell patterns (or
C-code) appears to me to be a bit bulky. (But probably there isn't
any neat and bulletproof standard tool existing...?)

Janis

Janis Papanagnou

unread,
Sep 8, 2023, 11:39:08 PM9/8/23
to
On 09.09.2023 04:53, Kaz Kylheku wrote:
> On 2023-09-08, Janis Papanagnou <janis_pap...@hotmail.com> wrote:
>> On 03.08.2023 20:31, Kaz Kylheku wrote:
>>> [...]
>>> local status="$esc[7m$(date +%m-%d/%H:%M)$esc[m $HOSTNAME $dots$pwd"
>>> local status_nohl="$(date +%m-%d/%H:%M) $HOSTNAME $dots$pwd"
>>> [ ${#status_nohl} -le $COLUMNS ] && break
>>> [...]
>>
>> One more question; is there any neat way to get the "net" length of a
>> string that carries ANSI control characters (without duplicating the
>> code as above)?
>
> There could be ways, like using a function like $(ansi "$esc[m").

Yes, any neat interface that prevents duplication would be nice.

>
> Let's recall that the code is Bash specific.

I took the code from your bash code for explanation only, but I
have the same issue with my ksh variant; it's a general task
independent from these status line projects.

>
> Bash has dynamically scoped local variables, allowing us to cleanly
> control the behavior of functions with flags, without resorting
> to blatant global variables.
>
> local ansi_enable=y
>
> such that $(ansi ...) will output its argument, rather than
> consume it and output nothing. When the scope terminates
> the prior value of ansi_enable is restored automatically.

The interface question is crucial for a neat implementation.

For ksh, maybe defining a discipline function for a "no_ansi"
variable would be a sensible implementation, say,

no_ansi=${status} ; printf "${no_ansi}"

with function no_ansi.set removing the ANSI escapes using a
variable substitution (or a C-compiled shell built-in)?

Janis

> [...]

Janis Papanagnou

unread,
Sep 8, 2023, 11:47:34 PM9/8/23
to
On 09.09.2023 05:39, Janis Papanagnou wrote:
>
> For ksh, maybe defining a discipline function for a "no_ansi"
> variable would be a sensible implementation, say,
>
> no_ansi=${status} ; printf "${no_ansi}"

no_ansi=${status} ; printf "${#no_ansi}"

Typo; I wanted to get the length of the string for the status line
calculation, but both calls are useful and the logic would anyway
be in the no_ansi.set discipline function.

Hmm.. - pondering also about a type definition with that property
(vs. a variable specific definition).
0 new messages