#define AL_LOG_SECTION "taro" #include #include #include #include #include #include #include #include #include "db.h" #include "file_ext.h" #include "config.h" #define TIMER_UPDATE_BASE 1.f #define TIMER_TICK(delay) MAX(TIMER_UPDATE_BASE * (delay / 1000000.f), 0.001f) #define TEXT_UPDATE_RATE 0.4f #define FLASH_DURATION 0.4f #define UI_RESERVE_WIDTH 33 #define UI_RESERVE_HEIGHT 2 #define GRID_TILE_WIDTH 22 // img = 20 #define GRID_TILE_HEIGHT 11 // img = 10 #define GRID_TEXT_WIDTH (GRID_TILE_WIDTH - 4) #define INFO_X_OFFSET 2 #define INFO_Y_OFFSET 1 #define INFO_WIDTH 30 // INFO_HEIGHT = term_rows + UI_RESERVE_HEIGHT #define INFO_IMG_WIDTH 24 #define INFO_IMG_HEIGHT 11 #define INFO_TEXT_WIDTH (GRID_TEXT_WIDTH + 6) #define PROP_TEXT_WIDTH 16 #define MAX_MONITOR_NAME 255 struct scrolling_text { wstr *s; s32 y; s32 x; s32 width; u32 index; s32 skip; }; struct image { u32 hash; bool gif; bool errored; struct ncvisual *v; }; struct image_pool { u32 max; u32 chunk_size; array(struct image) images; }; struct tile { struct ncplane *p, *ip; struct entry *e; bool blitted; struct scrolling_text text; }; struct monitors { array(char *) monitors; s32 selected_monitor; s32 set_monitor; str selection_path; }; struct grid { struct ncplane *p; s32 tiles_x; s32 tiles_y; u32 tiles_per_page; s32 previous_index; s32 min_index; s32 max_index; s32 selected_index; s32 set_index; f32 flash; array(struct tile) tiles; }; struct info_prop { struct prop *p; struct scrolling_text text; }; struct info { struct ncplane *ip; struct entry *e; bool blitted; struct scrolling_text text; u32 max_props; array(struct info_prop) props; s32 selected_prop; }; enum { MENU_GRID = 0, MENU_INFO, MENU_MONITORS }; struct ui { struct notcurses *nc; bool use_pixel_blit; struct nn_event_loop loop; struct nn_poll input_poll; u64 delay; struct nn_timer timer; bool layed_out; f32 text_update; u32 term_rows; u32 term_cols; u8 selected_menu; u8 previous_menu; struct ncplane *p, *mp, *gp, *ip; struct monitors m; struct grid g; struct info n; struct image_pool pool; struct db db; struct config conf; }; // ------- // Client // ------- static s32 get_entry_from_config(struct ui *u) { s32 ret = -1; str config; if (nn_file_open_and_read_wholly(&u->m.selection_path, &config, false)) { u32 index = al_str_find(&config, '|'); if (index != AL_STR_NO_POS && index > 2) { str id = al_str_substr(&config, 0, index - 2); struct entry *e; al_array_foreach_ptr(u->db.entries, i, e) { if (al_str_eq(&e->id, &id)) { ret = i; break; } } } al_str_free(&config); } return ret; } static void select_monitor(struct ui *u) { if (u->m.set_monitor == -1) return; u->m.set_monitor = u->m.selected_monitor; al_str_free(&u->m.selection_path); al_str_clone(&u->m.selection_path, &u->conf.config_dir); al_str_cat(&u->m.selection_path, &al_str_c("/")); char *monitor = al_array_at(u->m.monitors, u->m.set_monitor); al_str_cat(&u->m.selection_path, &al_str_cr(monitor)); al_str_cat(&u->m.selection_path, &al_str_c("/selection")); u->g.set_index = get_entry_from_config(u); if (u->g.set_index >= 0) { u->g.selected_index = u->g.set_index; } } static void write_selected_entry_to_config(struct ui *u) { if (u->m.set_monitor == -1) return; str config; if (u->g.set_index == -1) { al_str_from(&config, "||"); } else { struct entry *e = &al_array_at(u->db.entries, u->g.set_index); al_str_clone(&config, &e->id); al_str_cat(&config, e->has_package ? &al_str_c("_p") : &al_str_c("_d")); al_str_cat(&config, &al_str_c("|false,100|")); struct prop *p; al_array_foreach_ptr(e->props, i, p) { switch (p->type) { case PROP_TYPE_BOOL: { bool value = *((bool *)p->value); al_str_cat(&config, value ? &al_str_c("true") : &al_str_c("false")); break; } } al_str_cat(&config, &al_str_c(",")); } if (al_str_atr(&config, 1) == ',') config.length--; } nn_file_open_and_replace(&u->m.selection_path, &config, true); } // --------------- // User Interface // --------------- static bool init_info(struct info *n) { al_array_init(n->props); return true; } static bool init_image_pool(struct image_pool *pool) { pool->chunk_size = 0; pool->max = 0; al_array_init(pool->images); return true; } static bool init_grid(struct grid *g) { al_array_init(g->tiles); return true; } static s32 monitor_cmp(const void *va, const void *vb) { return strncmp(*(char **)va, *(char **)vb, MAX_MONITOR_NAME); } static bool init_monitors(struct ui *u, struct monitors *m) { al_array_init(m->monitors); str monitors_path; al_str_clone(&monitors_path, &u->conf.config_dir); al_str_cat(&monitors_path, &al_str_c("/monitors")); str monitors; bool ret = nn_file_open_and_read_wholly(&monitors_path, &monitors, false); al_str_free(&monitors_path); if (!ret) { log_error("failed to open monitor list, has `tarod` run?"); return false; } str monitor_dir; str monitor = al_str_null(); while (al_str_get_line(&monitors, '\n', &monitor)) { al_str_clone(&monitor_dir, &u->conf.config_dir); al_str_cat(&monitor_dir, &al_str_c("/")); al_str_cat(&monitor_dir, &monitor); if (nn_dir_exists(&monitor_dir)) { char *c_str = al_str_to_c_str(&monitor); al_array_push(m->monitors, c_str); } al_str_free(&monitor_dir); } if (!u->m.monitors.count) { u->m.set_monitor = -1; u->m.selected_monitor = -1; } al_array_sort(m->monitors, char *, monitor_cmp); u->g.set_index = -1; u->g.selected_index = -1; if (m->selected_monitor >= 0) { select_monitor(u); } if (u->db.entries.count > 0) { // set_index will be set in set_grid_tiles() otherwise. if (u->g.set_index == -1) u->g.selected_index = 0; } else { u->selected_menu = MENU_MONITORS; } u->previous_menu = u->selected_menu; return true; } static s32 void_print_callback(void *userdata, u8 level, char *message) { (void)userdata; (void)level; (void)message; return 0; } static bool init_ui(struct ui *u) { if (!init_monitors(u, &u->m)) return false; if (!init_grid(&u->g)) return false; if (!init_image_pool(&u->pool)) return false; if (!init_info(&u->n)) return false; if (!(u->nc = notcurses_init(NULL, stdin))) { log_error("failed to initialize notcurses"); return false; } //al_printf("%u\n", notcurses_palette_size(u->nc)); al_set_print(void_print_callback, NULL); return true; } static inline void ncplane_maybe_set_styles(struct ui *u, struct ncplane *p, u32 style) { // Setting text to bold messes up colors on some terminals. if (u->use_pixel_blit) ncplane_set_styles(p, style); } struct image *get_image_from_pool(struct image_pool *pool, str *path) { u32 hash = al_str_hash(path); struct image *cached; al_array_foreach_ptr(pool->images, i, cached) { if (cached->hash == hash) return cached; } if (pool->images.count >= pool->max) { // This should at least be based on LRU. for (u32 i = 0; i < pool->chunk_size; i++) { ncvisual_destroy(al_array_at(pool->images, i).v); } al_array_remove_range(pool->images, 0, pool->chunk_size); } u32 index = al_str_rfind(path, '.'); bool is_gif = index != AL_STR_NO_POS && al_str_cmp(path, &al_str_c(".gif"), index, 4) == 0; char *c_str = al_str_to_c_str(path); struct ncvisual *v = ncvisual_from_file(c_str); al_free(c_str); if (!v) return NULL; al_array_push(pool->images, ((struct image){ hash, is_gif, false, v })); return &al_array_last(pool->images); } static bool layout_ui(struct ui *u); static void draw_monitors(struct ui *u, struct monitors *m); static void draw_grid_tiles(struct ui *u, struct grid *g, bool update_text); static void draw_info(struct ui *u, struct info *n, bool update_text); static s32 resize_cb(struct ncplane *p) { struct ui *u = ncplane_userptr(p); if (!u) return 0; notcurses_stddim_yx(u->nc, &u->term_rows, &u->term_cols); if (!layout_ui(u)) { u->layed_out = false; return 0; } u->layed_out = true; struct tile *t; al_array_foreach_ptr(u->g.tiles, i, t) { t->blitted = false; ncplane_erase(t->ip); } u->n.blitted = false; ncplane_erase(u->n.ip); // To not break images we need to erase then render them in 2 steps. notcurses_refresh(u->nc, NULL, NULL); notcurses_render(u->nc); draw_monitors(u, &u->m); draw_grid_tiles(u, &u->g, false); draw_info(u, &u->n, false); notcurses_render(u->nc); return 0; } static bool wide_string_to_width(wstr *w, u32 index, s32 max_width, void (*func)(void *, wchar_t), void *userdata) { u32 i = index; s32 width = 0; do { wchar_t wc = al_wstr_at(w, i); width += al_wchar_width(wc); if (width >= max_width) { break; } func(userdata, wc); } while (++i < w->length); return i == w->length; } static void set_scrolling_text(struct scrolling_text *text, wstr *s, s32 y, s32 x, s32 width) { text->s = s; text->y = y; text->x = x; text->width = width; text->index = 0; text->skip = al_wchar_width(al_wstr_at(s, text->index)); } static void draw_func(void *userdata, wchar_t wc) { struct ncplane *p = (struct ncplane *)userdata; ncplane_putwc(p, wc); } static void draw_scrolling_text(struct ncplane *p, struct scrolling_text *text, bool update) { ncplane_cursor_move_yx(p, text->y, text->x); bool end = wide_string_to_width(text->s, text->index, text->width, draw_func, p); if (update && !--text->skip) { text->skip = al_wchar_width(al_wstr_at(text->s, text->index)); text->index = end ? 0 : text->index + 1; } } static void maybe_destroy_info(struct info *n) { if (n->ip) { ncplane_destroy(n->ip); n->ip = NULL; } } static void set_info_entry(struct ui *u, struct info *n) { struct entry *e = &al_array_at(u->db.entries, u->g.selected_index); if (e == n->e) return; n->e = e; n->blitted = false; s32 width = al_wstr_width(&u->n.e->title); s32 tx = 2 + ((width < INFO_TEXT_WIDTH) ? (INFO_TEXT_WIDTH - width) / 2 : 0); set_scrolling_text(&n->text, &u->n.e->title, INFO_IMG_HEIGHT + 1, tx, INFO_TEXT_WIDTH); n->props.count = 0; struct prop *p; struct info_prop *ip; al_array_foreach_ptr(u->n.e->props, i, p) { al_array_push(n->props, (struct info_prop){ 0 }); ip = &al_array_last(n->props); ip->p = p; s32 ty = INFO_IMG_HEIGHT + 3 + ((i % n->max_props) * 2); set_scrolling_text(&ip->text, &p->title, ty, 0, PROP_TEXT_WIDTH); } n->selected_prop = 0; } static bool layout_info(struct ui *u, struct info *n, struct ncplane *parent, s32 height) { maybe_destroy_info(n); n->max_props = (height - INFO_IMG_HEIGHT - 3) / 2; if (!n->max_props) return false; struct ncplane_options nopts = { 0 }; nopts.cols = INFO_IMG_WIDTH; nopts.rows = INFO_IMG_HEIGHT; nopts.x = 0; nopts.y = 1; if (!(n->ip = ncplane_create(parent, &nopts))) { maybe_destroy_info(n); return false; } if (u->g.selected_index >= 0) { set_info_entry(u, &u->n); } return true; } static void maybe_destroy_grid(struct grid *g) { if (!g->p) return; struct tile *t; al_array_foreach_ptr(g->tiles, i, t) { ncplane_destroy(t->ip); t->ip = NULL; ncplane_destroy(t->p); t->p = NULL; } g->tiles.count = 0; ncplane_destroy(g->p); g->p = NULL; } static void set_grid_tiles(struct ui *u, struct grid *g) { u32 selected = (g->selected_index >= 0) ? g->selected_index : g->set_index; u32 index = selected - (selected % g->tiles_per_page); if (g->previous_index >= 0 && index == (u32)g->previous_index) { return; } g->previous_index = index; g->min_index = index; g->max_index = index; for (s32 y = 0; y < g->tiles_y; y++) { for (s32 x = 0; x < g->tiles_x; x++) { struct tile *t = &al_array_at(g->tiles, y * g->tiles_x + x); ncplane_erase(t->ip); t->blitted = false; ncplane_erase(t->p); while (g->max_index < (s32)u->db.entries.count) { t->e = &al_array_at(u->db.entries, g->max_index); if (db_ensure_entry_loaded(&u->db, t->e)) { break; } // Entry failed to load. if (g->set_index == g->max_index) { g->set_index = -1; } else if (g->set_index > g->max_index) { g->set_index--; } } al_assert(g->max_index <= (s32)u->db.entries.count); if (g->max_index == (s32)u->db.entries.count) { t->e = NULL; t->text.s = NULL; ncplane_erase(t->ip); continue; } s32 width = al_wstr_width(&t->e->title); s32 tx = 2 + ((width < GRID_TEXT_WIDTH) ? (GRID_TEXT_WIDTH - width) / 2 : 0); set_scrolling_text(&t->text, &t->e->title, GRID_TILE_HEIGHT - 1, tx, GRID_TEXT_WIDTH); g->max_index++; }} if (g->selected_index == -1) { if (g->set_index == -1) { // set_index failed to load above. g->selected_index = 0; set_grid_tiles(u, g); return; } else { g->selected_index = g->set_index; } } g->max_index--; // This can happen in grid_next_page() and shuffle. Any other case where // selected_index goes over max_index is undefined. // Handling entry load failure in this way saves us from a lot of preloading. if (g->selected_index > g->max_index) { g->selected_index = g->max_index; } } static bool layout_grid(struct ui *u, struct grid *g, struct ncplane *parent, s32 width, s32 height) { maybe_destroy_grid(g); g->tiles_x = width / GRID_TILE_WIDTH; g->tiles_y = height / GRID_TILE_HEIGHT; if (!g->tiles_x || !g->tiles_y) { // Window is too small. return false; } g->tiles_per_page = g->tiles_x * g->tiles_y; g->previous_index = -1; // Reserve space for 4 pages, but never shrink. if (u->pool.chunk_size < g->tiles_per_page) { u->pool.chunk_size = g->tiles_per_page; u->pool.max = u->pool.chunk_size * 4; al_array_reserve(u->pool.images, u->pool.max); } s32 inner_width = width; s32 inner_height = height; while (inner_width && inner_width % g->tiles_x != 0) inner_width--; while (inner_height && inner_height % g->tiles_y != 0) inner_height--; struct ncplane_options nopts = { 0 }; nopts.cols = inner_width; nopts.rows = inner_height; nopts.x = (width - (g->tiles_x * GRID_TILE_WIDTH)) / 2; nopts.y = (height - (g->tiles_y * GRID_TILE_HEIGHT)) / 2; if (!(g->p = ncplane_create(parent, &nopts))) { return false; } struct tile t = { 0 }; for (s32 y = 0; y < g->tiles_y; y++) { for (s32 x = 0; x < g->tiles_x; x++) { nopts.cols = GRID_TILE_WIDTH; nopts.rows = GRID_TILE_HEIGHT; nopts.x = x * GRID_TILE_WIDTH; nopts.y = y * GRID_TILE_HEIGHT; if (!(t.p = ncplane_create(g->p, &nopts))) { maybe_destroy_grid(g); return false; } nopts.cols = GRID_TILE_WIDTH - 2; nopts.rows = GRID_TILE_HEIGHT - 2; nopts.x = 1; nopts.y = 1; if (!(t.ip = ncplane_create(t.p, &nopts))) { maybe_destroy_grid(g); return false; } al_array_push(g->tiles, t); }} if (u->db.entries.count > 0) { set_grid_tiles(u, g); } return true; } static void maybe_destroy_ui(struct ui *u) { if (u->ip) { ncplane_destroy(u->ip); u->ip = NULL; } if (u->gp) { ncplane_destroy(u->gp); u->gp = NULL; } if (u->mp) { ncplane_destroy(u->mp); u->mp = NULL; } if (u->p) { ncplane_destroy(u->p); u->p = NULL; } } bool layout_ui(struct ui *u) { bool even = u->term_cols % 2 == 0; u32 rwidth = even ? UI_RESERVE_WIDTH + 1 : UI_RESERVE_WIDTH; u32 rheight = even ? UI_RESERVE_HEIGHT + 1 : UI_RESERVE_HEIGHT; if (u->term_cols < GRID_TILE_WIDTH + rwidth || u->term_rows < GRID_TILE_HEIGHT + rheight) { return false; } maybe_destroy_ui(u); struct ncplane_options nopts = { 0 }; nopts.rows = u->term_rows; nopts.cols = u->term_cols; if (!(u->p = ncplane_create(notcurses_stdplane(u->nc), &nopts))) { return false; } s32 cols = (s32)u->term_cols - rwidth; s32 rows = (s32)u->term_rows - rheight; if (cols < 0 || rows < 0) { maybe_destroy_ui(u); return false; } nopts.x = 1; nopts.y = 1; nopts.cols = cols - 1; nopts.rows = 1; if (!(u->mp = ncplane_create(u->p, &nopts))) { maybe_destroy_ui(u); return false; } nopts.x = 1; nopts.y = 2; nopts.cols = cols - 1; nopts.rows = rows - 2; if (!(u->gp = ncplane_create(u->p, &nopts))) { maybe_destroy_ui(u); return false; } if (!layout_grid(u, &u->g, u->gp, nopts.cols, nopts.rows)) { maybe_destroy_ui(u); return false; } nopts.x = nopts.cols + (even ? INFO_X_OFFSET + 1 : INFO_X_OFFSET); nopts.y = INFO_Y_OFFSET; nopts.cols = INFO_WIDTH; nopts.rows = u->term_rows - rheight; if (!(u->ip = ncplane_create(u->p, &nopts))) { maybe_destroy_ui(u); return false; } if (!layout_info(u, &u->n, u->ip, nopts.rows)) { maybe_destroy_ui(u); return false; } return true; } void draw_monitors(struct ui *u, struct monitors *m) { if (!u->layed_out) return; char *monitor; const char *no_monitors = "NO MONITORS"; s32 width = 0; if (u->m.selected_monitor == -1) { width = al_strlen(no_monitors); } else { al_array_foreach(m->monitors, i, monitor) { width += strlen(monitor) + 1; } } ncplane_maybe_set_styles(u, u->mp, NCSTYLE_BOLD); s32 x = ((ncplane_dim_x(u->mp) - 1) - width) / 2; ncplane_cursor_move_yx(u->mp, 0, x); if (u->m.selected_monitor == -1) { if (u->selected_menu == MENU_MONITORS) { ncplane_set_bg_palindex(u->mp, 8); ncplane_set_fg_palindex(u->mp, 0); } else { ncplane_set_bg_palindex(u->mp, 7); ncplane_set_fg_palindex(u->mp, 0); } ncplane_putstr(u->mp, no_monitors); } else { al_array_foreach(m->monitors, i, monitor) { if (u->selected_menu == MENU_MONITORS && i == (u32)m->selected_monitor) { ncplane_set_bg_palindex(u->mp, 8); ncplane_set_fg_palindex(u->mp, 0); } else if (i == (u32)m->set_monitor) { ncplane_set_bg_palindex(u->mp, 7); ncplane_set_fg_palindex(u->mp, 0); } else { ncplane_set_fg_default(u->mp); ncplane_set_bg_default(u->mp); } ncplane_cursor_move_rel(u->mp, 0, 1); ncplane_putstr(u->mp, monitor); } } } void draw_info(struct ui *u, struct info *n, bool update_text) { if (!u->layed_out || !n->e) return; ncplane_erase(u->ip); struct ncvisual_options vopts = { 0 }; vopts.scaling = u->use_pixel_blit ? NCSCALE_SCALE_HIRES : NCSCALE_STRETCH; vopts.blitter = u->use_pixel_blit ? NCBLIT_PIXEL : NCBLIT_2x1; vopts.flags = NCVISUAL_OPTION_HORALIGNED; vopts.x = NCALIGN_CENTER; struct image *img = get_image_from_pool(&u->pool, &n->e->preview_path); if (img && !n->blitted && !img->errored) { ncplane_erase(n->ip); img->errored = img->gif && ncvisual_decode_loop(img->v) < 0; if (!img->errored) { vopts.n = n->ip; ncvisual_blit(u->nc, img->v, &vopts); n->blitted = !img->gif; u->delay = ncvisual_get_duration(img->v); // Default 100fps gifs to 10fps, seems like some weird wallpaper engine behavior. if (u->delay == 10000) u->delay = 100000; } } else if (!img) { ncplane_erase(n->ip); } ncplane_set_fg_default(u->ip); draw_scrolling_text(u->ip, &n->text, update_text); u32 i, index, selected_index = n->selected_prop - (n->selected_prop % n->max_props); struct info_prop *ip = NULL; for (i = 0; i < n->max_props; i++) { index = i + selected_index; if (index >= n->props.count) { break; } ip = &al_array_at(n->props, index); if (u->selected_menu == MENU_INFO && index == (u32)n->selected_prop) { ncplane_set_fg_palindex(u->ip, 8); } else { ncplane_set_fg_default(u->ip); } draw_scrolling_text(u->ip, &ip->text, update_text); switch (ip->p->type) { case PROP_TYPE_BOOL: { char *c = *((bool *)ip->p->value) ? "[x]" : "[ ]"; ncplane_putstr_yx(u->ip, ip->text.y, ip->text.width + 2, c); break; } case PROP_TYPE_SLIDER: { char c[16]; al_snprintf(c, sizeof(c), "%.2f", ((struct slider_value *)ip->p->value)->value); ncplane_putstr_yx(u->ip, ip->text.y, ip->text.width + 2, c); break; } } } if (ip && i == n->max_props && i != n->props.count) { ncplane_putstr_yx(u->ip, ip->text.y + 1, (INFO_IMG_WIDTH - 1) / 2, "V"); } } static void set_tile_highlight(struct tile *t, s32 findex, s32 bindex) { struct nccell ncl; nccell_init(&ncl); nccell_load(t->p, &ncl, " "); nccell_set_bg_palindex(&ncl, bindex); nccell_set_fg_default(&ncl); ncplane_polyfill_yx(t->p, 1, 0, &ncl); nccell_release(t->p, &ncl); // Portion of the top row. ncplane_set_bg_default(t->p); ncplane_set_fg_palindex(t->p, bindex); for (s32 i = 0; i < GRID_TILE_WIDTH; i++) { ncplane_putwc_yx(t->p, 0, i, L'▃'); } (void)findex; //ncplane_set_fg_palindex(t->p, findex); // If we set both fg and bg palindex here, the fg color // will carry over to the info prop list for a split second. // This might be a notcurses bug, I need to look more carefully. ncplane_set_fg_rgb8(t->p, 0, 0, 0); ncplane_set_bg_palindex(t->p, bindex); } void draw_grid_tiles(struct ui *u, struct grid *g, bool update_text) { if (!u->layed_out) return; struct ncvisual_options vopts = { 0 }; vopts.scaling = u->use_pixel_blit ? NCSCALE_SCALE_HIRES : NCSCALE_STRETCH; vopts.blitter = u->use_pixel_blit ? NCBLIT_PIXEL : NCBLIT_2x1; vopts.flags = NCVISUAL_OPTION_HORALIGNED; vopts.x = NCALIGN_CENTER; struct tile *t; al_array_foreach_ptr(g->tiles, i, t) { if (t->e && t->e->has_preview) { struct image *img = get_image_from_pool(&u->pool, &t->e->preview_path); if (img && !t->blitted && !img->errored) { vopts.n = t->ip; ncvisual_blit(u->nc, img->v, &vopts); t->blitted = true; } } } s32 index = g->selected_index - (g->selected_index % g->tiles_per_page); al_array_foreach_ptr(g->tiles, i, t) { ncplane_erase(t->p); if (!t->e) continue; if (index == g->selected_index && u->selected_menu == MENU_GRID) { if (g->flash >= 0.f) { if (index == g->set_index) { set_tile_highlight(t, 0, 7); ncplane_maybe_set_styles(u, t->p, NCSTYLE_BOLD); } else if (g->set_index == -1) { ncplane_set_fg_default(t->p); ncplane_set_bg_default(t->p); ncplane_maybe_set_styles(u, t->p, NCSTYLE_NONE); } g->flash -= TIMER_TICK(u->delay); } else { set_tile_highlight(t, 0, 8); ncplane_maybe_set_styles(u, t->p, NCSTYLE_BOLD); } } else if (index == g->set_index && g->set_index >= 0) { set_tile_highlight(t, 0, 7); ncplane_maybe_set_styles(u, t->p, NCSTYLE_BOLD); } else { ncplane_set_fg_default(t->p); ncplane_set_bg_default(t->p); ncplane_maybe_set_styles(u, t->p, NCSTYLE_NONE); } if (t->text.s && !al_wstr_is_empty(t->text.s)) { draw_scrolling_text(t->p, &t->text, update_text); } index++; } } static bool handle_input_monitors(struct ui *u, struct monitors *m, struct ncinput *input) { bool should_redraw_monitors = false; bool should_redraw_grid = false; bool should_redraw_info = false; switch (input->id) { case NCKEY_LEFT: case 'h': { if (m->selected_monitor - 1 < 0) { return false; } m->selected_monitor--; should_redraw_monitors = true; break; } case NCKEY_RIGHT: case 'l': { if ((u32)(m->selected_monitor + 1) >= m->monitors.count) { return false; } m->selected_monitor++; should_redraw_monitors = true; break; } case NCKEY_DOWN: case 'j': { if (u->db.entries.count > 0) { u->selected_menu = MENU_GRID; should_redraw_monitors = true; should_redraw_grid = true; } break; } case NCKEY_RETURN: case NCKEY_SPACE: { select_monitor(u); u->selected_menu = MENU_GRID; should_redraw_monitors = true; should_redraw_grid = true; should_redraw_info = true; break; } } if (should_redraw_monitors) { draw_monitors(u, m); } if (should_redraw_grid) { set_grid_tiles(u, &u->g); draw_grid_tiles(u, &u->g, false); } if (should_redraw_info) { set_info_entry(u, &u->n); draw_info(u, &u->n, false); } return should_redraw_monitors || should_redraw_grid || should_redraw_info; } static bool handle_input_info(struct ui *u, struct info *n, struct ncinput *input) { bool should_redraw_info = false; switch (input->id) { case NCKEY_UP: case 'k': { if (n->selected_prop - 1 < 0) { return false; } n->selected_prop--; should_redraw_info = true; break; } case NCKEY_DOWN: case 'j': { if ((u32)(n->selected_prop + 1) >= n->props.count) { return false; } n->selected_prop++; should_redraw_info = true; break; } case NCKEY_RETURN: case NCKEY_SPACE: { if (!n->props.count) return false; struct prop *p = &al_array_at(n->e->props, n->selected_prop); if (p->type == PROP_TYPE_BOOL) { *((bool *)p->value) = !*((bool *)p->value); write_selected_entry_to_config(u); should_redraw_info = true; } break; } case NCKEY_LEFT: case 'h': case NCKEY_RIGHT: case 'l': { if (!n->props.count) return false; struct prop *p = &al_array_at(n->e->props, n->selected_prop); if (p->type == PROP_TYPE_SLIDER) { struct slider_value *value = (struct slider_value *)p->value; if (input->id == NCKEY_RIGHT) { if (value->value == value->max) { break; } if (value->value + value->step <= value->max) { value->value += value->step; } else { value->value = value->max; } } else if (input->id == NCKEY_LEFT) { if (value->value == value->min) { break; } if (value->value - value->step >= value->min) { value->value -= value->step; } else { value->value = value->min; } } write_selected_entry_to_config(u); should_redraw_info = true; } break; } } if (should_redraw_info) draw_info(u, n, false); return should_redraw_info; } static bool grid_next_page(struct ui *u, struct grid *g, s32 offset) { if (!u->db.entries.count) { return false; } s32 max = (s32)u->db.entries.count - 1; if (g->selected_index == max) { return false; } g->selected_index += g->tiles_per_page - offset; if (g->selected_index > max) { g->selected_index = max; } return true; } static bool grid_previous_page(struct grid *g, s32 offset) { if (g->selected_index <= 0) { return false; } g->selected_index -= g->tiles_per_page - offset; if (g->selected_index < 0) { g->selected_index = 0; } return true; } static bool handle_input_grid(struct ui *u, struct grid *g, struct ncinput *input) { bool should_update_grid = false; bool should_redraw_grid = false; bool should_redraw_info = false; bool should_redraw_monitors = false; switch (input->id) { case NCKEY_LEFT: case 'h': { if (g->selected_index - 1 < 0) { return false; } if (g->selected_index % g->tiles_x == 0) { should_update_grid = grid_previous_page(&u->g, g->tiles_x - 1); } else { g->selected_index--; } should_redraw_info = true; should_redraw_grid = true; break; } case NCKEY_RIGHT: case 'l': { if (g->selected_index + 1 >= (s32)u->db.entries.count) { return false; } if ((g->selected_index + 1) % g->tiles_x == 0) { should_update_grid = grid_next_page(u, &u->g, g->tiles_x - 1); } else { g->selected_index++; } should_redraw_info = true; should_redraw_grid = true; break; } case NCKEY_UP: case 'k': { if (g->selected_index - g->tiles_x < g->min_index) { u->selected_menu = MENU_MONITORS; should_redraw_monitors = true; } else { g->selected_index -= g->tiles_x; should_redraw_info = true; } should_redraw_grid = true; break; } case NCKEY_DOWN: case 'j': { if (g->selected_index + g->tiles_x > g->max_index) { return false; } g->selected_index += g->tiles_x; should_redraw_info = true; should_redraw_grid = true; break; } case '0': { g->selected_index -= g->selected_index % g->tiles_x; should_redraw_info = true; should_redraw_grid = true; break; } case '4': { // '$' in vim. g->selected_index += g->tiles_x - (g->selected_index % g->tiles_x) - 1; should_redraw_info = true; should_redraw_grid = true; break; } case 'f': { if (grid_next_page(u, &u->g, 0)) { should_update_grid = true; should_redraw_info = true; should_redraw_grid = true; } break; } case 'b': { if (grid_previous_page(&u->g, 0)) { should_update_grid = true; should_redraw_info = true; should_redraw_grid = true; } break; } case 'N': case 'n': { if (g->selected_index < 0) { return false; } if (input->id == 'N' || input->modifiers & NCKEY_MOD_SHIFT) { if (g->selected_index == 0) { return false; } g->selected_index -= 1; } else { if ((u32)g->selected_index + 1 >= u->db.entries.count) { return false; } g->selected_index += 1; } if (g->selected_index > g->max_index || g->selected_index < g->min_index) { should_update_grid = true; } g->set_index = g->selected_index; g->flash = FLASH_DURATION; write_selected_entry_to_config(u); should_redraw_grid = true; should_redraw_info = true; break; } case 'r': { g->set_index = -1; g->flash = FLASH_DURATION; write_selected_entry_to_config(u); should_redraw_grid = true; should_redraw_info = true; break; } case 's': { if (!u->db.entries.count) { return false; } s32 max = (s32)u->db.entries.count - 1; g->selected_index = al_rand() % max; should_update_grid = true; should_redraw_info = true; should_redraw_grid = true; break; } case NCKEY_RETURN: case NCKEY_SPACE: { if (g->selected_index < 0) { return false; } g->set_index = g->selected_index; g->flash = FLASH_DURATION; write_selected_entry_to_config(u); should_redraw_grid = true; should_redraw_info = true; break; } } if (should_update_grid) { set_grid_tiles(u, g); } if (should_redraw_grid) { draw_grid_tiles(u, g, false); } if (should_redraw_info) { set_info_entry(u, &u->n); draw_info(u, &u->n, false); } if (should_redraw_monitors) { draw_monitors(u, &u->m); } return should_redraw_grid || should_redraw_info; } static void input_poll_callback(void *userdata, s32 revents) { struct ui *u = (struct ui *)userdata; (void)revents; struct ncinput input; bool should_render = false; u32 ret; while (1) { ret = notcurses_get_nblock(u->nc, &input); if (ret == (u32)-1 || ret == 0) break; if (input.evtype == NCTYPE_PRESS || input.evtype == NCTYPE_UNKNOWN) { if (input.id == NCKEY_TAB) { switch (u->selected_menu) { case MENU_MONITORS: break; case MENU_GRID: if (u->n.props.count > 0) { u->selected_menu = MENU_INFO; } break; case MENU_INFO: u->selected_menu = MENU_GRID; break; } draw_info(u, &u->n, false); draw_grid_tiles(u, &u->g, false); should_render = true; } else if (input.id == 'm') { if (u->selected_menu != MENU_MONITORS) { u->previous_menu = u->selected_menu; u->selected_menu = MENU_MONITORS; } else { u->selected_menu = u->previous_menu; } draw_monitors(u, &u->m); switch (u->previous_menu) { case MENU_GRID: draw_grid_tiles(u, &u->g, false); break; case MENU_INFO: draw_info(u, &u->n, false); break; } should_render = true; } else if (input.id == 'q') { nn_event_loop_break(&u->loop); } else { switch (u->selected_menu) { case MENU_GRID: should_render |= handle_input_grid(u, &u->g, &input); break; case MENU_INFO: should_render |= handle_input_info(u, &u->n, &input); break; case MENU_MONITORS: should_render |= handle_input_monitors(u, &u->m, &input); break; } } } } if (should_render) notcurses_render(u->nc); } static void timer_callback(void *userdata, struct nn_timer *timer) { struct ui *u = (struct ui *)userdata; (void)timer; if (!u->layed_out) { notcurses_render(u->nc); return; } if (u->text_update <= 0.f) { u->text_update = TEXT_UPDATE_RATE; draw_grid_tiles(u, &u->g, true); draw_info(u, &u->n, true); } else { u->text_update -= TIMER_TICK(u->delay); draw_grid_tiles(u, &u->g, false); draw_info(u, &u->n, false); } draw_monitors(u, &u->m); notcurses_render(u->nc); nn_timer_set_repeat(&u->timer, NNWT_TS_FROM_USEC(u->delay)); nn_timer_again(&u->timer); } static struct ui u = { 0 }; s32 main(void) { s32 exit_status = EXIT_FAILURE; if (!nn_common_init(NULL)) return exit_status; if (open_config(&u.conf, false) != CONFIG_LOADED) { log_error("failed to open config, run `tarod` at least once to generate the required files"); nn_common_close(); return exit_status; } db_init(&u.db); str *dir; al_array_foreach_ptr(u.conf.wallpaper_dirs, i, dir) { if (!db_load_entries(&u.db, dir)) goto out; } db_sort(&u.db); const char *term_env = getenv("TERM"); if ((strncmp(term_env, "xterm-kitty", sizeof("xterm-kitty")) == 0) || (strncmp(term_env, "foot", sizeof("foot")) == 0)) { u.use_pixel_blit = true; } else { u.use_pixel_blit = false; } if (!init_ui(&u)) goto out; notcurses_stddim_yx(u.nc, &u.term_rows, &u.term_cols); ncplane_set_resizecb(notcurses_stdplane(u.nc), resize_cb); ncplane_set_userptr(notcurses_stdplane(u.nc), &u); if (!layout_ui(&u)) { notcurses_stop(u.nc); log_error("failed to layout ui, your terminal window is probably too small"); goto out; } u.layed_out = true; draw_monitors(&u, &u.m); nn_event_loop_init(&u.loop); nn_poll_init(&u.input_poll, input_poll_callback, &u); nn_poll_set(&u.input_poll, notcurses_inputready_fd(u.nc), NNWT_POLL_READ); nn_poll_start(&u.input_poll, &u.loop); nn_timer_init(&u.timer, &u.loop, timer_callback, &u); u.delay = 1000000; // 1fps. timer_callback(&u, NULL); nn_event_loop_run(&u.loop); notcurses_stop(u.nc); exit_status = EXIT_SUCCESS; out: close_config(&u.conf); nn_common_close(); return exit_status; }