I am currently developing a terminal-based listview control, that uses ncurses for rendering. The main goal for the listview was to be intuitive, performant, and reusable for other projects.
An example, is shown below:
listview.c
#include "listview.h"
#include <ncurses.h>
#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdarg.h>
listview *listview_new() {
listview *lv = malloc(sizeof(listview));
if (!lv) { return NULL; }
lv->rows = NULL;
lv->rows_count = 0;
lv->columns = NULL;
lv->column_count = 0;
lv->listview_max_width = 0;
lv->listview_max_height = 0;
lv->cell_padding = 2;
return lv;
}
void listview_add_column(listview *lv, char *text) {
lv->columns = realloc(lv->columns, (lv->column_count + 1) * sizeof(lv_column));
if (!lv->columns) { return; }
lv->columns[lv->column_count].text = text;
lv->columns[lv->column_count].alignment = left;
lv->columns[lv->column_count].width = (2 * lv->cell_padding) + strlen(text);
lv->column_count++;
}
int listview_set_column_alignment(listview *lv, int column_index, column_alignment alignment) {
if (column_index >= lv->column_count)
return -1;
lv->columns[column_index].alignment = alignment;
return 0;
}
void listview_render(WINDOW *win, listview *lv) {
// Define colors
wclear(win);
init_pair(10, COLOR_BLACK, COLOR_WHITE);
init_pair(20, COLOR_WHITE, COLOR_BLACK);
getmaxyx(win, lv->listview_max_height, lv->listview_max_width);
wattron(win, COLOR_PAIR(10));
// Render columns first
int cursor_pos = 0;
for (int i = 0; i < lv->column_count; i++) {
char *col_text = lv->columns[i].text;
int col_width = lv->columns[i].width;
int col_fill = col_width - (2 * lv->cell_padding) - strlen(col_text);
int extra_pad = lv->listview_max_width - cursor_pos - (lv->cell_padding + strlen(col_text) + lv->cell_padding);
if (i == lv->column_count - 1) {
mvwprintw(win, 0, cursor_pos, "%*s%s%*s%*s", lv->cell_padding, "", col_text, lv->cell_padding, "",
extra_pad, "");
cursor_pos = 0;
break;
} else {
if (lv->columns[i].alignment == left) {
mvwprintw(win, 0, cursor_pos, "%*s%s%*s%*s", lv->cell_padding, "", col_text, lv->cell_padding, "",
col_fill,
"");
} else if (lv->columns[i].alignment == right) {
mvwprintw(win, 0, cursor_pos, "%*s%*s%s%*s", lv->cell_padding, "", col_fill, "", col_text,
lv->cell_padding, "");
} else if (lv->columns[i].alignment == center) {
int col_fill_left = col_fill / 2;
int coll_fill_right = col_fill - col_fill_left;
mvwprintw(win, 0, cursor_pos, "%*s%*s%s%*s%*s", lv->cell_padding, "", col_fill_left, "", col_text,
coll_fill_right, "", lv->cell_padding, "");
}
}
cursor_pos += lv->cell_padding + strlen(col_text) + lv->cell_padding + col_fill;
}
wattron(win, COLOR_PAIR(20));
// Render rows
for (int i = 0; i < lv->rows_count; i++) {
for (int j = 0; j < lv->rows[i].cell_count; j++) {
char *cell_text = lv->rows[i].cells[j].text;
int col_width = lv->columns[j].width;
int col_fill = col_width - (2 * lv->cell_padding) - strlen(cell_text);
int extra_pad =
lv->listview_max_width - cursor_pos - (lv->cell_padding + strlen(cell_text) + lv->cell_padding);
int color_id = alloc_pair(lv->rows[i].cells[j].fg_color, lv->rows[i].cells[j].bg_color);
wattron(win, COLOR_PAIR(color_id));
if (j == lv->rows[i].cell_count - 1) {
mvwprintw(win, i + 1, cursor_pos, "%*s%s%*s%*s", lv->cell_padding, "", cell_text, lv->cell_padding,
"", extra_pad, "");
cursor_pos = 0;
break;
} else {
if (lv->columns[j].alignment == left) {
mvwprintw(win, i + 1, cursor_pos, "%*s%s%*s%*s", lv->cell_padding, "", cell_text, lv->cell_padding,
"", col_fill, "");
} else if (lv->columns[j].alignment == right) {
mvwprintw(win, i + 1, cursor_pos, "%*s%*s%s%*s", lv->cell_padding, "", col_fill, "", cell_text,
lv->cell_padding, "");
} else if (lv->columns[j].alignment == center) {
int col_fill_left = col_fill / 2;
int coll_fill_right = col_fill - col_fill_left;
mvwprintw(win, i + 1, cursor_pos, "%*s%*s%s%*s%*s", lv->cell_padding, "", col_fill_left, "",
cell_text, coll_fill_right, "", lv->cell_padding, "");
}
}
free_pair(color_id);
wattroff(win, COLOR_PAIR(color_id));
cursor_pos += lv->cell_padding + strlen(cell_text) + lv->cell_padding + col_fill;
}
}
}
int listview_delete_column(listview *lv, int column_index)
{
if(column_index >= lv->column_count)
return -1;
// Remove column
lv_column *new_cols = malloc(sizeof(lv_column) * (lv->column_count - 1));
memmove(new_cols, lv->columns, column_index * sizeof(lv_column));
memmove(&new_cols[column_index], &lv->columns[column_index + 1], (lv->column_count - column_index - 1) * sizeof(lv_column));
free(lv->columns);
lv->columns = new_cols;
// Remove effected cells in every row
for (int i = 0; i < lv->rows_count; i++)
{
lv_cell *new_cells = malloc(sizeof(lv_cell) * (lv->column_count - 1));
memmove(new_cells, lv->rows[i].cells, column_index * sizeof(lv_cell));
memmove(&new_cells[column_index], &lv->rows[i].cells[column_index + 1], (lv->column_count - column_index - 1) * sizeof(lv_cell));
free(lv->rows[i].cells);
lv->rows[i].cells = new_cells;
lv->rows[i].cell_count--;
}
lv->column_count--;
return 0;
}
int listview_update_cell_background_color(listview *lv, int row_index, int cell_index, lv_color bg_color)
{
if (bg_color < 0 || bg_color > 7)
return -1;
lv->rows[row_index].cells[cell_index].bg_color = bg_color;
return 0;
}
int listview_update_cell_foreground_color(listview *lv, int row_index, int cell_index, lv_color fg_color)
{
if (fg_color < 0 || fg_color > 7)
return -1;
lv->rows[row_index].cells[cell_index].fg_color = fg_color;
return 0;
}
void listview_add_row(listview *lv, ...) {
va_list argp;
va_start(argp, lv);
lv->rows = realloc(lv->rows, sizeof(lv_row) * (lv->rows_count + 1));
lv->rows[lv->rows_count].cells = calloc(lv->column_count, sizeof(lv_cell));
lv->rows[lv->rows_count].cell_count = lv->column_count;
for (int i = 0; i < lv->column_count; i++) {
char *text = va_arg(argp,
char*);
lv->rows[lv->rows_count].cells[i].text = text;
lv->rows[lv->rows_count].cells[i].bg_color = black;
lv->rows[lv->rows_count].cells[i].fg_color = white;
}
lv->rows_count++;
va_end(argp);
update_max_col_width(lv, lv->rows_count);
}
int listview_delete_row(listview *lv, int row_index) {
if (row_index > lv->rows_count)
return - 1;
lv_row *new_rows = malloc(sizeof(lv_row) * (lv->rows_count - 1));
if (new_rows == NULL)
return -2;
memmove(new_rows, lv->rows, row_index * sizeof(lv_row));
memmove(&new_rows[row_index], &lv->rows[row_index + 1], (lv->rows_count - row_index - 1) * sizeof(lv_row));
free(lv->rows);
lv->rows = new_rows;
lv->rows_count--;
update_max_col_width(lv, lv->rows_count);
return 0;
}
int listview_insert_row(listview *lv, int row_index, ...) {
if (row_index > lv->rows_count)
return -1;
va_list argp;
va_start(argp, row_index);
lv->rows = realloc(lv->rows, sizeof(lv_row) * (lv->rows_count + 1));
if(!lv->rows)
return -2;
if (row_index < lv->rows_count) {
int entries_to_move = (lv->rows_count - row_index);
memmove(&lv->rows[row_index + 1], &lv->rows[row_index], entries_to_move * sizeof(lv_row));
lv->rows[row_index].cells = (lv_cell *) malloc(sizeof(lv_cell) * lv->column_count);
for (int i = 0; i < lv->column_count; i++) {
lv->rows[row_index].cell_count = lv->column_count;
lv->rows[row_index].cells[i].text = va_arg(argp, char*);
lv->rows[row_index].cells[i].fg_color = white;
lv->rows[row_index].cells[i].bg_color = black;
}
} else if (row_index == lv->rows_count) {
lv->rows[row_index].cells = (lv_cell *) malloc(sizeof(lv_cell) * lv->column_count);
for (int i = 0; i < lv->column_count; i++) {
lv->rows[row_index].cell_count = lv->column_count;
lv->rows[row_index].cells[i].text = va_arg(argp, char*);
lv->rows[row_index].cells[i].fg_color = white;
lv->rows[row_index].cells[i].bg_color = black;
}
}
lv->rows_count++;
update_max_col_width(lv, lv->rows_count);
return 0;
}
int listview_update_cell_text(listview *lv, int row_index, int cell_index, char *text) {
if (row_index >= lv->rows_count || cell_index >= lv->column_count)
return -1;
if(text == NULL)
return -2;
lv->rows[row_index].cells[cell_index].text = text;
update_max_col_width(lv, lv->rows_count);
return 0;
}
void update_max_col_width(listview *lv, int row_index) {
// Make sure index is valid
if (row_index > lv->rows_count || row_index < 0)
return;
// Reset all max col width values
for (int i = 0; i < lv->column_count; i++) {
lv->columns[i].width = strlen(lv->columns[i].text) + (2 * lv->cell_padding);
}
// Recompute the max col width values for rows
for (int i = 0; i < lv->rows_count; i++) {
for (int j = 0; j < lv->rows[i].cell_count; j++) {
if (strlen(lv->rows[i].cells[j].text) + (2 * lv->cell_padding) > lv->columns[j].width)
lv->columns[j].width = strlen(lv->rows[i].cells[j].text) + (2 * lv->cell_padding);
}
}
}
void listview_free(listview *lv) {
// Free rows
free(lv->rows);
// Free headers
free(lv->columns);
// Free lv itself
free(lv);
}
listview.h
#ifndef LISTVIEW_H
#define LISTVIEW_H
#include <stdint.h>
#include <stdlib.h>
#include <ncurses.h>
typedef enum lv_color {
black = COLOR_BLACK,
blue = COLOR_BLUE,
cyan = COLOR_CYAN,
green = COLOR_GREEN,
magenta = COLOR_MAGENTA,
red = COLOR_RED,
white = COLOR_WHITE,
yellow = COLOR_YELLOW,
} lv_color;
typedef enum column_alignment {
// Aligns the column text and the cells in that column to the left
left,
// Aligns the column text and the cells in that column centered
center,
// Aligns the column text and the cells in that column to the right
right
} column_alignment;
typedef struct lv_cell {
// The text in the cell
char *text;
short bg_color;
short fg_color;
} lv_cell;
typedef struct lv_column {
// The column text
char *text;
// The width of the column (depends on width of the row cells)
uint16_t width;
// Specifies the alignment of the text in entire column
column_alignment alignment;
} lv_column;
typedef struct lv_row {
// Holds all cells in the row
lv_cell *cells;
// Total amount of cells in the row
uint8_t cell_count;
} lv_row;
typedef struct listview {
// Holds all columns
lv_column *columns;
// Holds amount of columns
uint16_t column_count;
// Holds all rows
lv_row *rows;
// Holds amount of rows
uint16_t rows_count;
// Maximum window width the listview can occupy
uint16_t listview_max_width;
// Maximum window height the listview can occupy
uint16_t listview_max_height;
// Amount of spaces padded to the left and right of each cell
uint8_t cell_padding;
} listview;
listview *listview_new();
void listview_free(listview *lv);
void listview_add_column(listview *lv, char* text);
int listview_set_column_alignment(listview *lv, int column_index, column_alignment alignment);
int listview_insert_row(listview* lv, int row_index, ...);
int listview_delete_column(listview *lv, int column_index);
void listview_add_row(listview *lv, ...);
int listview_delete_row(listview *lv, int row_index);
int listview_update_cell_text(listview *lv, int row_index, int cell_index, char* text);
int listview_update_cell_background_color(listview* lv, int row_index, int cell_index, lv_color bg_color);
int listview_update_cell_foreground_color(listview* lv, int row_index, int cell_index, lv_color fg_color);
void listview_render(WINDOW *win, listview *lv);
void update_max_col_width(listview *lv, int row_index);
#endif
main.c
#include "ui.h"
#include <stdio.h>
#include <stdlib.h>
#include <ncurses.h>
#include "listview.h"
int main()
{
// Setup stuff
initscr();
start_color();
clear();
noecho();
curs_set(0);
cbreak();
WINDOW* win = newwin(0, 0, 0, 0);
refresh();
// Create listview
listview* lv = listview_new();
listview_add_column(lv, "Country");
listview_add_column(lv, "Capital");
listview_add_column(lv, "Area");
listview_add_column(lv, "Population");
listview_add_column(lv, "Currency");
// Add some rows of data
listview_add_row(lv, "Germany", "Berlin", "357,022 km2", "83,190,556", "Euro");
listview_add_row(lv, "Japan", "Tokyo", "377,975 km2", "125,470,000", "Yen");
listview_add_row(lv, "Netherlands", "Amsterdam", "41,865 km2", "83,190,556", "Euro");
listview_add_row(lv, "China", "Beijing", "9,596,961 km2", "1,444,390,177", "Renminbi");
listview_add_row(lv, "France", "Paris", "640,679 km2", "67,413,000", "Euro");
listview_add_row(lv, "New Zealand", "Wellington", "268,021 km2", "5,123,760", "New Zealand Dollar");
listview_render(win, lv);
wgetch(win);
listview_update_cell_foreground_color(lv, 0, 0, red);
listview_update_cell_foreground_color(lv, 0, 2, green);
listview_update_cell_foreground_color(lv, 0, 4, blue);
listview_update_cell_background_color(lv, 1, 1, yellow);
listview_update_cell_background_color(lv, 1, 3, magenta);
listview_render(win, lv);
wgetch(win);
listview_insert_row(lv, 1, "United States of America", "Washington, D.C.", "9,525,067 km2", "331,449,281", "US Dollar");
listview_render(win, lv);
wgetch(win);
listview_delete_column(lv, 2);
listview_render(win, lv);
wgetch(win);
listview_free(lv);
delwin(win);
return 0;
}
Make sure to compile the example with -lncurses
, to try it out.
While the listview works just fine it its current state, I am sure, there is lots of room for improvement. To me it seems that especially the render function can vastly be improved. However, some feedback regarding my api-design and performance are also welcome :)