diff --git a/CMakeLists.txt b/CMakeLists.txt index 4c6d54412..7be458688 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -212,7 +212,7 @@ install(FILES DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig ) -file(GLOB TESTDATA CONFIGURE_DEPENDS tests/*.png tests/*.jpg tests/*.mkv tests/*.bmp) +file(GLOB TESTDATA CONFIGURE_DEPENDS data/*) install(FILES ${TESTDATA} DESTINATION ${CMAKE_INSTALL_PREFIX}/share/notcurses diff --git a/README.md b/README.md index 279b2cdf0..4037c4da3 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ for more information, see [my wiki](https://nick-black.com/dankwiki/index.php/No * [Introduction](#introduction) * [Requirements](#requirements) + * [Building](#building) * [Use](#use) * [Input](#input) * [Planes](#planes) ([Plane Channels API](#plane-channels-api), [Wide chars](#wide-chars)) @@ -105,6 +106,20 @@ that fine library. * From NCURSES: terminfo 6.1+ * From FFMpeg: libswscale 5.0+, libavformat 57.0+, libavutil 56.0+ +### Building + +* Create a subdirectory, traditionally `build`. Enter the directory. +* `cmake ..`. You might want to set e.g. `CMAKE_BUILD_TYPE`. +* `make` +* `make test` + +If you have unit test failures, *please* file a bug including the output of +`./notcurses-tester > log 2>&1` (`make test` also runs `notcurses-tester`, but +hides important output). + +To watch the bitchin' demo, run `./notcurses-demo -p ../data`. More details can +be found on the `notcurses-demo(1)` man page. + ## Use A program wishing to use notcurses will need to link it, ideally using the @@ -158,6 +173,8 @@ typedef struct notcurses_options { // Notcurses typically prints version info in notcurses_init() and // performance info in notcurses_stop(). This inhibits that output. bool suppress_bannner; + // Notcurses does not clear the screen on startup unless thus requested to. + bool clear_screen_start; // If non-NULL, notcurses_render() will write each rendered frame to this // FILE* in addition to outfp. This is used primarily for debugging. FILE* renderfp; @@ -286,12 +303,6 @@ must be readable without delay for it to be interpreted as such. // returned to indicate that no input was available, but only by // notcurses_getc(). Otherwise (including on EOF) (char32_t)-1 is returned. -// is this wide character a Supplementary Private Use Area-B codepoint? -static inline bool -wchar_supppuab_p(char32_t w){ - return w >= 0x100000 && w <= 0x10fffd; -} - #define suppuabize(w) ((w) + 0x100000) // Special composed key defintions. These values are added to 0x100000. @@ -319,6 +330,26 @@ wchar_supppuab_p(char32_t w){ #define NCKEY_F08 suppuabize(28) #define NCKEY_F09 suppuabize(29) #define NCKEY_F10 suppuabize(30) +#define NCKEY_F11 suppuabize(31) +#define NCKEY_F12 suppuabize(32) +#define NCKEY_F13 suppuabize(33) +#define NCKEY_F14 suppuabize(34) +#define NCKEY_F15 suppuabize(35) +#define NCKEY_F16 suppuabize(36) +#define NCKEY_F17 suppuabize(37) +#define NCKEY_F18 suppuabize(38) +#define NCKEY_F19 suppuabize(39) +#define NCKEY_F20 suppuabize(40) +#define NCKEY_F21 suppuabize(41) +#define NCKEY_F22 suppuabize(42) +#define NCKEY_F23 suppuabize(43) +#define NCKEY_F24 suppuabize(44) +#define NCKEY_F25 suppuabize(45) +#define NCKEY_F26 suppuabize(46) +#define NCKEY_F27 suppuabize(47) +#define NCKEY_F28 suppuabize(48) +#define NCKEY_F29 suppuabize(49) +#define NCKEY_F30 suppuabize(50) // ... leave room for up to 100 function keys, egads #define NCKEY_ENTER suppuabize(121) #define NCKEY_CLS suppuabize(122) // "clear-screen or erase" @@ -335,31 +366,102 @@ wchar_supppuab_p(char32_t w){ #define NCKEY_EXIT suppuabize(133) #define NCKEY_PRINT suppuabize(134) #define NCKEY_REFRESH suppuabize(135) +// Mouse events. We try to encode some details into the char32_t (i.e. which +// button was pressed), but some is embedded in the ncinput event. The release +// event is generic across buttons; callers must maintain state, if they care. +#define NCKEY_BUTTON1 suppuabize(201) +#define NCKEY_BUTTON2 suppuabize(202) +#define NCKEY_BUTTON3 suppuabize(203) +#define NCKEY_BUTTON4 suppuabize(204) +#define NCKEY_BUTTON5 suppuabize(205) +#define NCKEY_BUTTON6 suppuabize(206) +#define NCKEY_BUTTON7 suppuabize(207) +#define NCKEY_BUTTON8 suppuabize(208) +#define NCKEY_BUTTON9 suppuabize(209) +#define NCKEY_BUTTON10 suppuabize(210) +#define NCKEY_BUTTON11 suppuabize(211) +#define NCKEY_RELEASE suppuabize(212) + +// Is this char32_t a Supplementary Private Use Area-B codepoint? +static inline bool +wchar_supppuab_p(char32_t w){ + return w >= 0x100000 && w <= 0x10fffd; +} + +// Is the event a synthesized mouse event? +static inline bool +nckey_mouse_p(char32_t r){ + return r >= NCKEY_BUTTON1 && r <= NCKEY_RELEASE; +} + +// An input event. Cell coordinates are currently defined only for mouse events. +typedef struct ncinput { + char32_t id; // identifier. Unicode codepoint or synthesized NCKEY event + int y; // y cell coordinate of event, -1 for undefined + int x; // x cell coordinate of event, -1 for undefined + // FIXME modifiers (alt, etc?) +} ncinput; // See ppoll(2) for more detail. Provide a NULL 'ts' to block at length, a 'ts' // of 0 for non-blocking operation, and otherwise a timespec to bound blocking. // Signals in sigmask (less several we handle internally) will be atomically // masked and unmasked per ppoll(2). It should generally contain all signals. // Returns a single Unicode code point, or (char32_t)-1 on error. 'sigmask' may -// be NULL. -char32_t notcurses_getc(struct notcurses* n, const struct timespec* ts, sigset_t* sigmask); +// be NULL. Returns 0 on a timeout. If an event is processed, the return value +// is the 'id' field from that event. 'ni' may be NULL. +API char32_t notcurses_getc(struct notcurses* n, const struct timespec* ts, + sigset_t* sigmask, ncinput* ni); +// 'ni' may be NULL if the caller is uninterested in event details. If no event +// is ready, returns 0. static inline char32_t -notcurses_getc_nblock(struct notcurses* n){ +notcurses_getc_nblock(struct notcurses* n, ncinput* ni){ sigset_t sigmask; sigfillset(&sigmask); struct timespec ts = { .tv_sec = 0, .tv_nsec = 0 }; - return notcurses_getc(n, &ts, &sigmask); + return notcurses_getc(n, &ts, &sigmask, ni); } +// 'ni' may be NULL if the caller is uninterested in event details. Blocks +// until an event is processed or a signal is received. static inline char32_t -notcurses_getc_blocking(struct notcurses* n){ +notcurses_getc_blocking(struct notcurses* n, ncinput* ni){ sigset_t sigmask; sigemptyset(&sigmask); - return notcurses_getc(n, NULL, &sigmask); + return notcurses_getc(n, NULL, &sigmask, ni); } + +// Enable the mouse in "button-event tracking" mode with focus detection and +// UTF8-style extended coordinates. On failure, -1 is returned. On success, 0 +// is returned, and mouse events will be published to notcurses_getc(). +API int notcurses_mouse_enable(struct notcurses* n); + +// Disable mouse events. Any events in the input queue can still be delivered. +API int notcurses_mouse_disable(struct notcurses* n); ``` +### Mice + +notcurses supports mice, though only through brokers such as X or +[GPM](https://www.nico.schottelius.org/software/gpm/). It does not speak +directly to hardware. Mouse events must be explicitly enabled with a +successful call to `notcurses_mouse_enable()`, and can later be disabled. + +```c +// Enable the mouse in "button-event tracking" mode with focus detection and +// UTF8-style extended coordinates. On failure, -1 is returned. On success, 0 +// is returned, and mouse events will be published to notcurses_getc(). +int notcurses_mouse_enable(struct notcurses* n); + +// Disable mouse events. Any events in the input queue can still be delivered. +int notcurses_mouse_disable(struct notcurses* n); +``` + +"Button-event tracking mode" implies the ability to detect mouse button +presses, and also mouse movement while holding down a mouse button (i.e. to +effect drag-and-drop). Mouse events are returned via the `NCKEY_MOUSE*` values, +with coordinate information in the `ncinput` struct. + ### Planes Fundamental to notcurses is a z-buffer of rectilinear virtual screens, known @@ -2178,6 +2280,7 @@ up someday **FIXME**. * Linux: [ioctl_tty(2)](http://man7.org/linux/man-pages/man2/ioctl_tty.2.html) * Linux: [ioctl_console(2)](http://man7.org/linux/man-pages/man2/ioctl_console.2.html) * Portable: [terminfo(5)](http://man7.org/linux/man-pages/man5/terminfo.5.html) +* Portable: [user_caps(5)](http://man7.org/linux/man-pages/man5/user_caps.5.html) ### Other TUI libraries of note diff --git a/tests/PurpleDrank.jpg b/data/PurpleDrank.jpg similarity index 100% rename from tests/PurpleDrank.jpg rename to data/PurpleDrank.jpg diff --git a/tests/changes.jpg b/data/changes.jpg similarity index 100% rename from tests/changes.jpg rename to data/changes.jpg diff --git a/tests/dsscaw-purp.png b/data/dsscaw-purp.png similarity index 100% rename from tests/dsscaw-purp.png rename to data/dsscaw-purp.png diff --git a/tests/eagles.png b/data/eagles.png similarity index 100% rename from tests/eagles.png rename to data/eagles.png diff --git a/tests/fm6.mkv b/data/fm6.mkv similarity index 100% rename from tests/fm6.mkv rename to data/fm6.mkv diff --git a/tests/lamepatents.jpg b/data/lamepatents.jpg similarity index 100% rename from tests/lamepatents.jpg rename to data/lamepatents.jpg diff --git a/tests/megaman2.bmp b/data/megaman2.bmp similarity index 100% rename from tests/megaman2.bmp rename to data/megaman2.bmp diff --git a/tests/notcursesI.avi b/data/notcursesI.avi similarity index 100% rename from tests/notcursesI.avi rename to data/notcursesI.avi diff --git a/tests/profoundchangesbeautiful-bare.jpg b/data/profoundchangesbeautiful-bare.jpg similarity index 100% rename from tests/profoundchangesbeautiful-bare.jpg rename to data/profoundchangesbeautiful-bare.jpg diff --git a/tests/samoa.avi b/data/samoa.avi similarity index 100% rename from tests/samoa.avi rename to data/samoa.avi diff --git a/debian/changelog b/debian/changelog index 43d2e69a3..98fbb1200 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +notcurses (1.0.0) UNRELEASED; urgency=medium + + * notcurses-bin depends on ncurses-term + * libnotcurses0 recommends ncurses-term + + -- Nick Black Mon, 23 Dec 2019 00:57:22 -0500 + notcurses (0.9.2-1) unstable; urgency=medium * New upstream diff --git a/debian/control b/debian/control index 7cac534f1..843680d8b 100644 --- a/debian/control +++ b/debian/control @@ -22,6 +22,7 @@ Description: Development files for notcurses Package: libnotcurses0 Architecture: any Multi-Arch: same +Recommends: ncurses-term (>= 6.1) Depends:${shlibs:Depends}, ${misc:Depends} Description: Shared libraries for notcurses TUI notcurses facilitates the creation of modern TUI programs, @@ -31,7 +32,7 @@ Description: Shared libraries for notcurses TUI Package: notcurses-bin Architecture: any Multi-Arch: foreign -Depends:${shlibs:Depends}, ${misc:Depends} +Depends:${shlibs:Depends}, ${misc:Depends}, ncurses-term (>= 6.1) Description: Binaries from libnotcurses notcurses facilitates the creation of modern TUI programs, making full use of Unicode and 24-bit direct color. It presents diff --git a/include/notcurses.h b/include/notcurses.h index 06a43bb00..4b6370e9b 100644 --- a/include/notcurses.h +++ b/include/notcurses.h @@ -121,6 +121,8 @@ typedef struct notcurses_options { // Notcurses typically prints version info in notcurses_init() and performance // info in notcurses_stop(). This inhibits that output. bool suppress_bannner; + // Notcurses does not clear the screen on startup unless thus requested to. + bool clear_screen_start; // If non-NULL, notcurses_render() will write each rendered frame to this // FILE* in addition to outfp. This is used primarily for debugging. FILE* renderfp; @@ -159,12 +161,6 @@ API struct ncplane* notcurses_top(struct notcurses* n); #define suppuabize(w) ((w) + 0x100000) -// is this wide character a Supplementary Private Use Area-B codepoint? -static inline bool -wchar_supppuab_p(char32_t w){ - return w >= 0x100000 && w <= 0x10fffd; -} - // Special composed key defintions. These values are added to 0x100000. #define NCKEY_INVALID suppuabize(0) #define NCKEY_RESIZE suppuabize(1) // generated interally in response to SIGWINCH @@ -226,30 +222,79 @@ wchar_supppuab_p(char32_t w){ #define NCKEY_EXIT suppuabize(133) #define NCKEY_PRINT suppuabize(134) #define NCKEY_REFRESH suppuabize(135) +// Mouse events. We try to encode some details into the char32_t (i.e. which +// button was pressed), but some is embedded in the ncinput event. The release +// event is generic across buttons; callers must maintain state, if they care. +#define NCKEY_BUTTON1 suppuabize(201) +#define NCKEY_BUTTON2 suppuabize(202) +#define NCKEY_BUTTON3 suppuabize(203) +#define NCKEY_BUTTON4 suppuabize(204) +#define NCKEY_BUTTON5 suppuabize(205) +#define NCKEY_BUTTON6 suppuabize(206) +#define NCKEY_BUTTON7 suppuabize(207) +#define NCKEY_BUTTON8 suppuabize(208) +#define NCKEY_BUTTON9 suppuabize(209) +#define NCKEY_BUTTON10 suppuabize(210) +#define NCKEY_BUTTON11 suppuabize(211) +#define NCKEY_RELEASE suppuabize(212) + +// Is this char32_t a Supplementary Private Use Area-B codepoint? +static inline bool +wchar_supppuab_p(char32_t w){ + return w >= 0x100000 && w <= 0x10fffd; +} + +// Is the event a synthesized mouse event? +static inline bool +nckey_mouse_p(char32_t r){ + return r >= NCKEY_BUTTON1 && r <= NCKEY_RELEASE; +} + +// An input event. Cell coordinates are currently defined only for mouse events. +typedef struct ncinput { + char32_t id; // identifier. Unicode codepoint or synthesized NCKEY event + int y; // y cell coordinate of event, -1 for undefined + int x; // x cell coordinate of event, -1 for undefined + // FIXME modifiers (alt, etc?) +} ncinput; // See ppoll(2) for more detail. Provide a NULL 'ts' to block at length, a 'ts' // of 0 for non-blocking operation, and otherwise a timespec to bound blocking. // Signals in sigmask (less several we handle internally) will be atomically // masked and unmasked per ppoll(2). It should generally contain all signals. // Returns a single Unicode code point, or (char32_t)-1 on error. 'sigmask' may -// be NULL. -API char32_t notcurses_getc(struct notcurses* n, const struct timespec* ts, sigset_t* sigmask); +// be NULL. Returns 0 on a timeout. If an event is processed, the return value +// is the 'id' field from that event. 'ni' may be NULL. +API char32_t notcurses_getc(struct notcurses* n, const struct timespec* ts, + sigset_t* sigmask, ncinput* ni); +// 'ni' may be NULL if the caller is uninterested in event details. If no event +// is ready, returns 0. static inline char32_t -notcurses_getc_nblock(struct notcurses* n){ +notcurses_getc_nblock(struct notcurses* n, ncinput* ni){ sigset_t sigmask; sigfillset(&sigmask); struct timespec ts = { .tv_sec = 0, .tv_nsec = 0 }; - return notcurses_getc(n, &ts, &sigmask); + return notcurses_getc(n, &ts, &sigmask, ni); } +// 'ni' may be NULL if the caller is uninterested in event details. Blocks +// until an event is processed or a signal is received. static inline char32_t -notcurses_getc_blocking(struct notcurses* n){ +notcurses_getc_blocking(struct notcurses* n, ncinput* ni){ sigset_t sigmask; sigemptyset(&sigmask); - return notcurses_getc(n, NULL, &sigmask); + return notcurses_getc(n, NULL, &sigmask, ni); } +// Enable the mouse in "button-event tracking" mode with focus detection and +// UTF8-style extended coordinates. On failure, -1 is returned. On success, 0 +// is returned, and mouse events will be published to notcurses_getc(). +API int notcurses_mouse_enable(struct notcurses* n); + +// Disable mouse events. Any events in the input queue can still be delivered. +API int notcurses_mouse_disable(struct notcurses* n); + // Refresh our idea of the terminal's dimensions, reshaping the standard plane // if necessary. Without a call to this function following a terminal resize // (as signaled via SIGWINCH), notcurses_render() might not function properly. diff --git a/src/demo/panelreel.c b/src/demo/panelreel.c index a78e59944..98ecffd19 100644 --- a/src/demo/panelreel.c +++ b/src/demo/panelreel.c @@ -231,7 +231,7 @@ handle_input(struct notcurses* nc, struct panelreel* pr, int efd, return (wchar_t)-1; }else{ if(fds[0].revents & POLLIN){ - key = notcurses_getc_blocking(nc); + key = notcurses_getc_blocking(nc, NULL); if(key < 0){ return -1; } diff --git a/src/demo/view.c b/src/demo/view.c index 156912839..5e63219e4 100644 --- a/src/demo/view.c +++ b/src/demo/view.c @@ -5,7 +5,7 @@ static int watch_for_keystroke(struct notcurses* nc, struct ncvisual* ncv __attribute__ ((unused))){ wchar_t w; // we don't want a keypress, but should handle NCKEY_RESIZE - if((w = notcurses_getc_nblock(nc)) != (wchar_t)-1){ + if((w = notcurses_getc_nblock(nc, NULL)) != (wchar_t)-1){ if(w == NCKEY_RESIZE){ // FIXME resize that sumbitch }else{ diff --git a/src/demo/witherworm.c b/src/demo/witherworm.c index 210430b30..9c1a33fce 100644 --- a/src/demo/witherworm.c +++ b/src/demo/witherworm.c @@ -673,7 +673,7 @@ int witherworm_demo(struct notcurses* nc){ struct timespec left, cur; clock_gettime(CLOCK_MONOTONIC, &cur); timespec_subtract(&left, &screenend, &cur); - key = notcurses_getc(nc, &left, NULL); + key = notcurses_getc(nc, &left, NULL, NULL); clock_gettime(CLOCK_MONOTONIC, &cur); int64_t ns = timespec_subtract_ns(&cur, &screenend); if(ns > 0){ diff --git a/src/input/input.cpp b/src/input/input.cpp index 739b6d3d0..4ea53dea6 100644 --- a/src/input/input.cpp +++ b/src/input/input.cpp @@ -4,13 +4,14 @@ #include #include #include +#include #include static int dimy, dimx; static struct notcurses* nc; // return the string version of a special composed key -const char* nckeystr(wchar_t spkey){ +const char* nckeystr(char32_t spkey){ switch(spkey){ // FIXME case NCKEY_RESIZE: notcurses_resize(nc, &dimy, &dimx); @@ -73,23 +74,73 @@ const char* nckeystr(wchar_t spkey){ case NCKEY_EXIT: return "exit"; case NCKEY_PRINT: return "print"; case NCKEY_REFRESH: return "refresh"; + case NCKEY_BUTTON1: return "mouse (button 1 pressed)"; + case NCKEY_BUTTON2: return "mouse (button 2 pressed)"; + case NCKEY_BUTTON3: return "mouse (button 3 pressed)"; + case NCKEY_BUTTON4: return "mouse (button 4 pressed)"; + case NCKEY_BUTTON5: return "mouse (button 5 pressed)"; + case NCKEY_BUTTON6: return "mouse (button 6 pressed)"; + case NCKEY_BUTTON7: return "mouse (button 7 pressed)"; + case NCKEY_BUTTON8: return "mouse (button 8 pressed)"; + case NCKEY_BUTTON9: return "mouse (button 9 pressed)"; + case NCKEY_BUTTON10: return "mouse (button 10 pressed)"; + case NCKEY_BUTTON11: return "mouse (button 11 pressed)"; + case NCKEY_RELEASE: return "mouse (button released)"; default: return "unknown"; } } -// print the utf8 Control Pictures for otherwise unprintable chars -wchar_t printutf8(wchar_t kp){ - if(kp <= 27 && kp >= 0){ +// Print the utf8 Control Pictures for otherwise unprintable ASCII +char32_t printutf8(char32_t kp){ + if(kp <= 27){ return 0x2400 + kp; } return kp; } +// Dim all text on the plane by the same amount. This will stack for +// older text, and thus clearly indicate the current output. +static int +dim_rows(struct ncplane* n){ + int y, x; + cell c = CELL_TRIVIAL_INITIALIZER; + for(y = 2 ; y < dimy ; ++y){ + for(x = 0 ; x < dimx ; ++x){ + if(ncplane_at_yx(n, y, x, &c) < 0){ + cell_release(n, &c); + return -1; + } + unsigned r, g, b; + cell_get_fg_rgb(&c, &r, &g, &b); + r -= r / 32; + g -= g / 32; + b -= b / 32; + if(r > 247){ r = 0; } + if(g > 247){ g = 0; } + if(b > 247){ b = 0; } + if(cell_set_fg_rgb(&c, r, g, b)){ + cell_release(n, &c); + return -1; + } + if(ncplane_putc_yx(n, y, x, &c) < 0){ + cell_release(n, &c); + return -1; + } + if(cell_double_wide_p(&c)){ + ++x; + } + } + } + cell_release(n, &c); + return 0; +} + int main(void){ if(setlocale(LC_ALL, "") == nullptr){ return EXIT_FAILURE; } notcurses_options opts{}; + opts.clear_screen_start = true; if((nc = notcurses_init(&opts, stdout)) == nullptr){ return EXIT_FAILURE;; } @@ -97,48 +148,64 @@ int main(void){ notcurses_term_dim_yx(nc, &dimy, &dimx); ncplane_set_fg(n, 0); ncplane_set_bg(n, 0xbb64bb); - ncplane_styles_set(n, CELL_STYLE_UNDERLINE); - if(ncplane_putstr_aligned(n, 0, "mash some keys, yo", NCALIGN_CENTER) <= 0){ + ncplane_styles_on(n, CELL_STYLE_UNDERLINE); + if(ncplane_putstr_aligned(n, 0, "mash keys, yo. give that mouse some waggle! ctrl+d exits.", NCALIGN_CENTER) <= 0){ notcurses_stop(nc); return EXIT_FAILURE; } - ncplane_styles_off(n, CELL_STYLE_UNDERLINE); + ncplane_styles_set(n, 0); ncplane_set_bg_default(n); notcurses_render(nc); - int y = 1; + int y = 2; std::deque cells; - wchar_t r; - while(errno = 0, (r = notcurses_getc_blocking(nc)) >= 0){ + char32_t r; + if(notcurses_mouse_enable(nc)){ + notcurses_stop(nc); + return EXIT_FAILURE; + } + ncinput ni; + while(errno = 0, (r = notcurses_getc_blocking(nc, &ni)) != (char32_t)-1){ if(r == 0){ // interrupted by signal continue; } + if(r == CEOT){ + notcurses_stop(nc); + return EXIT_SUCCESS; + } if(ncplane_cursor_move_yx(n, y, 0)){ break; } if(r < 0x80){ ncplane_set_fg_rgb(n, 128, 250, 64); - if(ncplane_printf(n, "Got ASCII: [0x%02x (%03d)] '%lc'\n", + if(ncplane_printf(n, "Got ASCII: [0x%02x (%03d)] '%lc'", r, r, iswprint(r) ? r : printutf8(r)) < 0){ break; } }else{ if(wchar_supppuab_p(r)){ ncplane_set_fg_rgb(n, 250, 64, 128); - if(ncplane_printf(n, "Got special key: [0x%02x (%02d)] '%s'\n", + if(ncplane_printf(n, "Got special key: [0x%02x (%02d)] '%s'", r, r, nckeystr(r)) < 0){ break; } + if(nckey_mouse_p(r)){ + if(ncplane_printf(n, " x: %d y: %d", ni.x, ni.y) < 0){ + break; + } + } }else{ ncplane_set_fg_rgb(n, 64, 128, 250); - ncplane_printf(n, "Got UTF-8: [0x%08x] '%lc'\n", r, r); + ncplane_printf(n, "Got UTF-8: [0x%08x] '%lc'", r, r); } } - // FIXME reprint all lines, fading older ones + if(dim_rows(n)){ + break; + } if(notcurses_render(nc)){ break; } if(++y >= dimy - 2){ // leave a blank line at the bottom - y = 1; // and at the top + y = 2; // and at the top } while(cells.size() >= dimy - 3u){ cells.pop_back(); @@ -147,7 +214,7 @@ int main(void){ } int e = errno; notcurses_stop(nc); - if(r < 0 && e){ + if(r == (char32_t)-1 && e){ std::cerr << "Error reading from terminal (" << strerror(e) << "?)\n"; } return EXIT_FAILURE; diff --git a/src/lib/input.c b/src/lib/input.c index 44bea3ea1..b3ec0a7b4 100644 --- a/src/lib/input.c +++ b/src/lib/input.c @@ -1,8 +1,15 @@ #include // needed for some definitions, see terminfo(3ncurses) #include +#include #include #include "internal.h" +// CSI (Control Sequence Indicators) originate in the terminal itself, and are +// not reported in their bare form to the user. For our purposes, these usually +// indicate a mouse event. +#define CSIPREFIX "\x1b[<" +static const char32_t NCKEY_CSI = 1; + static const unsigned char ESC = 0x1b; // 27 sig_atomic_t resize_seen = 0; @@ -30,7 +37,7 @@ unpop_keypress(notcurses* nc, int kpress){ // we assumed escapes can only be composed of 7-bit chars typedef struct esctrie { - int special; // composed key terminating here + char32_t special; // composed key terminating here struct esctrie** trie; // if non-NULL, next level of radix-128 trie } esctrie; @@ -66,7 +73,7 @@ notcurses_add_input_escape(notcurses* nc, const char* esc, char32_t special){ fprintf(stderr, "Not an escape: %s (0x%x)\n", esc, special); return -1; } - if(!wchar_supppuab_p(special)){ + if(!wchar_supppuab_p(special) && special != NCKEY_CSI){ fprintf(stderr, "Not a supplementary-b PUA char: %lc (0x%x)\n", special, special); return -1; } @@ -102,21 +109,98 @@ notcurses_add_input_escape(notcurses* nc, const char* esc, char32_t special){ return 0; } +// We received the CSI prefix. Extract the data payload. +static char32_t +handle_csi(notcurses* nc, ncinput* ni){ + enum { + PARAM1, // reading first param (button + modifiers) plus delimiter + PARAM2, // reading second param (x coordinate) plus delimiter + PARAM3, // reading third param (y coordinate) plus terminator + } state = PARAM1; + int param = 0; // numeric translation of param thus far + char32_t id = (char32_t)-1; + while(nc->inputbuf_occupied){ + int candidate = pop_input_keypress(nc); + if(state == PARAM1){ + if(candidate == ';'){ + state = PARAM2; + // modifiers: 32 (motion) 16 (control) 8 (alt) 4 (shift) + // buttons 4, 5, 6, 7: adds 64 + // buttons 8, 9, 10, 11: adds 128 + if(param >= 0 && param < 64){ + if(param % 4 == 3){ + id = NCKEY_RELEASE; + }else{ + id = NCKEY_BUTTON1 + (param % 4); + } + }else if(param >= 64 && param < 128){ + id = NCKEY_BUTTON4 + (param % 4); + }else if(param >= 128 && param < 192){ + id = NCKEY_BUTTON8 + (param % 4); + }else{ + break; + } + param = 0; + }else if(isdigit(candidate)){ + param *= 10; + param += candidate - '0'; + }else{ + break; + } + }else if(state == PARAM2){ + if(candidate == ';'){ + state = PARAM3; + if(param == 0){ + break; + } + if(ni){ + ni->x = param - 1; + } + param = 0; + }else if(isdigit(candidate)){ + param *= 10; + param += candidate - '0'; + }else{ + break; + } + }else if(state == PARAM3){ + if(candidate == 'm' || candidate == 'M'){ + if(candidate == 'm'){ + id = NCKEY_RELEASE; + } + if(param == 0){ + break; + } + if(ni){ + ni->y = param - 1; + ni->id = id; + } + return id; + }else if(isdigit(candidate)){ + param *= 10; + param += candidate - '0'; + }else{ + break; + } + } + } + // FIXME ungetc on failure! walk trie backwards or something + return (char32_t)-1; +} + // add the keypress we just read to our input queue (assuming there is room). // if there is a full UTF8 codepoint or keystroke (composed or otherwise), // return it, and pop it from the queue. static char32_t -handle_getc(notcurses* nc, int kpress){ +handle_getc(notcurses* nc, int kpress, ncinput* ni){ // fprintf(stderr, "KEYPRESS: %d\n", kpress); if(kpress < 0){ return -1; } if(kpress == ESC){ - // FIXME delay a little waiting for more? const esctrie* esc = nc->inputescapes; while(esc && esc->special == NCKEY_INVALID && nc->inputbuf_occupied){ int candidate = pop_input_keypress(nc); -//fprintf(stderr, "CANDIDATE: %c\n", candidate); if(esc->trie == NULL){ esc = NULL; }else if(candidate >= 0x80 || candidate < 0){ @@ -125,8 +209,10 @@ handle_getc(notcurses* nc, int kpress){ esc = esc->trie[candidate]; } } -//fprintf(stderr, "esc? %c special: %d\n", esc ? 'y' : 'n', esc ? esc->special : NCKEY_INVALID); if(esc && esc->special != NCKEY_INVALID){ + if(esc->special == NCKEY_CSI){ + return handle_csi(nc, ni); + } return esc->special; } // FIXME ungetc on failure! walk trie backwards or something @@ -184,7 +270,7 @@ input_queue_full(const notcurses* nc){ } static char32_t -handle_input(notcurses* nc){ +handle_input(notcurses* nc, ncinput* ni){ int r; // getc() returns unsigned chars cast to ints while(!input_queue_full(nc) && (r = getc(nc->ttyinfp)) >= 0){ @@ -205,17 +291,18 @@ handle_input(notcurses* nc){ return -1; } r = pop_input_keypress(nc); - return handle_getc(nc, r); + return handle_getc(nc, r, ni); } -// infp has always been set non-blocking -char32_t notcurses_getc(notcurses* nc, const struct timespec *ts, sigset_t* sigmask){ +// infp has already been set non-blocking +char32_t notcurses_getc(notcurses* nc, const struct timespec *ts, + sigset_t* sigmask, ncinput* ni){ errno = 0; - char32_t r = handle_input(nc); + char32_t r = handle_input(nc, ni); if(r == (char32_t)-1){ if(errno == EAGAIN || errno == EWOULDBLOCK){ block_on_input(nc->ttyinfp, ts, sigmask); - return handle_input(nc); + return handle_input(nc, ni); } return r; } @@ -302,5 +389,9 @@ int prep_special_keys(notcurses* nc){ return -1; } } + if(notcurses_add_input_escape(nc, CSIPREFIX, NCKEY_CSI)){ + fprintf(stderr, "Couldn't add support for %s\n", k->tinfo); + return -1; + } return 0; } diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c index 6133d4686..b1bfa8bad 100644 --- a/src/lib/notcurses.c +++ b/src/lib/notcurses.c @@ -23,6 +23,8 @@ #include "version.h" #include "egcpool.h" +#define ESC "\x1b" + // only one notcurses object can be the target of signal handlers, due to their // process-wide nature. static notcurses* _Atomic signal_nc = ATOMIC_VAR_INIT(NULL); // ugh @@ -575,9 +577,7 @@ interrogate_terminfo(notcurses* nc, const notcurses_options* opts){ // support for the style in that case. int nocolor_stylemask = tigetnum("ncv"); if(nocolor_stylemask > 0){ - // FIXME this doesn't work if we're using sgr, which we are at the moment! - // ncv is defined in terms of curses style bits, which differ from ours - if(nocolor_stylemask & WA_STANDOUT){ + if(nocolor_stylemask & WA_STANDOUT){ // ncv is composed of terminfo bits, not ours nc->standout = NULL; } if(nocolor_stylemask & WA_UNDERLINE){ @@ -599,6 +599,7 @@ interrogate_terminfo(notcurses* nc, const notcurses_options* opts){ nc->italics = NULL; } } + term_verify_seq(&nc->getm, "getm"); // get mouse events // Not all terminals support setting the fore/background independently term_verify_seq(&nc->setaf, "setaf"); term_verify_seq(&nc->setab, "setab"); @@ -716,6 +717,7 @@ notcurses* notcurses_init(const notcurses_options* opts, FILE* outfp){ free(ret); return NULL; } + notcurses_mouse_disable(ret); if(tcgetattr(ret->ttyfd, &ret->tpreserved)){ fprintf(stderr, "Couldn't preserve terminal state for %d (%s)\n", ret->ttyfd, strerror(errno)); @@ -766,7 +768,6 @@ notcurses* notcurses_init(const notcurses_options* opts, FILE* outfp){ free_plane(ret->top); goto err; } - // term_emit("clear", ret->clear, ret->ttyfp, false); ret->suppress_banner = opts->suppress_bannner; if(!opts->suppress_bannner){ char prefixbuf[BPREFIXSTRLEN + 1]; @@ -796,6 +797,9 @@ notcurses* notcurses_init(const notcurses_options* opts, FILE* outfp){ fprintf(ret->ttyfp, "Are you specifying a proper DirectColor TERM?\n"); } } + if(opts->clear_screen_start){ + term_emit("clear", ret->clearscr, ret->ttyfp, false); + } return ret; err: @@ -823,6 +827,7 @@ int notcurses_stop(notcurses* nc){ if(nc->sgr0 && term_emit("sgr0", nc->sgr0, nc->ttyfp, true)){ ret = -1; } + ret |= notcurses_mouse_disable(nc); ret |= tcsetattr(nc->ttyfd, TCSANOW, &nc->tpreserved); while(nc->top){ ncplane* p = nc->top; @@ -1547,3 +1552,20 @@ ncplane* notcurses_top(notcurses* n){ ncplane* ncplane_below(ncplane* n){ return n->z; } + +#define SET_BTN_EVENT_MOUSE "1002" +#define SET_FOCUS_EVENT_MOUSE "1004" +#define SET_SGR_MODE_MOUSE "1006" +int notcurses_mouse_enable(notcurses* n){ + return term_emit("mouse", ESC "[?" SET_BTN_EVENT_MOUSE ";" + SET_FOCUS_EVENT_MOUSE ";" SET_SGR_MODE_MOUSE "h", + n->ttyfp, true); +} + +// this seems to work (note difference in suffix, 'l' vs 'h'), but what about +// the sequences 1000 etc? +int notcurses_mouse_disable(notcurses* n){ + return term_emit("mouse", ESC "[?" SET_BTN_EVENT_MOUSE ";" + SET_FOCUS_EVENT_MOUSE ";" SET_SGR_MODE_MOUSE "l", + n->ttyfp, true); +} diff --git a/src/planereel/main.cpp b/src/planereel/main.cpp index 036e8d70f..dba5ffaa9 100644 --- a/src/planereel/main.cpp +++ b/src/planereel/main.cpp @@ -64,7 +64,7 @@ int main(void){ } PR = pr; // FIXME eliminate char32_t key; - while((key = notcurses_getc_blocking(nc)) != (char32_t)-1){ + while((key = notcurses_getc_blocking(nc, nullptr)) != (char32_t)-1){ switch(key){ case 'q': return notcurses_stop(nc) ? EXIT_FAILURE : EXIT_SUCCESS; diff --git a/src/view/view.cpp b/src/view/view.cpp index c8f0903c3..6e02dc890 100644 --- a/src/view/view.cpp +++ b/src/view/view.cpp @@ -37,7 +37,7 @@ int ncview(struct notcurses* nc, struct ncvisual* ncv, int* averr){ .tv_sec = start.tv_sec + (long)(ns / 1000000000), .tv_nsec = start.tv_nsec + (long)(ns % 1000000000), }; - clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &interval, NULL); + clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &interval, nullptr); } if(*averr == AVERROR_EOF){ return 0; @@ -78,7 +78,7 @@ int main(int argc, char** argv){ std::cerr << "Error decoding " << argv[i] << ": " << errbuf.data() << std::endl; return EXIT_FAILURE; } - notcurses_getc_blocking(nc); + notcurses_getc_blocking(nc, nullptr); ncvisual_destroy(ncv); } if(notcurses_stop(nc)){ diff --git a/tests/input.cpp b/tests/input.cpp new file mode 100644 index 000000000..9384ae46b --- /dev/null +++ b/tests/input.cpp @@ -0,0 +1,38 @@ +#include "main.h" + +class InputTest : public :: testing::Test { + protected: + void SetUp() override { + setlocale(LC_ALL, ""); + if(getenv("TERM") == nullptr){ + GTEST_SKIP(); + } + notcurses_options nopts{}; + nopts.inhibit_alternate_screen = true; + nopts.suppress_bannner = true; + outfp_ = fopen("/dev/tty", "wb"); + ASSERT_NE(nullptr, outfp_); + nc_ = notcurses_init(&nopts, outfp_); + ASSERT_NE(nullptr, nc_); + } + + void TearDown() override { + if(nc_){ + EXPECT_EQ(0, notcurses_stop(nc_)); + } + if(outfp_){ + fclose(outfp_); + } + } + + struct notcurses* nc_{}; + FILE* outfp_{}; +}; + +TEST_F(InputTest, TestMouseOn){ + ASSERT_EQ(0, notcurses_mouse_enable(nc_)); +} + +TEST_F(InputTest, TestMouseOff){ + ASSERT_EQ(0, notcurses_mouse_disable(nc_)); +} diff --git a/tests/libav.cpp b/tests/libav.cpp index e9521ceda..eee105437 100644 --- a/tests/libav.cpp +++ b/tests/libav.cpp @@ -37,7 +37,7 @@ TEST_F(LibavTest, LoadImage) { int averr; int dimy, dimx; ncplane_dim_yx(ncp_, &dimy, &dimx); - auto ncv = ncplane_visual_open(ncp_, "../tests/dsscaw-purp.png", &averr); + auto ncv = ncplane_visual_open(ncp_, "../data/dsscaw-purp.png", &averr); ASSERT_NE(nullptr, ncv); ASSERT_EQ(0, averr); auto frame = ncvisual_decode(ncv, &averr); @@ -58,7 +58,7 @@ TEST_F(LibavTest, LoadVideo) { int averr; int dimy, dimx; ncplane_dim_yx(ncp_, &dimy, &dimx); - auto ncv = ncplane_visual_open(ncp_, "../tests/fm6.mkv", &averr); + auto ncv = ncplane_visual_open(ncp_, "../data/fm6.mkv", &averr); ASSERT_NE(nullptr, ncv); EXPECT_EQ(0, averr); auto frame = ncvisual_decode(ncv, &averr); @@ -75,7 +75,7 @@ TEST_F(LibavTest, LoadVideoCreatePlane) { int averr; int dimy, dimx; ncplane_dim_yx(ncp_, &dimy, &dimx); - auto ncv = ncvisual_open_plane(nc_, "../tests/fm6.mkv", &averr, 0, 0, NCSCALE_STRETCH); + auto ncv = ncvisual_open_plane(nc_, "../data/fm6.mkv", &averr, 0, 0, NCSCALE_STRETCH); ASSERT_NE(nullptr, ncv); EXPECT_EQ(0, averr); auto frame = ncvisual_decode(ncv, &averr);