Making interactive user inputs

786 views
Skip to first unread message

Brad Wood

unread,
Dec 26, 2017, 2:17:41 PM12/26/17
to jline-users
Ok, so this is something I've wanted to do for a couple years with my use of JLine in CommandBox CLI, but with the troubles I had just making a simple progress bar, I had avoided attempting this.  Now that I'm on JLine3, I wanted to check and see what's built in (if anything) or any guides, examples, or pointers on what classes to use.

See this npm library for examples of what I'm talking about:

I want to interact with users via the command with radio button or checkbox-style inputs that redraw in place to allow the user to navigate options with arrow keys and use the spacebar/enter or something similar to select options prior to submitting them.  This would be great for command line installers, wizards, or for general task scripting.  The trick has always been how I redraw the screen to update as they manipulate the "form" controls.  
[?] What do you want to do?
 > Order a pizza
   Make a reservation
   --------
   Ask opening hours
   Talk to the receptionist
In Jline2, I couldn't even get a consistent way to detect something as simple as up/down/left/right arrows!  

I was excited to see that the completor stuff in JLine3 is very fancy and already incorporates many of the same behaviors that I've been wanting to do for so long.  

Any input is appreciated on where to attack this from:
  • How do I consistently detect control keys like arrow keys?
  • How do I redraw several lines of the terminal over and over and potentially also keep the cursor on a line above?
  • Is there anything built in to JLine at this point for larger interactions like this? (outside of the tab-completion stuff)
Thanks!

~Brad

Brad Wood

unread,
Dec 26, 2017, 2:25:43 PM12/26/17
to jline-users
Here's another npm lib that has some better examples of the stuff I'm looking to do:

I'd also love to know how they get the little circles that can be filled in.  I've had horrible luck using any sort of Unicode characters at all.  Heck, any ASCII chars over 128 have given me troubles displaying consistently.  

Guillaume Nodet

unread,
Dec 26, 2017, 3:16:01 PM12/26/17
to jline...@googlegroups.com
On Tue, Dec 26, 2017 at 8:17 PM, Brad Wood <bdw...@gmail.com> wrote:
Ok, so this is something I've wanted to do for a couple years with my use of JLine in CommandBox CLI, but with the troubles I had just making a simple progress bar, I had avoided attempting this.  Now that I'm on JLine3, I wanted to check and see what's built in (if anything) or any guides, examples, or pointers on what classes to use.

See this npm library for examples of what I'm talking about:

I want to interact with users via the command with radio button or checkbox-style inputs that redraw in place to allow the user to navigate options with arrow keys and use the spacebar/enter or something similar to select options prior to submitting them.  This would be great for command line installers, wizards, or for general task scripting.  The trick has always been how I redraw the screen to update as they manipulate the "form" controls.  
[?] What do you want to do?
 > Order a pizza
   Make a reservation
   --------
   Ask opening hours
   Talk to the receptionist
In Jline2, I couldn't even get a consistent way to detect something as simple as up/down/left/right arrows!  

I was excited to see that the completor stuff in JLine3 is very fancy and already incorporates many of the same behaviors that I've been wanting to do for so long.  

Any input is appreciated on where to attack this from:
  • How do I consistently detect control keys like arrow keys?
As I explained in the jline2 issue, JLine does not read key presses.  This concept only exists on windows, while JLine is more inspired of the unix world. So the input is a byte stream which can be decoded in keys: standard keys are transferred as is, while others such as arrows are encoded.
You need to use the BindingReader / KeyMap class.
 
  • How do I redraw several lines of the terminal over and over and potentially also keep the cursor on a line above?
The Display class is what you're looking for. 
  • Is there anything built in to JLine at this point for larger interactions like this? (outside of the tab-completion stuff)
Yes, the three above classes ;-)
Have a look at the ttop example.  It's a small fullscreen app similar to the "top" unix command:
The same package contains other similar apps like nano / tmux.
The LineReaderImpl also use those 3 classes, in a non fullscreen mode.

Using those 3 classes may not be the simpliest thing, but it will give you a lot of power.
 
Thanks!

~Brad

--
You received this message because you are subscribed to the Google Groups "jline-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jline-users+unsubscribe@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Brad Wood

unread,
Dec 26, 2017, 4:04:19 PM12/26/17
to jline...@googlegroups.com
As I explained in the jline2 issue, JLine does not read key presses. 

Ahh, yes - this is the long conversation we had in the JLine2 ticket over again.  We seemed to have some philosophical differences and I was hoping you had softened your stance.  I felt back then, and still do today, that JLine would benefit immensely from an out-of-the box abstraction that "just works" on any OS to allow users of the JLine library to capture individual keystrokes/bindings-- even if it takes more than one byte being read from the input stream behind the scenes.  This just feels like basic keyboard interactions.

Is the code sample you provided in the Jline2 ticket still the only way for me to do this?  I'm still unclear on whether that was code that would only work on Windows, or if it would have worked on all operating systems.  I do see that the BindingReader class looks to be new in JLine3 so I'm guessing that Jline2 version is outdated now.  

Is there anyway to just get JLine to use the internal Keymap it's already using?  Like with the Jline2 examples, I still don't understand why I need to create an key map that handles the codes for things like up arrow, when Jline already does this seamlessly in all other areas (for instance, up arrow works on any OS for displaying the history).  It seems JLine already has a keymap constructed that it's using internally.  Can't I just have it use that one?

The Display class is what you're looking for. 

Thanks.  I'll check out those examples you linked to.  

Thanks!

~Brad

Developer Advocate
Ortus Solutions, Corp 

ColdBox Platform: http://www.coldbox.org 

Guillaume Nodet

unread,
Dec 26, 2017, 5:39:46 PM12/26/17
to jline...@googlegroups.com
On Tue, Dec 26, 2017 at 10:03 PM, Brad Wood <bdw...@gmail.com> wrote:
As I explained in the jline2 issue, JLine does not read key presses. 

Ahh, yes - this is the long conversation we had in the JLine2 ticket over again.  We seemed to have some philosophical differences and I was hoping you had softened your stance.  I felt back then, and still do today, that JLine would benefit immensely from an out-of-the box abstraction that "just works" on any OS to allow users of the JLine library to capture individual keystrokes/bindings-- even if it takes more than one byte being read from the input stream behind the scenes.  This just feels like basic keyboard interactions.

Yeah, but you're missing a piece which is the way the terminal works in the real world.  Think about ssh / telnet, the keyboard is on the remote side, and virtual key codes are *not* transmitted.  Sorry, but it's just the way it works... ;-)  
Now, using the BindingReader / KeyMap, it's easy to convert the char sequence back to a virtual key code if you really want to.  Though windows virtual key codes are not sufficient and you also need to encode the key modifiers (control, shift, alt...). You can define your own encoding if you want.

So what JLine provides is a simple abstraction to read bindings from the input character stream.  But I can't decide what those bindings are for you, so you simply need to write this mapping into a KeyMap and then read them.  If you want to handle the 4 arrows and that's all, it would be 5 lines to define the keymap.  I don't think it's really complicated:

KeyMap<String> keys = new KeyMap<>();
keys.setUnicode("self-insert");
keys.setNomatch("self-insert");
EnumSet.of(Capability.key_up, Capability.key_down, Capability.key_left, Capability.key_right).forEach(c -> keys.bind(c.name(), KeyMap.key(terminal, c))); 

Is the code sample you provided in the Jline2 ticket still the only way for me to do this?  I'm still unclear on whether that was code that would only work on Windows, or if it would have worked on all operating systems.  I do see that the BindingReader class looks to be new in JLine3 so I'm guessing that Jline2 version is outdated now.  

Is there anyway to just get JLine to use the internal Keymap it's already using?  Like with the Jline2 examples, I still don't understand why I need to create an key map that handles the codes for things like up arrow, when Jline already does this seamlessly in all other areas (for instance, up arrow works on any OS for displaying the history).  It seems JLine already has a keymap constructed that it's using internally.  Can't I just have it use that one?

I'm really not sure to understand.  The KeyMap / BindingReader allows you to decode "actions" from the input.  The LineReader uses different keymaps, but they all convert a char sequence into a named widget.  I'm really not sure why you would want to obtain VI_MATCH_BRACKET when the user hits ^X^B in your case.  You need to make the translation from key sequence to action somewhere in your code.

Brad Wood

unread,
Dec 27, 2017, 2:56:39 PM12/27/17
to jline...@googlegroups.com
How do I capture Ctrl-C when the bindingReader.readBinding() method is running?  It seems the signal handlers aren't kicking in and if I try to bind ctrl('C') it doesn't fire.  

I think the use case I'm seeing here that you're missing is an out-of-the box key map that comes pre-loaded with mappings for all the physical keys on my keyboard.  What you've provided is extremely flexible, but it's absolute overkill for a run of the mill "what key did my user just press" scenario where I don't care about fancy key combinations. I've replicated what I need but it should never ever have been this much work.  I basically had to just do a few hours of trial and error to get basic key mapping up and running.  I would expect some pre-built keyboard mapping to contain these:  

// left, right, up, down arrow
keys.bind( capability.key_left.name(), keys.key( terminal, capability.key_left ) );
keys.bind( capability.key_right.name(), keys.key( terminal, capability.key_right ) );
keys.bind( capability.key_up.name(), keys.key( terminal, capability.key_up ) );
keys.bind( capability.key_down.name(), keys.key( terminal, capability.key_down ) );
// Home/end
keys.bind( capability.key_home.name(), keys.key( terminal, capability.key_home ) );
keys.bind( capability.key_end.name(), keys.key( terminal, capability.key_end ) );
// delete key/delete line/backspace/Insert
keys.bind( capability.key_dc.name(), keys.key( terminal, capability.key_dc ) );
keys.bind( capability.key_dl.name(), keys.key( terminal, capability.key_dl ) );
keys.bind( capability.key_backspace.name(), keys.key( terminal, capability.key_backspace ) );
keys.bind( capability.key_ic.name(), keys.key( terminal, capability.key_ic ) );
// Page up/down
keys.bind( capability.key_npage.name(), keys.key( terminal, capability.key_npage ) );
keys.bind( capability.key_ppage.name(), keys.key( terminal, capability.key_ppage ) );
// Function keys
keys.bind( capability.key_f1.name(), keys.key( terminal, capability.key_f1 ) );
keys.bind( capability.key_f2.name(), keys.key( terminal, capability.key_f2 ) );
keys.bind( capability.key_f3.name(), keys.key( terminal, capability.key_f3 ) );
keys.bind( capability.key_f4.name(), keys.key( terminal, capability.key_f4 ) );
keys.bind( capability.key_f5.name(), keys.key( terminal, capability.key_f5 ) );
keys.bind( capability.key_f6.name(), keys.key( terminal, capability.key_f6 ) );
keys.bind( capability.key_f7.name(), keys.key( terminal, capability.key_f7 ) );
keys.bind( capability.key_f8.name(), keys.key( terminal, capability.key_f8 ) );
keys.bind( capability.key_f9.name(), keys.key( terminal, capability.key_f9 ) );
keys.bind( capability.key_f10.name(), keys.key( terminal, capability.key_f10 ) );
keys.bind( capability.key_f11.name(), keys.key( terminal, capability.key_f11 ) );
keys.bind( capability.key_f12.name(), keys.key( terminal, capability.key_f12 ) );

// This doesn't seem to match anything on Windows
keys.bind( 'delete', keys.del() );
keys.bind( 'escape', keys.esc() );
keys.setAmbiguousTimeout( 50 );

// Everything else
keys.setnomatch( 'self-insert' );
var binding = bindingReader.readBinding( keys );
if( binding == 'self-insert' ) {
key = bindingReader.getLastBinding();
} else {
key = binding;
}

return key;


Now I can call this function when I want to block for the next key that the user presses.  My function will either return one of the special key names above or a single character that represents what the user typed and I can then react accordingly.  

Thanks!

~Brad

Developer Advocate
Ortus Solutions, Corp 

ColdBox Platform: http://www.coldbox.org 


Guillaume Nodet

unread,
Dec 28, 2017, 11:27:45 AM12/28/17
to jline...@googlegroups.com
On Wed, Dec 27, 2017 at 8:56 PM, Brad Wood <bdw...@gmail.com> wrote:
How do I capture Ctrl-C when the bindingReader.readBinding() method is running?  It seems the signal handlers aren't kicking in and if I try to bind ctrl('C') it doesn't fire. 

The BindingReader has nothing to do with signals.  Did you manage to catch any signal at all ?
I understand what you're trying to achieve.  And that's not easy because you don't want to bend into JLine's concepts, so that obviously makes your life more difficult.

For example in your snake example, you have the following:

key = shell.waitForKey();
if( isQuit( key ) ) {
break;
} else if( isLeft( key ) ) {
go( 'left' );
} else if( isRight( key ) ) {
go( 'right' );
} else if( isUp( key ) ) {
go( 'up' );
} else if( isDown( key ) ) {
go( 'down' );
} else if( isRetry( key ) ) {
resetGame();
}


If you were to use JLine KeyMap / BindingReader as it has been designed, you'd have the following:


   enum Action {
     quit, retry, left, right, up, down
   }

   KeyMap<Action> keys = new KeyMap();
   keys.bind( quit, "q", "Q" );
   keys.bind( retry, "r", "R" );
   keys.bind( left, keys.key( terminal, capability.key_left ) );
   keys.bind( right, keys.key( terminal, capability.key_right ) );
   keys.bind( up, keys.key( terminal, capability.key_up ) );
   keys.bind( down, keys.key( terminal, capability.key_down ) );
  
   while (true) {
      key = bindingReader.readBinding(keys);
      switch (key) {
         case quit: return;
         case retry: resetGame(); break;
         default: go(key); break;
      }
   }

So you find it difficult to use just because you don't want to put your actions in the KeyMap, which has been designed for that.  So given you're stepping out of the design, it's difficult to use.

Brad Wood

unread,
Dec 28, 2017, 3:31:21 PM12/28/17
to jline...@googlegroups.com

The BindingReader has nothing to do with signals.  

Sorry for the confusion, I didn't intend to imply that it did.  I was simply saying that no signals seemed to fire and no key bindings were reported.  It was a list of two items that failed to do anything.

Did you manage to catch any signal at all ?

Not that I know of, but then again I haven't put in any code to try and do so.  Do I need to?  I'm used to the Ctrl-C stuff just working automatically when I'm calling readLine().  Do the signals behave differently when calling readBinding()?  


Thanks!

~Brad

Developer Advocate
Ortus Solutions, Corp 

ColdBox Platform: http://www.coldbox.org 


Guillaume Nodet

unread,
Dec 28, 2017, 4:45:08 PM12/28/17
to jline...@googlegroups.com
On Thu, Dec 28, 2017 at 9:30 PM, Brad Wood <bdw...@gmail.com> wrote:

The BindingReader has nothing to do with signals.  

Sorry for the confusion, I didn't intend to imply that it did.  I was simply saying that no signals seemed to fire and no key bindings were reported.  It was a list of two items that failed to do anything.

Did you manage to catch any signal at all ?

Not that I know of, but then again I haven't put in any code to try and do so.  Do I need to?  I'm used to the Ctrl-C stuff just working automatically when I'm calling readLine().  Do the signals behave differently when calling readBinding()?  

So the LineReader will set up some signal handlers to handle the INT signal.
If you want to catch them outside a call to LineReader#readLine(), you'll also need to set up a signal handler on the Terminal in order to catch those.

Brad Wood

unread,
Dec 28, 2017, 5:18:55 PM12/28/17
to jline...@googlegroups.com
Ok, I figured as much.  Can you help provide some direction on how to specify a signal handler?  Do I need to implement my own handler, or can I re-use one that JLine already has?  You seem to be using a lambda expression in the reader class that calls interrupt() on the thread (though I'm still not entirely sure where the actual exception is coming from).  However, a Java lambda doesn't really translate well to what I need to write in CFML.  CFML has closures, but they create a variable that represents a native "function" data type and my (limited) understanding of Java lambdas is that they're closer to an anonymous class instance that overrides a method.  Either way, I'm really not sure what I need to pass into the Terminal.handle() method.  

Thanks!

~Brad

Developer Advocate
Ortus Solutions, Corp 

ColdBox Platform: http://www.coldbox.org 


Guillaume Nodet

unread,
Dec 28, 2017, 5:23:14 PM12/28/17
to jline...@googlegroups.com
Basically, you need to implement the Terminal.SignalHandler interface and pass an instance of it to the #handle() method.

The handler should be called whenever the signal is raised by the terminal.

Brad Wood

unread,
Dec 28, 2017, 6:59:10 PM12/28/17
to jline...@googlegroups.com
Ok, looks like I've got it working.  I created a class that meets the handler interface and registered it in the terminal builder.  It fires when I hit Ctrl-C.   I had to do some extra work to keep track of the original thread that the readBinding() is happening in so I could interrupt it.  

I can definitely confirm that the INT signal is being sent twice for me which made things a bit of a pain.  I'm on Windows 7 using a normal cmd shell. 

This is very nice though.  Now in some of my blocking code like the progressable downloader, I can check and see if the thread has been interrupted after reading every few bytes.  If it has, I reset the interrupt flag and throw an InterruptedException and the shell gracefully cancels the command.  Very cool.  I've wanted to do this for a long time.

Thanks!

~Brad

Developer Advocate
Ortus Solutions, Corp 

ColdBox Platform: http://www.coldbox.org 


Guillaume Nodet

unread,
Dec 29, 2017, 2:06:59 AM12/29/17
to jline...@googlegroups.com
Coule you raise an issue for the double signal please ?

 
Thanks!

~Brad
To unsubscribe from this group and stop receiving emails from it, send an email to jline-users...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "jline-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jline-users...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "jline-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jline-users...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "jline-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jline-users...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "jline-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jline-users...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "jline-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jline-users...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "jline-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jline-users...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "jline-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jline-users...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "jline-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jline-users...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "jline-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jline-users...@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "jline-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jline-users...@googlegroups.com.

Brad Wood

unread,
Dec 29, 2017, 2:16:40 AM12/29/17
to jline...@googlegroups.com

Thanks!

~Brad

Developer Advocate
Ortus Solutions, Corp 

ColdBox Platform: http://www.coldbox.org 


 
Thanks!

~Brad
To unsubscribe from this group and stop receiving emails from it, send an email to jline-users+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "jline-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jline-users+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "jline-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jline-users+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "jline-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jline-users+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "jline-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jline-users+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "jline-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jline-users+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "jline-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jline-users+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "jline-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jline-users+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "jline-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jline-users+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "jline-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jline-users+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "jline-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jline-users+unsubscribe@googlegroups.com.

For more options, visit https://groups.google.com/d/optout.

--
You received this message because you are subscribed to the Google Groups "jline-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to jline-users+unsubscribe@googlegroups.com.

Brad Wood

unread,
Apr 4, 2018, 2:54:30 AM4/4/18
to jline-users
Just to bring this original conversation full circle for anyone who trips across it in the future, I finally got a chance to mess with creating some interactive user controls with the Display class tonight and it worked pretty easily for my first proof of concept.  The following code in CommandBox

var colors = multiselect()
    .setQuestion( 'What is your favorite colors? ' )
    .setOptions( 'Red,Green,Blue,Violet,Pink' )
    .ask();

creates this:
 


You can navigate the options with up/down and tab/shift tab and space bar or the X key toggles a selection.  Here's the code if anyone is curious:


The important parts are the use of the org.jline.utils.Display class.

Thanks!

~Brad
Reply all
Reply to author
Forward
0 new messages