You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
notcurses/src/input/input.cpp

460 lines
14 KiB
C++

#include <deque>
#include <cerrno>
#include <atomic>
#include <chrono>
#include <memory>
#include <thread>
#include <cstring>
#include <cstdlib>
#include <clocale>
#include <getopt.h>
#include <iostream>
#include <ncpp/Plane.hh>
#include <ncpp/NotCurses.hh>
#define NANOSECS_IN_SEC 1000000000
static inline uint64_t
timenow_to_ns(){
struct timespec t;
if(clock_gettime(CLOCK_MONOTONIC, &t)){
throw std::runtime_error("error retrieving time");
}
return t.tv_sec * NANOSECS_IN_SEC + t.tv_nsec;
}
using namespace ncpp;
std::mutex mtx;
uint64_t start;
std::atomic<bool> done;
static unsigned dimy, dimx;
static struct ncuplot* plot;
// return the string version of a special composed key
const char* nckeystr(char32_t spkey){
switch(spkey){ // FIXME
case NCKEY_RESIZE:
mtx.lock();
NotCurses::get_instance().refresh(&dimy, &dimx);
mtx.unlock();
return "resize event";
case NCKEY_INVALID: return "invalid";
case NCKEY_LEFT: return "left";
case NCKEY_UP: return "up";
case NCKEY_RIGHT: return "right";
case NCKEY_DOWN: return "down";
case NCKEY_INS: return "insert";
case NCKEY_DEL: return "delete";
case NCKEY_PGDOWN: return "pgdown";
case NCKEY_PGUP: return "pgup";
case NCKEY_HOME: return "home";
case NCKEY_END: return "end";
case NCKEY_F00: return "F0";
case NCKEY_F01: return "F1";
case NCKEY_F02: return "F2";
case NCKEY_F03: return "F3";
case NCKEY_F04: return "F4";
case NCKEY_F05: return "F5";
case NCKEY_F06: return "F6";
case NCKEY_F07: return "F7";
case NCKEY_F08: return "F8";
case NCKEY_F09: return "F9";
case NCKEY_F10: return "F10";
case NCKEY_F11: return "F11";
case NCKEY_F12: return "F12";
case NCKEY_F13: return "F13";
case NCKEY_F14: return "F14";
case NCKEY_F15: return "F15";
case NCKEY_F16: return "F16";
case NCKEY_F17: return "F17";
case NCKEY_F18: return "F18";
case NCKEY_F19: return "F19";
case NCKEY_F20: return "F20";
case NCKEY_F21: return "F21";
case NCKEY_F22: return "F22";
case NCKEY_F23: return "F23";
case NCKEY_F24: return "F24";
case NCKEY_F25: return "F25";
case NCKEY_F26: return "F26";
case NCKEY_F27: return "F27";
case NCKEY_F28: return "F28";
case NCKEY_F29: return "F29";
case NCKEY_F30: return "F30";
case NCKEY_F31: return "F31";
case NCKEY_F32: return "F32";
case NCKEY_F33: return "F33";
case NCKEY_F34: return "F34";
case NCKEY_F35: return "F35";
case NCKEY_F36: return "F36";
case NCKEY_F37: return "F37";
case NCKEY_F38: return "F38";
case NCKEY_F39: return "F39";
case NCKEY_F40: return "F40";
case NCKEY_F41: return "F41";
case NCKEY_F42: return "F42";
case NCKEY_F43: return "F43";
case NCKEY_F44: return "F44";
case NCKEY_F45: return "F45";
case NCKEY_F46: return "F46";
case NCKEY_F47: return "F47";
case NCKEY_F48: return "F48";
case NCKEY_F49: return "F49";
case NCKEY_F50: return "F50";
case NCKEY_F51: return "F51";
case NCKEY_F52: return "F52";
case NCKEY_F53: return "F53";
case NCKEY_F54: return "F54";
case NCKEY_F55: return "F55";
case NCKEY_F56: return "F56";
case NCKEY_F57: return "F57";
case NCKEY_F58: return "F58";
case NCKEY_F59: return "F59";
case NCKEY_BACKSPACE: return "backspace";
case NCKEY_CENTER: return "center";
case NCKEY_ENTER: return "enter";
case NCKEY_CLS: return "clear";
case NCKEY_DLEFT: return "down+left";
case NCKEY_DRIGHT: return "down+right";
case NCKEY_ULEFT: return "up+left";
case NCKEY_URIGHT: return "up+right";
case NCKEY_BEGIN: return "begin";
case NCKEY_CANCEL: return "cancel";
case NCKEY_CLOSE: return "close";
case NCKEY_COMMAND: return "command";
case NCKEY_COPY: return "copy";
case NCKEY_EXIT: return "exit";
case NCKEY_PRINT: return "print";
case NCKEY_REFRESH: return "refresh";
case NCKEY_SEPARATOR: return "separator";
case NCKEY_CAPS_LOCK: return "caps lock";
case NCKEY_SCROLL_LOCK: return "scroll lock";
case NCKEY_NUM_LOCK: return "num lock";
case NCKEY_PRINT_SCREEN: return "print screen";
case NCKEY_PAUSE: return "pause";
case NCKEY_MENU: return "menu";
// media keys, similarly only available through kitty's protocol
case NCKEY_MEDIA_PLAY: return "play";
case NCKEY_MEDIA_PAUSE: return "pause";
case NCKEY_MEDIA_PPAUSE: return "play-pause";
case NCKEY_MEDIA_REV: return "reverse";
case NCKEY_MEDIA_STOP: return "stop";
case NCKEY_MEDIA_FF: return "fast-forward";
case NCKEY_MEDIA_REWIND: return "rewind";
case NCKEY_MEDIA_NEXT: return "next track";
case NCKEY_MEDIA_PREV: return "previous track";
case NCKEY_MEDIA_RECORD: return "record";
case NCKEY_MEDIA_LVOL: return "lower volume";
case NCKEY_MEDIA_RVOL: return "raise volume";
case NCKEY_MEDIA_MUTE: return "mute";
case NCKEY_LSHIFT: return "left shift";
case NCKEY_LCTRL: return "left ctrl";
case NCKEY_LALT: return "left alt";
case NCKEY_LSUPER: return "left super";
case NCKEY_LHYPER: return "left hyper";
case NCKEY_LMETA: return "left meta";
case NCKEY_RSHIFT: return "right shift";
case NCKEY_RCTRL: return "right ctrl";
case NCKEY_RALT: return "right alt";
case NCKEY_RSUPER: return "right super";
case NCKEY_RHYPER: return "right hyper";
case NCKEY_RMETA: return "right meta";
case NCKEY_L3SHIFT: return "level 3 shift";
case NCKEY_L5SHIFT: return "level 5 shift";
case NCKEY_MOTION: return "mouse (no buttons pressed)";
case NCKEY_BUTTON1: return "mouse (button 1)";
case NCKEY_BUTTON2: return "mouse (button 2)";
case NCKEY_BUTTON3: return "mouse (button 3)";
case NCKEY_BUTTON4: return "mouse (button 4)";
case NCKEY_BUTTON5: return "mouse (button 5)";
case NCKEY_BUTTON6: return "mouse (button 6)";
case NCKEY_BUTTON7: return "mouse (button 7)";
case NCKEY_BUTTON8: return "mouse (button 8)";
case NCKEY_BUTTON9: return "mouse (button 9)";
case NCKEY_BUTTON10: return "mouse (button 10)";
case NCKEY_BUTTON11: return "mouse (button 11)";
default: return "unknown";
}
}
// Print the utf8 Control Pictures for otherwise unprintable ASCII
char32_t printutf8(char32_t kp){
if(kp <= NCKEY_ESC){
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 bool
dim_rows(const Plane* n){
Cell c;
for(unsigned y = 0 ; y < dimy ; ++y){
for(unsigned x = 0 ; x < dimx ; ++x){
if(n->get_at(y, x, &c) < 0){
n->release(c);
return false;
}
unsigned r, g, b;
c.get_fg_rgb8(&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(!c.set_fg_rgb8(r, g, b)){
n->release(c);
return false;
}
if(n->putc(y, x, c) < 0){
n->release(c);
return false;
}
if(c.is_double_wide()){
++x;
}
}
}
n->release(c);
return true;
}
void Tick(ncpp::NotCurses* nc, uint64_t sec) {
const std::lock_guard<std::mutex> lock(mtx);
// might fail on various geometry changes
if(ncuplot_add_sample(plot, sec, 0) == 0){
if(!nc->render()){
throw std::runtime_error("error rendering");
}
}
}
void Ticker(ncpp::NotCurses* nc) {
do{
std::this_thread::sleep_for(std::chrono::milliseconds{100});
const uint64_t sec = (timenow_to_ns() - start) / NANOSECS_IN_SEC;
Tick(nc, sec);
}while(!done);
}
char evtype_to_char(ncinput* ni){
switch(ni->evtype){
case EvType::Unknown:
return 'u';
case EvType::Press:
return 'P';
case EvType::Repeat:
return 'R';
case EvType::Release:
return 'L';
}
return 'X';
}
int input_demo(ncpp::NotCurses* nc) {
constexpr auto PLOTHEIGHT = 6;
constexpr auto PLOTWIDTH = 56;
auto n = nc->get_stdplane(&dimy, &dimx);
// FIXME no ncpp wrapper for Plane::pixelgeom?
unsigned celldimx, maxbmapx;
ncplane_pixel_geom(*n, nullptr, nullptr, nullptr, &celldimx, nullptr, &maxbmapx);
struct ncplane_options nopts = {
.y = static_cast<int>(dimy) - PLOTHEIGHT - 1,
.x = NCALIGN_CENTER,
.rows = PLOTHEIGHT,
.cols = PLOTWIDTH,
.userptr = nullptr,
.name = "plot",
.resizecb = ncplane_resize_realign,
.flags = NCPLANE_OPTION_HORALIGNED,
.margin_b = 0,
.margin_r = 0,
};
struct ncplane* pplane = ncplane_create(*n, &nopts);
if(pplane == nullptr){
return EXIT_FAILURE;
}
struct ncplot_options popts{};
// FIXME would be nice to switch over to exponential at some level
popts.flags = NCPLOT_OPTION_LABELTICKSD | NCPLOT_OPTION_PRINTSAMPLE;
popts.minchannels = popts.maxchannels = 0;
ncchannels_set_fg_rgb8(&popts.minchannels, 0x40, 0x50, 0xb0);
ncchannels_set_fg_rgb8(&popts.maxchannels, 0x40, 0xff, 0xd0);
popts.gridtype = static_cast<ncblitter_e>(NCBLIT_PIXEL);
plot = ncuplot_create(pplane, &popts, 0, 0);
if(!plot){
return EXIT_FAILURE;
}
n->set_fg_rgb8(0x00, 0x00, 0x00);
n->set_bg_rgb8(0xbb, 0x64, 0xbb);
n->styles_on(CellStyle::Underline);
if(n->putstr(n->get_dim_y() - 1, NCAlign::Center, "mash keys, yo. give that mouse some waggle! ctrl+d exits.") <= 0){
ncuplot_destroy(plot);
return -1;
}
n->styles_set(CellStyle::None);
n->set_bg_default();
if(!nc->render()){
ncuplot_destroy(plot);
return -1;
}
unsigned y = 0;
std::deque<wchar_t> cells;
char32_t r;
done = false;
start = timenow_to_ns();
std::thread tid(Ticker, nc);
ncinput ni;
while(errno = 0, (r = nc->get(true, &ni)) != (char32_t)-1){
if(r == 0){ // interrupted by signal
continue;
}
if((r == 'D' && ncinput_ctrl_p(&ni)) || r == NCKEY_EOF){
done = true;
tid.join();
ncuplot_destroy(plot);
return 0;
}
if(r == 'L' && ncinput_ctrl_p(&ni)){
mtx.lock();
if(!nc->refresh(nullptr, nullptr)){
mtx.unlock();
break;
}
mtx.unlock();
}
if(!n->cursor_move(y, 0)){
break;
}
n->set_fg_rgb8(0xd0, 0xd0, 0xd0);
n->printf("%c%c%c%c%c%c%c%c%c ",
ncinput_shift_p(&ni) ? 'S' : 's',
ncinput_alt_p(&ni) ? 'A' : 'a',
ncinput_ctrl_p(&ni) ? 'C' : 'c',
ncinput_super_p(&ni) ? 'U' : 'u',
ncinput_hyper_p(&ni) ? 'H' : 'h',
ncinput_meta_p(&ni) ? 'M' : 'm',
ncinput_capslock_p(&ni) ? 'X' : 'x',
ncinput_numlock_p(&ni) ? '#' : '.',
evtype_to_char(&ni));
if(r < 0x80){
n->set_fg_rgb8(128, 250, 64);
if(n->printf("ASCII: [0x%02x (%03d)] '%lc'", r, r,
(wint_t)(iswprint(r) ? r : printutf8(r))) < 0){
break;
}
}else{
if(nckey_synthesized_p(r)){
n->set_fg_rgb8(250, 64, 128);
if(n->printf("Special: [0x%02x (%02d)] '%s'", r, r, nckeystr(r)) < 0){
break;
}
if(NCKey::IsMouse(r)){
if(n->printf(-1, NCAlign::Right, " %d/%d", ni.x, ni.y) < 0){
break;
}
}
}else{
n->set_fg_rgb8(64, 128, 250);
n->printf("Unicode: [0x%08x] '%s'", r, ni.utf8);
}
}
if(ni.eff_text[0] != ni.id || ni.eff_text[1] != 0){
n->printf(" effective text '");
for (int c=0; ni.eff_text[c]!=0; c++){
unsigned char egc[5]={0};
if(notcurses_ucs32_to_utf8(&ni.eff_text[c], 1, egc, 4)>=0){
n->printf("%s", egc);
}
}
n->printf("'");
}
unsigned x;
n->get_cursor_yx(nullptr, &x);
for(unsigned i = x ; i < n->get_dim_x() ; ++i){
n->putc(' ');
}
if(!dim_rows(n)){
break;
}
const uint64_t sec = (timenow_to_ns() - start) / NANOSECS_IN_SEC;
mtx.lock();
if(ncuplot_add_sample(plot, sec, 1)){
mtx.unlock();
break;
}
if(!nc->render()){
mtx.unlock();
ncuplot_destroy(plot);
throw std::runtime_error("error rendering");
}
mtx.unlock();
if(++y >= dimy - PLOTHEIGHT - 1){
y = 0;
}
while(cells.size() >= dimy - 3u){
cells.pop_back();
}
cells.push_front(r);
}
int e = errno;
if(r == (char32_t)-1 && e){
std::cerr << "Error reading from terminal (" << strerror(e) << "?)\n";
}
done = true;
tid.join();
ncuplot_destroy(plot);
return 0;
}
static void
usage(const char* arg0, FILE* fp){
fprintf(fp, "usage: %s [ -v ] [ -m ]\n", arg0);
if(fp == stderr){
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
int main(int argc, char** argv){
if(setlocale(LC_ALL, "") == nullptr){
return EXIT_FAILURE;
}
notcurses_options nopts{};
nopts.margin_t = 2;
nopts.margin_l = 2;
nopts.margin_r = 2;
nopts.margin_b = 2;
nopts.loglevel = NCLOGLEVEL_ERROR;
bool nomice = false;
int opt;
while((opt = getopt(argc, argv, "vm")) != -1){
switch(opt){
case 'm':
nomice = true;
break;
case 'v':
nopts.loglevel = NCLOGLEVEL_TRACE;
break;
default:
usage(argv[0], stderr);
break;
}
}
if(argv[optind]){ // non-option argument was provided
usage(argv[0], stderr);
}
nopts.flags = NCOPTION_INHIBIT_SETLOCALE;
NotCurses nc(nopts);
if(!nomice){
nc.mouse_enable(NCMICE_ALL_EVENTS);
}
int ret = input_demo(&nc);
if(!nc.stop() || ret){
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}