Many people I've read about have trouble fetching a key from the keyboard in a terminal.
Normally, CTRL-C stops the terminal. In this case, we don't always get a clean shutdown of the application, which can cause issues.
In my case, I use it to control a multi-track player with the MPG123 music player.
The player I built is started five times with different tracks: Original, Bass, Vocals, Drums, and Others.
CTRL-C doesn't shut down these players cleanly and cuts off the playback.
To catch CTRL-C, I handle the rest in B4J, giving the players time to stop and clean up as needed.
The sample uses inline Java, and no extra libraries are required.
There's also a timer loop in the Java code that prevents the terminal from hanging up if you haven't called stopMessageLoop in your B4J code.
The code works well in a terminal and is very reliable on a Raspberry Pi, though it should also work on other Linux systems.
There’s also a Windows part, but it’s not finished yet and still needs some work.
If you are in Release mode in a Terminal. All the text like Log something has to end with CHR(13) "\r"
I hope you find it useful and enjoy it.
Please let me know about your experience or if you have any questions.
Normally, CTRL-C stops the terminal. In this case, we don't always get a clean shutdown of the application, which can cause issues.
In my case, I use it to control a multi-track player with the MPG123 music player.
The player I built is started five times with different tracks: Original, Bass, Vocals, Drums, and Others.
CTRL-C doesn't shut down these players cleanly and cuts off the playback.
To catch CTRL-C, I handle the rest in B4J, giving the players time to stop and clean up as needed.
The sample uses inline Java, and no extra libraries are required.
There's also a timer loop in the Java code that prevents the terminal from hanging up if you haven't called stopMessageLoop in your B4J code.
The code works well in a terminal and is very reliable on a Raspberry Pi, though it should also work on other Linux systems.
There’s also a Windows part, but it’s not finished yet and still needs some work.
If you are in Release mode in a Terminal. All the text like Log something has to end with CHR(13) "\r"
I hope you find it useful and enjoy it.
Please let me know about your experience or if you have any questions.
CTRL-C Fetch and Handle.:
'Non-UI application (console / server application)
#Region Project Attributes
#CommandLineArgs:
#MergeLibraries: True
#End Region
Sub Process_Globals
Private jo As JavaObject
Private testtimer As Timer
End Sub
Sub AppStart (Args() As String)
Log("Hello world!!!")
jo = Me
if jo.RunMethod("isRunningInTerminal", Null) = True then 'Check this first if you are in a terminal. Give false when you are in the B4J IDE
' Create an instance of KeyPressHandler in Java
jo.RunMethod("captureKeyPress", Null)
end if
testtimer.Initialize("testtimer", 10000)
testtimer_Tick
testtimer.Enabled = True
StartMessageLoop
End Sub
Private Sub testtimer_Tick
Log(DateTime.Time(DateTime.Now) & Chr(13))
End Sub
Sub On_KeyPress (Key As Int)
If Key <> 13 Then
Log(Key & " " & Chr(Key) & Chr(13))
Else
ExitApplication
End If
' Log(CRLF & "Key pressed: " & Chr(Key) & CRLF)
If Key = 3 Then
jo.RunMethod("stopMessageLoop", Null)
Key = Asc("q")
Log("Detect CTRL-C" & Chr(13))
End If
Select Key
Case Asc("n") ' "n" for next song
Log("Next song command detected!" & Chr(13))
' Implement your logic to play the next song
' PlayerItSelf.NextSong
Case Asc("p") ' "p" for previous song
Log("Previous song command detected!" & Chr(13))
' PlayerItSelf.PreviousSong
Case Asc("q") ' "q" to quit
Log("Quit command detected!" & Chr(13))
testtimer.Enabled = False
StopMessageLoop
' ExitApplication
End Select
End Sub
#If Java
import java.io.IOException;
import java.io.InputStreamReader;
private static volatile boolean stopMessageLoop = false; // Flag to control shutdown
public static boolean isRunningInTerminal() {
return System.getenv("TERM") != null || System.getenv("TTY") != null;
}
public static void stopMessageLoop() {
stopMessageLoop = true; // Called from B4J when cleanup is done
}
public static void captureKeyPress() {
new Thread(() -> {
try {
if (System.getProperty("os.name").toLowerCase().contains("win")) {
captureWindowsKeyPress();
} else {
captureLinuxKeyPress();
}
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
// ✅ Windows Keypress Detection (uses JNA-Free Native Console Input)
private static void captureWindowsKeyPress() throws IOException {
try {
String[] cmd = {"cmd", "/c", "for /L %x in () do @pause>nul & echo."};
Process process = new ProcessBuilder(cmd).redirectInput(ProcessBuilder.Redirect.PIPE).redirectOutput(ProcessBuilder.Redirect.PIPE).start();
InputStreamReader reader = new InputStreamReader(process.getInputStream());
while (true) {
if (reader.ready()) { // Check if a key is available
int key = reader.read();
if (key != -1) {
ba.raiseEventFromDifferentThread(null, null, 0, "on_keypress", true, new Object[]{key});
}
}
Thread.sleep(10); // Prevent CPU overuse
}
} catch (Exception e) {
e.printStackTrace();
}
}
// ✅ Linux/MacOS Keypress Detection (Uses Raw Mode)
private static void captureLinuxKeyPress() {
try {
// ✅ Enable RAW mode (disables line buffering & echo)
Process rawMode = new ProcessBuilder("/bin/sh", "-c", "stty raw -echo </dev/tty").start();
rawMode.waitFor();
while (true) {
int key = System.in.read(); // Read single byte
// ✅ Handle multi-byte sequences (e.g., arrow keys)
if (key == 27) { // ESC character (start of escape sequence)
if (System.in.available() > 0) {
int next = System.in.read();
if (next == '[' && System.in.available() > 0) {
int finalKey = System.in.read();
System.out.println("\rSpecial key detected: " + finalKey);
continue; // Skip processing special keys
}
}
}
// ✅ Ignore newlines (Enter key sends '\r' (13) in raw mode)
if (key == 13) continue;
// ✅ If 'q' (113) is pressed, restore terminal but **continue sending key**
if (key == 113) {
// System.out.println("\rRestoring terminal...");
new ProcessBuilder("/bin/sh", "-c", "stty cooked echo </dev/tty").start().waitFor();
}
// ✅ If `CTRL+C` (3) is pressed, restore terminal and **exit cleanly**
if (key == 3) {
System.out.println("\rCTRL+C detected! Sending event to B4J for cleanup...");
// 🔹 Send event to B4J (but DON'T stop Java process yet)
ba.raiseEventFromDifferentThread(null, null, 0, "on_keypress", true, new Object[]{3});
// 🔹 Wait for B4J to send `stopMessageLoop`, but **add timeout**
int counter = 0;
while (!stopMessageLoop && counter < 50) { // 50 * 100ms = 5 seconds timeout
Thread.sleep(250);
counter++;
}
if (!stopMessageLoop) {
System.out.println("⚠ Timeout reached! Forcing shutdown...\r");
System.out.println("⚠ You need to Call stopMessageLoop from BA to manage the rest of the code\r");
} else {
System.out.println("B4J cleanup completed. Shutting down player...");
}
System.out.println("\rShutting down player after cleanup...\r");
break; // Exit loop only when B4J has finished cleanup
}
// ✅ Send only valid keypress events to B4J
ba.raiseEventFromDifferentThread(null, null, 0, "on_keypress", true, new Object[]{(char) key});
// ✅ Send `13` (Enter) after every keypress
ba.raiseEventFromDifferentThread(null, null, 0, "on_keypress", true, new Object[]{13});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
// ✅ Restore terminal mode
new ProcessBuilder("/bin/sh", "-c", "stty cooked echo </dev/tty").start().waitFor();
} catch (Exception ignored) {}
}
}
#End If
Last edited: