Share My Creation Keypress and CTRL-C Controle for Linux Raspberry PI Non-UI

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.
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:
Top