From 623a1bd12877164ce2ae515e176b809d9c1a2041 Mon Sep 17 00:00:00 2001 From: Kevin Lamonte Date: Wed, 11 Mar 2015 05:22:47 -0400 Subject: [PATCH] terminal/backend handshaking, no more polling --- src/jexer/TApplication.java | 10 +- src/jexer/backend/Backend.java | 4 +- src/jexer/backend/ECMA48Backend.java | 33 +++++- src/jexer/io/ECMA48Terminal.java | 165 ++++++++++++++------------- 4 files changed, 122 insertions(+), 90 deletions(-) diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index 5df59d9..52bd435 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -249,12 +249,12 @@ public class TApplication { * Run this application until it exits, using stdin and stdout */ public final void run() { - + List events = new LinkedList(); + while (quit == false) { // Timeout is in milliseconds, so default timeout after 1 second // of inactivity. int timeout = getSleepTime(1000); - // std.stdio.stderr.writefln("poll() timeout: %d", timeout); if (eventQueue.size() > 0) { // Do not wait if there are definitely events waiting to be @@ -263,8 +263,9 @@ public class TApplication { } // Pull any pending input events - List events = backend.getEvents(timeout); + backend.getEvents(events, timeout); metaHandleEvents(events); + events.clear(); // Process timers and call doIdle()'s doIdle(); @@ -432,7 +433,8 @@ public class TApplication { assert(sleepTime.total!("msecs")() >= 0); return cast(uint)sleepTime.total!("msecs")(); */ - return 0; + // TODO: fix timers. Until then, come back after 250 millis. + return 250; } } diff --git a/src/jexer/backend/Backend.java b/src/jexer/backend/Backend.java index 66c7cf3..256e9bb 100644 --- a/src/jexer/backend/Backend.java +++ b/src/jexer/backend/Backend.java @@ -65,11 +65,11 @@ public abstract class Backend { * Subclasses must provide an implementation to get keyboard, mouse, and * screen resize events. * + * @param queue list to append new events to * @param timeout maximum amount of time (in millis) to wait for an * event. 0 means to return immediately, i.e. perform a poll. - * @return events received, or an empty list if the timeout was reached */ - abstract public List getEvents(int timeout); + abstract public void getEvents(List queue, int timeout); /** * Subclasses must provide an implementation that closes sockets, diff --git a/src/jexer/backend/ECMA48Backend.java b/src/jexer/backend/ECMA48Backend.java index 835dfaa..f150833 100644 --- a/src/jexer/backend/ECMA48Backend.java +++ b/src/jexer/backend/ECMA48Backend.java @@ -87,14 +87,39 @@ public class ECMA48Backend extends Backend { /** * Get keyboard, mouse, and screen resize events. * + * @param queue list to append new events to * @param timeout maximum amount of time (in millis) to wait for an * event. 0 means to return immediately, i.e. perform a poll. - * @return events received, or an empty list if the timeout was reached - * first */ @Override - public List getEvents(int timeout) { - return terminal.getEvents(); + public void getEvents(List queue, int timeout) { + if (timeout > 0) { + // Try to sleep, let the terminal's input thread wake me up if + // something came in. + synchronized (terminal) { + try { + terminal.wait(timeout); + if (terminal.hasEvents()) { + // System.err.println("getEvents()"); + terminal.getIdleEvents(queue); + } else { + // If I got here, then I timed out. Call + // terminal.getIdleEvents() to pick up stragglers + // like bare resize. + // System.err.println("getIdleEvents()"); + terminal.getIdleEvents(queue); + } + } catch (InterruptedException e) { + // Spurious interrupt, pretend it was like a timeout. + // System.err.println("[interrupt] getEvents()"); + terminal.getIdleEvents(queue); + } + } + } else { + // Asking for a poll, go get it. + System.err.println("[polled] getEvents()"); + terminal.getEvents(queue); + } } /** diff --git a/src/jexer/io/ECMA48Terminal.java b/src/jexer/io/ECMA48Terminal.java index ba745c5..d7d2391 100644 --- a/src/jexer/io/ECMA48Terminal.java +++ b/src/jexer/io/ECMA48Terminal.java @@ -193,6 +193,17 @@ public class ECMA48Terminal implements Runnable { return output; } + /** + * Check if there are events in the queue. + * + * @return if true, getEvents() has something to return to the backend + */ + public boolean hasEvents() { + synchronized (eventQueue) { + return (eventQueue.size() > 0); + } + } + /** * Call 'stty cooked' to set cooked mode. */ @@ -764,52 +775,59 @@ public class ECMA48Terminal implements Runnable { /** * Return any events in the IO queue. * - * @return list of new events (which may be empty) + * @param queue list to append new events to */ - public List getEvents() { - List events = new LinkedList(); - - synchronized(this) { + public void getEvents(List queue) { + synchronized (eventQueue) { if (eventQueue.size() > 0) { - events.addAll(eventQueue); + queue.addAll(eventQueue); eventQueue.clear(); } } - - // TEST: drop a cmAbort - // events.add(new jexer.event.TCommandEvent(jexer.TCommand.cmAbort)); - // events.add(new jexer.event.TKeypressEvent(kbAltX)); - - return events; } /** - * Parses the next character of input to see if an InputEvent is fully - * here. + * Return any events in the IO queue due to timeout. * - * @param ch Unicode code point - * @return list of new events (which may be empty) + * @param queue list to append new events to */ - public List getEvents(char ch) { - return getEvents(ch, false); + public void getIdleEvents(List queue) { + + // Check for new window size + session.queryWindowSize(); + int newWidth = session.getWindowWidth(); + int newHeight = session.getWindowHeight(); + if ((newWidth != windowResize.width) || + (newHeight != windowResize.height)) { + TResizeEvent event = new TResizeEvent(TResizeEvent.Type.Screen, + newWidth, newHeight); + windowResize.width = newWidth; + windowResize.height = newHeight; + synchronized (eventQueue) { + eventQueue.add(event); + } + } + + synchronized (eventQueue) { + if (eventQueue.size() > 0) { + queue.addAll(eventQueue); + eventQueue.clear(); + } + } } /** * Parses the next character of input to see if an InputEvent is * fully here. * + * @param events list to append new events to * @param ch Unicode code point - * @param noChar if true, ignore ch. This is currently used to return a - * bare ESC and RESIZE events. - * @return list of new events (which may be empty) */ - public List getEvents(char ch, boolean noChar) { - List events = new LinkedList(); + private void processChar(List events, char ch) { TKeypressEvent keypress; Date now = new Date(); - /* // ESCDELAY type timeout if (state == ParseState.ESCAPE) { long escDelay = now.getTime() - escapeTime; @@ -819,23 +837,6 @@ public class ECMA48Terminal implements Runnable { reset(); } } - */ - - if (noChar == true) { - int newWidth = session.getWindowWidth(); - int newHeight = session.getWindowHeight(); - if ((newWidth != windowResize.width) || - (newHeight != windowResize.height)) { - TResizeEvent event = new TResizeEvent(TResizeEvent.Type.Screen, - newWidth, newHeight); - windowResize.width = newWidth; - windowResize.height = newHeight; - events.add(event); - } - - // Nothing else to do, bail out - return events; - } // System.err.printf("state: %s ch %c\r\n", state, ch); @@ -845,14 +846,14 @@ public class ECMA48Terminal implements Runnable { if (ch == 0x1B) { state = ParseState.ESCAPE; escapeTime = now.getTime(); - return events; + return; } if (ch <= 0x1F) { // Control character events.add(controlChar(ch)); reset(); - return events; + return; } if (ch >= 0x20) { @@ -862,7 +863,7 @@ public class ECMA48Terminal implements Runnable { keypress.key.ch = ch; events.add(keypress); reset(); - return events; + return; } break; @@ -874,19 +875,19 @@ public class ECMA48Terminal implements Runnable { keypress.key.alt = true; events.add(keypress); reset(); - return events; + return; } if (ch == 'O') { // This will be one of the function keys state = ParseState.ESCAPE_INTERMEDIATE; - return events; + return; } // '[' goes to CSI_ENTRY if (ch == '[') { state = ParseState.CSI_ENTRY; - return events; + return; } // Everything else is assumed to be Alt-keystroke @@ -899,7 +900,7 @@ public class ECMA48Terminal implements Runnable { } events.add(keypress); reset(); - return events; + return; case ESCAPE_INTERMEDIATE: if ((ch >= 'P') && (ch <= 'S')) { @@ -924,25 +925,25 @@ public class ECMA48Terminal implements Runnable { } events.add(keypress); reset(); - return events; + return; } // Unknown keystroke, ignore reset(); - return events; + return; case CSI_ENTRY: // Numbers - parameter values if ((ch >= '0') && (ch <= '9')) { params.set(paramI, params.get(paramI) + ch); state = ParseState.CSI_PARAM; - return events; + return; } // Parameter separator if (ch == ';') { paramI++; params.set(paramI, ""); - return events; + return; } if ((ch >= 0x30) && (ch <= 0x7E)) { @@ -965,7 +966,7 @@ public class ECMA48Terminal implements Runnable { } events.add(keypress); reset(); - return events; + return; case 'B': // Down keypress = new TKeypressEvent(); @@ -984,7 +985,7 @@ public class ECMA48Terminal implements Runnable { } events.add(keypress); reset(); - return events; + return; case 'C': // Right keypress = new TKeypressEvent(); @@ -1003,7 +1004,7 @@ public class ECMA48Terminal implements Runnable { } events.add(keypress); reset(); - return events; + return; case 'D': // Left keypress = new TKeypressEvent(); @@ -1022,7 +1023,7 @@ public class ECMA48Terminal implements Runnable { } events.add(keypress); reset(); - return events; + return; case 'H': // Home keypress = new TKeypressEvent(); @@ -1030,7 +1031,7 @@ public class ECMA48Terminal implements Runnable { keypress.key.fnKey = TKeypress.HOME; events.add(keypress); reset(); - return events; + return; case 'F': // End keypress = new TKeypressEvent(); @@ -1038,7 +1039,7 @@ public class ECMA48Terminal implements Runnable { keypress.key.fnKey = TKeypress.END; events.add(keypress); reset(); - return events; + return; case 'Z': // CBT - Cursor backward X tab stops (default 1) keypress = new TKeypressEvent(); @@ -1046,11 +1047,11 @@ public class ECMA48Terminal implements Runnable { keypress.key.fnKey = TKeypress.BTAB; events.add(keypress); reset(); - return events; + return; case 'M': // Mouse position state = ParseState.MOUSE; - return events; + return; default: break; } @@ -1058,26 +1059,26 @@ public class ECMA48Terminal implements Runnable { // Unknown keystroke, ignore reset(); - return events; + return; case CSI_PARAM: // Numbers - parameter values if ((ch >= '0') && (ch <= '9')) { params.set(paramI, params.get(paramI) + ch); state = ParseState.CSI_PARAM; - return events; + return; } // Parameter separator if (ch == ';') { paramI++; params.set(paramI, ""); - return events; + return; } if (ch == '~') { events.add(csiFnKey()); reset(); - return events; + return; } if ((ch >= 0x30) && (ch <= 0x7E)) { @@ -1100,7 +1101,7 @@ public class ECMA48Terminal implements Runnable { } events.add(keypress); reset(); - return events; + return; case 'B': // Down keypress = new TKeypressEvent(); @@ -1119,7 +1120,7 @@ public class ECMA48Terminal implements Runnable { } events.add(keypress); reset(); - return events; + return; case 'C': // Right keypress = new TKeypressEvent(); @@ -1138,7 +1139,7 @@ public class ECMA48Terminal implements Runnable { } events.add(keypress); reset(); - return events; + return; case 'D': // Left keypress = new TKeypressEvent(); @@ -1157,7 +1158,7 @@ public class ECMA48Terminal implements Runnable { } events.add(keypress); reset(); - return events; + return; default: break; } @@ -1165,7 +1166,7 @@ public class ECMA48Terminal implements Runnable { // Unknown keystroke, ignore reset(); - return events; + return; case MOUSE: params.set(0, params.get(paramI) + ch); @@ -1174,14 +1175,14 @@ public class ECMA48Terminal implements Runnable { events.add(parseMouse()); reset(); } - return events; + return; default: break; } // This "should" be impossible to reach - return events; + return; } /** @@ -1589,6 +1590,7 @@ public class ECMA48Terminal implements Runnable { // available() will often return > 1, so we need to read in chunks to // stay caught up. char [] readBuffer = new char[128]; + List events = new LinkedList(); while ((done == false) && (stopReaderThread == false)) { try { @@ -1609,15 +1611,18 @@ public class ECMA48Terminal implements Runnable { } else { for (int i = 0; i < rc; i++) { int ch = readBuffer[i]; - - // System.err.printf("** READ 0x%x '%c'", ch, ch); - List events = getEvents((char)ch); - synchronized (this) { - /* - System.err.printf("adding %d events\n", - events.size()); - */ - eventQueue.addAll(events); + processChar(events, (char)ch); + if (events.size() > 0) { + // Add to the queue for the backend thread to + // be able to obtain. + synchronized (eventQueue) { + eventQueue.addAll(events); + } + // Now wake up the backend + synchronized (this) { + this.notifyAll(); + } + events.clear(); } } } -- 2.27.0