Easiest way to react on cursor keys (arrows) in interactive shell script - shell

I'd like to write a shell script which shall be interactive and react on the use of cursor keys. My current approach seems a little complicated. I would determine the escape sequence for the cursor keys using tput kcud1 etc. and then use read -s -n 1 a to read byte-by-byte, append the byte to a collected string, and then compare the collected string to the determined escape sequences. If one matches, then I can react.
This is problematic because I've got no idea where an escape sequence ends. For instance, tput kcud1 (down arrow) here returns "\eOB". I would use a timeout in order to ignore unknown escape sequences (and probably start over if an escape character arrives). This all doesn't look nice to me.
Isn't there a simpler way of reacting on arrow key usage in a shell script?

Related

How can I make my terminal prompt extend the width of the terminal?

I noticed in this video, that the terminal prompt extends the entire width of the terminal before breaking down to a new line. How can I set my PS1 variable to fill the remaining terminal space with some character, like the way this user did?
The issue is, I don't know how to update the PS1 variable per command. It seems to me, that the string value for PS1 is only read in once just as the .bashrc file is only read in once. Do I have to write some kind of hook after each command or something?
I should also point out, that the PS1 variable will be evaluated to a different length based on the escape characters that make up it. For example, \w print the path.
I know I can get the terminal width using $(COLUMNS), and the width of the current PS1 variable with ${#PS1}, do the math, and print the right amount of buffer characters, but how do I get it to update everytime. Is there a preferred way?
Let's suppose you want your prompt to look something like this:
left text----------------------------------------------------------right text
prompt$
This is pretty straight-forward provided that right text has a known size. (For example, it might be the current date and time.) What we do is to print the right number of dashes (or, for utf-8 terminals, the prettier \u2500), followed by right text, then a carriage return (\r, not a newline) and the left text, which will overwrite the dashes. The only tricky bit is "the right number of dashes", but we can use $(tput cols) to see how wide the terminal is, and fortunately bash will command-expand PS1. So, for example:
PS1='\[$(printf "%*s" $(($(tput cols)-20)) "" | sed "s/ /-/g") \d \t\r\u#\h:\w \]\n\$ '
Here, $(($(tput cols)-20)) is the width of the terminal minus 20, which is based on \d \t being exactly 20 characters wide (including the initial space).
PS1 does not understand utf-8 escapes (\uxxxx), and inserting the appropriate substitution into the sed command involves an annoying embedded quote issue, although it's possible. However, printf does understand utf-8 escapes, so it is easier to produce the sequence of dashes in a different way:
PS1='\[$(printf "\\u2500%.0s" $(seq 21 $(tput cols))) \d \t\r\u#\h:\w \]\n\$ '
Yet another way to do this involves turning off the terminal's autowrap, which is possible if you are using xterm or a terminal emulator which implements the same control codes (or the linux console itself). To disable autowrap, output the sequence ESC[?7l. To turn it back on, use ESC[?7h. With autowrap disabled, once output reaches the end of a line, the last character will just get overwritten with the next character instead of starting a new line. With this technique, it's not really necessary to compute the exact length of the dash sequence; we just need a string of dashes which is longer than any console will be wide, say the following:
DASHES="$(printf '\u2500%0.s' {1..1000})"
PS1='\[\e[?7l\u#\h:\w $DASHES \e[19D \d \t\e[?7h\]\n\$ '
Here, \e[19D is the terminal-emulator code for "move cursor backwards 19 characters". I could have used $(tput cub 19) instead. (There might be a tput parameter for turning autowrap on and off, but I don't know what it would be.)
The example in the video also involves inserting a right-aligned string in the actual command line. I don't know any clean way of doing this with bash; the console in the video is almost certainly using zsh with the RPROMPT feature. Of course, you can output right-aligned prompts in bash, using the same technique as above, but readline won't know anything about them, so as soon as you do something to edit the line, the right prompt will vanish.
Use PROMPT_COMMAND to reset the value of PS1 before each command.
PROMPT_COMMAND=set_prompt
set_prompt () {
PS1=...
}
Although some system script (or you yourself) may already use PROMPT_COMMAND for something, in which case you can simply add to it.
PROMPT_COMMAND="$PROMPT_COMMAND; set_prompt"

Determining escape sequences for cursor keys canonically

I'd like to react on cursor keys; for this I want to find out the escape sequences the current terminal is using in a canonical way. Most solutions just have the strings "\e[A" etc. in the source code. I understand that this is not portable in case the target system uses a different escape sequence.
I tried using tput to get the proper escape sequence but tput kcud1 returns a different escape sequence than what is used for the down arrow key: It returns "\eOB" but the key results in "\e[B". But no capname given to tput results in this string; the best I can manage is tput cud which returns "\e[%p1%dB" from which stripping of all parameters then will finally be the wanted string. But that stripping of parameters also does not sound nice.
What's the canonical way of finding out the proper escape sequence the arrow keys will produce when pressed? I would be happy about a solution using tput or curses or even a completely different approach.
What the cursor keys send depends on the keyboard mode.
tput reports their application mode value but by default, the keyboard is in standard mode.
The DEKCKM command can be used to just change they cursor key behavior, example:
printf "\033[?1h"
or you can use the smkx command which set the keyboard to application mode:
tput smkx
Reset with:
printf "\033[?1l"
or
tput rmkx
I think what you needed is tput cud1 (without the k). You can also get a list of tput commands here:
http://www.tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
Also you can store those sequences on a variable through VAR=$(tput xyz).
And everything is in man terminfo. Online: http://www.manpagez.com/man/5/terminfo/

BASH/Readline Verbatim Null Character

I'm trying to understand exactly who among the set of TTY, kernel, line discipline, and shell actually deals with any given part of my input. In particular, I've been working through this article:
The TTY demystified
My question: if I want to have the actual delete character show up in BASH, I can use ^V to make it verbatim. E.g. I can use Ctrl+V then Ctrl+H to have a backspace character. Pretty neat!
echo '3^H'
3
My question is this: if I'm in something that reads in canonical mode (I believe cat does this) I can put a null character in there by doing Ctrl+V then Ctrl+2 (basically Ctrl+#, which is the caret notation for the null character).
BASH won't allow me to have a verbatim null character on one of its lines, though, and it looks like python and other readline programs won't either.
Does anybody know why this is, or a general-purpose workaround?
The C library uses a literal null as a string terminator, so anything using that is unable to represent strings containing literal nulls.
Programs which need to support literal nulls define their own string data type.

How to escape unicode characters in bash prompt correctly

I have a specific method for my bash prompt, let's say it looks like this:
CHAR="༇ "
my_function="
prompt=\" \[\$CHAR\]\"
echo -e \$prompt"
PS1="\$(${my_function}) \$ "
To explain the above, I'm builidng my bash prompt by executing a function stored in a string, which was a decision made as the result of this question. Let's pretend like it works fine, because it does, except when unicode characters get involved
I am trying to find the proper way to escape a unicode character, because right now it messes with the bash line length. An easy way to test if it's broken is to type a long command, execute it, press CTRL-R and type to find it, and then pressing CTRL-A CTRL-E to jump to the beginning / end of the line. If the text gets garbled then it's not working.
I have tried several things to properly escape the unicode character in the function string, but nothing seems to be working.
Special characters like this work:
COLOR_BLUE=$(tput sgr0 && tput setaf 6)
my_function="
prompt="\\[\$COLOR_BLUE\\] \"
echo -e \$prompt"
Which is the main reason I made the prompt a function string. That escape sequence does NOT mess with the line length, it's just the unicode character.
The \[...\] sequence says to ignore this part of the string completely, which is useful when your prompt contains a zero-length sequence, such as a control sequence which changes the text color or the title bar, say. But in this case, you are printing a character, so the length of it is not zero. Perhaps you could work around this by, say, using a no-op escape sequence to fool Bash into calculating the correct line length, but it sounds like that way lies madness.
The correct solution would be for the line length calculations in Bash to correctly grok UTF-8 (or whichever Unicode encoding it is that you are using). Uhm, have you tried without the \[...\] sequence?
Edit: The following implements the solution I propose in the comments below. The cursor position is saved, then two spaces are printed, outside of \[...\], then the cursor position is restored, and the Unicode character is printed on top of the two spaces. This assumes a fixed font width, with double width for the Unicode character.
PS1='\['"`tput sc`"'\] \['"`tput rc`"'༇ \] \$ '
At least in the OSX Terminal, Bash 3.2.17(1)-release, this passes cursory [sic] testing.
In the interest of transparency and legibility, I have ignored the requirement to have the prompt's functionality inside a function, and the color coding; this just changes the prompt to the character, space, dollar prompt, space. Adapt to suit your somewhat more complex needs.
#tripleee wins it, posting the final solution here because it's a pain to post code in comments:
CHAR="༇"
my_function="
prompt=\" \\[`tput sc`\\] \\[`tput rc`\\]\\[\$CHAR\\] \"
echo -e \$prompt"
PS1="\$(${my_function}) \$ "
The trick as pointed out in #tripleee's link is the use of the commands tput sc and tput rc which save and then restore the cursor position. The code is effectively saving the cursor position, printing two spaces for width, restoring the cursor position to before the spaces, then printing the special character so that the width of the line is from the two spaces, not the character.
(Not the answer to your problem, but some pointers and general experience related to your issue.)
I see the behaviour you describe about cmd-line editing (Ctrl-R, ... Cntrl-A Ctrl-E ...) all the time, even without unicode chars.
At one work-site, I spent the time to figure out the diff between the terminals interpretation of the TERM setting VS the TERM definition used by the OS (well, stty I suppose).
NOW, when I have this problem, I escape out of my current attempt to edit the line, bring the line up again, and then immediately go to the 'vi' mode, which opens the vi editor. (press just the 'v' char, right?). All the ease of use of a full-fledged session of vi; why go with less ;-)?
Looking again at your problem description, when you say
my_function="
prompt=\" \[\$CHAR\]\"
echo -e \$prompt"
That is just a string definition, right? and I'm assuming your simplifying the problem definition by assuming this is the output of your my_function. It seems very likely in the steps of creating the function definition, calling the function AND using the values returned are a lot of opportunities for shell-quoting to not work the way you want it to.
If you edit your question to include the my_function definition, and its complete use (reducing your function to just what is causing the problem), it may be easier for others to help with this too. Finally, do you use set -vx regularly? It can help show how/wnen/what of variable expansions, you may find something there.
Failing all of those, look at Orielly termcap & terminfo. You may need to look at the man page for your local systems stty and related cmds AND you may do well to look for user groups specific to you Linux system (I'm assuming you use a Linux variant).
I hope this helps.

ncurses escape sequence detection

what is the best way to detect escape sequences in ncurses raw mode.
The thing that come to my mind is do getch and then add it to some kind of buffer and then when
the text matches known escape sequence execute appropriate command otherwise ignore the sequence.
However in this algorithm if i hit an escape sequence that system doesn't know about even if a continue typing other characters they will be considered as a part of escape sequence.
Are there any rules of how long the escape sequence can be or some standart characters that end escape sequence
Or is there a way of detecting in ncurses when user stopped typing the characters? like escape sequence usually come as sequence of characters but i have no way to detect the last character because i have a blocking getch that just blocks when there are no characters from input system
like for example if i press page down and c button i have continuous stream of
escape sequence characters ^[[6~ and then character c. how can i detect between those two that the user pressed first escape sequence and then c character if i don't have a predefined set of known escape sequences
You may wish to use libtermkey to parse the key events.
http://www.leonerd.org.uk/code/libtermkey/
Either you can connect it directly to stdin, or feed it bytes as given to you by getch().
As to the general idea of detecting the end; there are certain rules that sequences follow. Sequences starting ESC [ are CSI sequences, and end on the first octet in the range [\x40-\x7e]. There are a few other kinds but they're rare to come from a terminal.
Without a predefined set of escape sequences you will not be able to determine the end of a sequence.
Ncurses uses termcap/terminfo to enumerate the sequences and their meanings.

Resources