26

I have a simple echo print out that I've added to my .bashrc:

echo "$(tput setaf 2)Wake up....."
sleep 2s
reset
sleep 2s
echo "$(tput setaf 2)Wake up....."
sleep 2s
reset
echo "$(tput setaf 2)Wake up neo....."
sleep 2s
echo "$(tput setaf 2)The Matrix has you......"
sleep 2s
reset
echo "$(tput setaf 2)Follow the white rabbit......"
sleep 2s
reset
cmatrix

This prints a message to terminal, but I want it look as though it's being typed, with a consistent delay between characters.

3
  • 1
    To be realistic I think you should have random typo thrown in with one or more back spaces to correct. For example whilst typing this comment I had to back space over "through" to correct it to "thrown". It makes it a more difficult question to answer but it makes it more realistic. If characters repeat at a steady pace of 30 CPS or 60 CPS it look less human. Certain keys are typed faster together, whilst other key combinations appear slower. Some sort of pattern matching of key combinations to speed is needed. May 1, 2018 at 3:55
  • 2
    @WinEunuuchs2Unix I don't think this is still SimplySimplified. ;)
    – dessert
    May 1, 2018 at 10:28
  • 1
    See my answer here May 1, 2018 at 23:34

8 Answers 8

28

This does not work with Wayland; if you're using Ubuntu 17.10 and didn't change to using Xorg at login, this solution isn't for you.

You can use xdotool Install xdotool for that. If the delay between the keystrokes should be consistent, it's as simple as that:

xdotool type --delay 100 something

This types something with a delay of 100 milliseconds between each keystroke.


If the delay between the keystrokes should be random, let's say from 100 to 300 milliseconds, things get a bit more complicated:

$ text="some text"
  for ((i=0;i<${#text};i++));
  do
    if [[ "${text:i:1}" == " " ]];
    then
      echo -n "key space";
    else
      echo -n "key ${text:i:1}";
    fi;
  [[ $i < $((${#text}-1)) ]] && echo -n " sleep 0.$(((RANDOM%3)+1)) ";
  done | xdotool -

This for loop goes through every single letter of the string saved in variable text, printing either key <letter> or key space in the case of a space followed by sleep 0. and a random number between 1 and 3 (xdotool's sleep interprets the number as seconds). The whole output of the loop is then piped to xdotool, which prints the letters with the random delay in between. If you want to change the delay just change the (RANDOM%x)+y part, y being the lower and x-1+y the upper limit – for 0.2 to 0.5 seconds it would be (RANDOM%4)+2.

Note that this approach does not print the text, but rather type it exactly like the user would do, synthesizing single keypresses. In consequence the text gets typed into the currently focused window; if you change the focus part of the text will get typed in the newly focused window, which may or may not be what you want. In either case have a look at the other answers here, all of which are brilliant!

0
25

I tried xdotool after reading @dessert's answer but couldn't get it to work for some reason. So I came up with this:

while read line
do
    grep -o . <<<$line | while read a
    do
        sleep 0.1
        echo -n "${a:- }"
    done
    echo
done

Pipe your text into the above code and it will be printed like typed. You can also add randomness by replacing sleep 0.1 with sleep 0.$((RANDOM%3)).

Extended version with fake typos

This version will introduce a fake typo every now and then and correct it:

while read line
do
    # split single characters into lines
    grep -o . <<<$line | while read a
    do
        # short random delay between keystrokes
        sleep 0.$((RANDOM%3))
        # make fake typo every 30th keystroke
        if [[ $((RANDOM%30)) == 1 ]]
        then
            # print random character between a-z
            printf "\\$(printf %o "$((RANDOM%26+97))")"
            # wait a bit and delete it again
            sleep 0.5; echo -ne '\b'; sleep 0.2
        fi
        # output a space, or $a if it is not null
        echo -n "${a:- }"
    done
    echo
done
1
  • I think I would do this instead: while IFS= read -r line; do for (( i = 0; i < ${#line}; i++ )); do sleep 0.1; printf "%s" "${line:i:1}"; done; echo; done (replace ; with newlines and good indentation as necessary). The IFS= read -r and printf "%s" ensure that whitespace and special characters are not treated any differently. And the grep on each line to split into characters is unnecessary - just a for loop over each char in the line is sufficient. May 1, 2018 at 15:57
18

You mention a consistent delay between characters, but if you really want it to look like its being typed, the timing won't be perfectly consistent. To achieve this you can record your own typing with the script command and play it back with scriptreplay:

$ script -t -c "sed d" script.out 2> script.timing
Script started, file is script.out
Wake up ...
Wake up ...
Wake up Neo ...
Script done, file is script.out
$ 
$ scriptreplay script.timing script.out
Wake up ...
Wake up ...
Wake up Neo ...

$ 

Recording is stopped by hitting CTRL-D.

Passing the -t parameter to script instructs it also generate timing information, which I have redirected to the script.timing file. I have passed sed d as a command to script as this is simply a way to absorb input (and this record the keystrokes) with no side-effects.

If you want to do all the tput/reset stuff too, you might want to do a script recording for each of your lines, and play them back, interleaved with the tput/reset commands.

11

Another possibility is using Demo Magic, or, to be more precise just the print function of this script collection, which basically amounts to

#!/bin/bash

. ./demo-magic.sh -w2

p "this will look as if typed"

Under the hood, this uses pv, which of course you can also use to directly get the desired effect, the basic form looks as follows:

echo "this will look as if typed" | pv -qL 20
2
  • 1
    This usage of pv is just great. Apr 30, 2018 at 20:03
  • 3
    No need to pipe echo to pv, just use pv -qL20 <<< "Hello world" if your shell supports herestrings.
    – dr_
    May 1, 2018 at 8:43
8

In line with my nickname I can offer another solution:

echo "something" | 
    perl \
        -MTime::HiRes=usleep \
        -F'' \
        -e 'BEGIN {$|=1} for (@F) { print; usleep(100_000+rand(200_000)) }'

Looks weird, doesn't it?

  • -MTime::HiRes=usleep imports the function usleep (microsecond sleep) from the Time::HiRes module because the usual sleep accepts only integer seconds.
  • -F'' splits the given input into characters (the delimiter being empty '') and puts the characters in the array @F.
  • BEGIN {$|=1} disables output buffering so each character is immediatly printed.
  • for (@F) { print; usleep(100_000+rand(200_000)) } just iterates over the characters
  • putting underscores in numbers is a common way to use some kind of thousands separators in Perl. They are simply ignored by Perl, so we can e.g. write 1_000 (==1000) or even 1_0_00 if we consider that easier to read.
  • rand() returns a random number between 0 and the given argument, so together this sleeps between 100,000 and 299,999 microseconds (0.1-0.3 seconds).
5
  • Just out of curiosity: Does rand() return a number from 0 to the argument (100k to 300k in your example) or between them (100k+1 to 300k-1 in your example)?
    – dessert
    Apr 30, 2018 at 18:44
  • 1
    It returns a number in the interval [0,200k), i.e. including 0 but excluding 200k. The exact behaviour is documented here: "Returns a random fractional number greater than or equal to 0 and less than the value of EXPR. (EXPR should be positive.)"
    – PerlDuck
    Apr 30, 2018 at 18:46
  • 1
    This doesn't work without -a and -n
    – rrauenza
    May 2, 2018 at 17:35
  • @rrauenza Since Perl 5.20 it does. -F implies -a and -a implies -n.
    – PerlDuck
    May 2, 2018 at 18:15
  • Ah, ok I was using 5.16, which is what is on CentOS7.
    – rrauenza
    May 2, 2018 at 18:41
6

Another tool that might work, which does not depend on x11 or whatever, is asciicinema. It records everything you do in your terminal and lets you replay that as if it were a screen capture, only then it's purely ascii based! You might have to temporarily disable your prompt however for it to be purely visually clean. As other have pointed out, adding a consistent delay will not look natural, and typing it yourself might be one of the most natural looks you can achieve.

After having recorded the text, you can do something like:

$ asciinema play [your recording].cast; cmatrix
6

I'm surprised nobody's mentioned this yet, but you can accomplish this with stock tools and a loop:

typeit() {
    local IFS=''
    while read -n1 c; do
        echo -n "$c"
        sleep .1
    done <<< "$1"
}

It just loops over the input character by character and prints them out with a delay after each. The only tricky bit is that you have to set your IFS to an empty string so bash doesn't try to split away your spaces.

This solution is dead-simple, so adding variable delays between characters, typos, whatever is super easy.

EDIT (thanks, @dessert): If you want a slightly more natural interface, you could instead do

typeit() {
    local IFS=''
    while read -n1 c; do
        echo -n "$c"
        sleep .1
    done <<< "$@"
}

This would allow you to call the function as typeit foo bar rather than typeit 'foo bar'. Be aware that without quotes, the arguments are subject to bash's word splitting, so for example typeit foo<space><space>bar will print foo<space>bar. To preserve whitespace, use quotes.

2
  • Good suggestion, though it should be noted that word-splitting applies. For example, typeit foo<space>bar will result in foo bar, whereas typeit foo<space><space>bar will also result in foo bar. You've got to quote it to make sure it's verbatim. @dessert feel free to suggest an edit. I can do it myself, but I want to give you the chance to get credit for it. May 1, 2018 at 15:12
  • +1 for teaching me about read -n1 (which, btw. is read -k1 in zsh) May 2, 2018 at 14:58
5

First, "look as though it's being typed, with a consistent delay between characters..." is a little contradictory, as others have pointed out. Something being typed doesn't have a consistent delay. When you see something produced with an inconsistent delay, you'll get chills. "What's taken over my computer!!!??!?"

Anyway...

I have to make a shout out to expect, which should be available on most Linux distributions. Old School, I know, but- assuming it's installed- it could hardly be simpler:

echo 'set send_human {.1 .3 1 .05 2}; send -h "The Matrix has you......\n"' | expect -f /dev/stdin

From the man page:

The -h flag forces output to be sent (somewhat) like a human actually typing. Human-like delays appear between the characters. (The algorithm is based upon a Weibull distribution, with modifications to suit this particular application.) This output is controlled by the value of the variable "send_human"...

See https://www.tcl.tk/man/expect5.31/expect.1.html

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.