Skip to content

Latest commit

 

History

9 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation


🖥 Terminal Widgets

This tool enables you to create and run fully customisable dashboards directly in your terminal.

Getting startedConfigurationAdding new widgetsContributingLicense

Example Image of Terminal Widgets

Note

A Python version is also available here.

1. Getting started

1.1 Install dependencies

macOS

brew install pkgconf ncurses curl libstatgrab cjson glib libmrss cmake

Debian / Ubuntu

sudo apt install libncurses-dev libcurl4-openssl-dev libstatgrab-dev libcjson-dev libglib2.0-dev libx11-dev libpci-dev libmrss0-dev cmake

Arch

sudo pacman -S ncurses curl libstatgrab cjson glib2 libx11 pciutils cmake
yay -S libmrss

FreeBSD

sudo pkg install ncurses curl libstatgrab libcjson glib libX11 pciutils libmrss cmake
Library Used for
ncurses The entire terminal UI
libcurl HTTP requests (weather, news)
libcjson Parsing the weather API response
mrss Reading the RSS feed (news)
libstatgrab CPU, memory, swap, disk and network usage
gio / glib Opening links in your browser, /etc/os-release
libX11 Screen resolution on Linux / BSD
libpci GPU name on Linux / BSD

On macOS the CoreFoundation, CoreGraphics and IOKit frameworks are used instead of libX11 and libpci. They ship with the Command Line Tools, so there is nothing to install.

1.2 Build & run

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
./build/twidgets

CMakeLists.txt declares cmake_minimum_required(VERSION 4.2). If your distribution ships an older CMake, either install a newer one or lower that line - nothing in the build actually depends on 4.2.

1.3 Requirements

  • A terminal of at least 176 × 35 cells for the default layout. Widgets that do not fit are hidden automatically and reappear once the terminal is large enough again.
  • A UTF-8 locale (LANG / LC_ALL), otherwise box drawing characters and emoji titles break.
  • A 256-color terminal is recommended. With fewer colors the palette falls back to the eight basic ANSI colors.

1.4 Default global controls

Key Action
Left click Focus the widget under the cursor
Esc Remove the focus from the current widget
h Toggle the help page of the focused widget (show per-widget controls)
r Reload - tears down and rebuilds every widget
q Quit

q and r only work while no widget is focused - press Esc first. Every other key is forwarded to the focused widget.

1.5 Platform support

Everything runs on macOS, Linux and FreeBSD. Only the sysfetch widget is platform specific:

Field macOS Linux FreeBSD / other BSD
OS sysctl kern.osproductversion g_get_os_info() (/etc/os-release) uname
Host sysctl hw.model DMI / device tree kenv smbios.system.product
Packages Homebrew Cellar & Caskroom pacman, dpkg, rpm, apk pkg info
Resolution CoreGraphics X11, else /sys/class/drm X11
CPU sysctl machdep.cpu.brand_string /proc/cpuinfo sysctl hw.model
GPU IOKit registry libpci, else unknown libpci, else unknown
Terminal font Terminal config file Terminal config file Terminal config file

Anything that cannot be determined is displayed as unknown - the widget never fails because of a missing optional dependency.

2. Configuration

twidgets has no runtime configuration file. Everything is configured with defines and rebuilt. That means you have to recompile after every configuration change.

2.1 Global settings

All of these live in include/core/base_config.h:

Define Meaning
USE_STANDARD_TERMINAL_BACKGROUND true keeps your terminal background (transparency stays intact)
USE_EMOJI_TITLES Switches between the plain and the emoji widget titles
RESET_HELP_MODE_AFTER_ESCAPE Closes an open help page when the widget loses focus
QUIT_KEY, RELOAD_KEY, HELP_KEY Global keybindings

The colors themselves are defined in src/core/base_config.c as RGB values from 0 to 1000:

Color Used for
BACKGROUND / FOREGROUND Default text and background
PRIMARY Border and title of the focused widget
SECONDARY Selected rows
LOADING Border while a widget waits for its first update
ERROR Border of a widget that failed

If the terminal supports redefining colors, these exact values are used. On a 256-color terminal the closest palette entries are picked instead, and on an 8-color terminal the basic ANSI colors are used. Color pairs 1 - 5 hold the colors above, pairs 6 - 23 hold the green -> yellow -> red -> magenta -> blue gradient that widgets can use too.

2.2 Default widgets

Widget Shows Update interval Size (h × w) Position (y | x) Network
calendar The current month on every frame 10 x 25 0 | 119 no
clock Weekday, date and time on every frame 6 x 30 4 | 88 no
greeting A greeting message on every frame 4 x 30 30 | 2 no
mode Name of the currently focused widget on every frame 4 x 30 0 | 88 no
news Headlines from an RSS feed 2 h 10 x 87 10 | 88 yes
resources CPU, memory, swap, disk and network usage 4 s 10 x 87 20 | 88 no
sysfetch System information next to your OS logo 60 s 20 x 85 0 | 2 no
weather Current weather from OpenWeatherMap 30 min 10 x 30 0 | 145 yes
todo A to-do list you can edit on every frame 10 x 85 20 | 2 no

Widgets with an interval of 0.0 are never scheduled - they build their content directly in their draw function.

Per-widget settings sit at the top of the corresponding file in src/widgets/:

Define File Meaning
WEATHER_API_KEY weather_widget.c Your own OpenWeatherMap API key
WEATHER_CITY weather_widget.c City,CountryCode, e.g. Vienna,AT
WEATHER_UNIT_RAW weather_widget.c WEATHER_METRIC, WEATHER_IMPERIAL or WEATHER_STANDARD
NEWS_FEED_URL, NEWS_FEED_NAME news_widget.c The RSS feed and the label in the title
GREETING_WIDGET_YOUR_NAME greeting_widget.c The name in the greeting
CLOCK_WIDGET_TIME_FORMAT_L1 - L3 clock_widget.c strftime formats for the three lines
SYSFETCH_LOGO_L1 - L17 sysfetch_widget.c The ASCII logo, one string per line

2.3 Layout

Position and size are passed to widget_init() inside every *_build_widget() function, as height, width, y, x, z. z decides the drawing order - higher values are drawn last and therefore on top.

     x=2                        x=88        x=119    x=145
y=0  ┌───────────────────────┐ ┌─────────┐ ┌───────┐ ┌────────┐
     │                       │ │  Mode   │ │       │ │        │
y=4  │                       │ └─────────┘ │ Cale- │ │  Wea-  │
     │                       │ ┌─────────┐ │ ndar  │ │  ther  │
     │       Sysfetch        │ │  Clock  │ │       │ │        │
y=10 │                       │ └─────────┘ └───────┘ └────────┘
     │                       │ ┌──────────────────────────────┐
     │                       │ │             News             │
     └───────────────────────┘ └──────────────────────────────┘            
y=20 ┌───────────────────────┐ ┌──────────────────────────────┐
     │        To-Do          │ │        Resources             │
     │                       │ │                              │
     └───────────────────────┘ └──────────────────────────────┘
y=30 ┌──────────┐
     │ Greeting │
     └──────────┘                                    (not to scale)

Widget windows are created once at startup and re-created on a terminal resize. Because of that, dimensions must not be changed while the program is running - the z-sorted draw order would become stale.

3. Adding new widgets

A widget is a Widget struct plus up to seven callbacks. None of them are mandatory, you can set each one individually to NULL if you don't need it.

Callback Runs on Purpose
init_func startup Allocate internal_data. Returning false (on error) disables the widget
update_func reloader thread Fetch data every interval seconds (for heavy loading)
draw_func main loop; every frame Render the widget (every frame)
mouse_click_func main loop; mouse click Handle clicks while focused (also runs when help mode is enabled)
keyboard_func main loop; keyboard press Handle keys while focused (also runs when help mode is enabled)
help_func main loop; help page activated Render the help page (h)
exit_func shutdown Free internal_data

3.1 Rules

  • update_func runs on a separate thread and must never touch ncurses. Only draw_func, help_func and the input callbacks may draw.
  • internal_data and update_func_called_once are protected by self->mutex. Take the lock, copy what you need onto the stack, release it, then render from the copy.
  • Never block inside draw_func - it runs on every frame.
  • widget_add_content() takes a NULL-terminated array of lines, widget_safe_addstr() takes -1-terminated arrays of color pairs and attributes. Both clip at the border and are UTF-8 aware.
  • widget_draw(self, container, title, loading, error) draws the frame - pass loading while update_func_called_once is still false and error when the data could not be fetched.

3.2 Skeleton

#include "widgets/widgets.h"

typedef struct Example {
    int value;
} Example;

bool example_init_func(Widget* self, WidgetContainer* widget_container) {
    (void) widget_container; // Unused variables are casted to void, so the compiler doesn't warn you

    // Allocate enough space for `Example` on the heap
    self->internal_data = malloc(sizeof(Example));
    
    // If allocation fails, return false (the widget gets disabled by the core)
    if (self->internal_data == NULL) return false;

    // Zero-initialize internal_data
    *(Example*) self->internal_data = (Example) {0};
    
    return true;
}

void example_exit_func(Widget* self, WidgetContainer* widget_container) {
    (void) widget_container; // Unused variables are casted to void, so the compiler doesn't warn you

    // Free internal_data and set the pointer to NULL
    free(self->internal_data);
    self->internal_data = NULL;
}

void example_update_func(Widget* self, WidgetContainer* widget_container) {
    (void) widget_container; // Unused variables are casted to void, so the compiler doesn't warn you

     // Slow work belongs here, not in draw_func
    const int value = 42;

    // Lock when writing/reading internal_data
    pthread_mutex_lock(&self->mutex);
    ((Example*) self->internal_data)->value = value;
    pthread_mutex_unlock(&self->mutex);
}

void example_draw_func(Widget* self, WidgetContainer* widget_container) {
    // Lock when writing/reading internal_data
    pthread_mutex_lock(&self->mutex);
    const Example e = *(const Example*) self->internal_data;
    const bool ready = self->update_func_called_once;
    pthread_mutex_unlock(&self->mutex);

    // If ready is false, draw widget with `loading`
    widget_draw(self, widget_container, NULL, !ready, false);

    int required = snprintf(NULL, 0, "Value: %d", e.value);
    char l1[required < 0 ? (size_t)required + 1];
    if (required < 0) l1[0] = '\0';
    else snprintf(l1, sizeof l1, "Value: %d", e.value);

    // NULL-terminated array
    widget_add_content(self, (const char* const[]){
        l1, NULL
    });
}

Widget* example_build_widget(void) {
    // Allocate enough for the widget
    Widget* w = malloc(sizeof *w);
    if (w == NULL) return NULL;

    // Register all functions
    widget_init(
        w,
        "example", " Example ", " ★ Example ", true, 5.0,
        4, 30, 34, 2, 1,
        example_draw_func, example_update_func, NULL, NULL,
        example_init_func, example_exit_func, NULL
    );

    return w;
}

3.3 Registration

  1. Add Widget* example_build_widget(void); to include/widgets/widgets.h.
  2. Add add_widget_to_builder(example_build_widget()); to build_all_widgets() in src/widgets/widgets.c.
  3. Add src/widgets/example_widget.c to the source list in CMakeLists.txt.
  4. Make sure the position you chose does not overlap another widget.

4. Contributing

Pull requests are welcome. A few things to keep in mind:

  • The project builds with -Wall -Wextra -Werror -Wpedantic and C11 - it has to stay warning-free on all supported operating systems macOS (macOS, Linux and FreeBSD).
  • Follow the existing style: four spaces, const wherever possible, early returns, snake_case for functions and variables, PascalCase for types.
  • Platform-specific code belongs behind #if defined(...) with a working fallback, not behind a hard requirement.
  • Keep new dependencies optional if the widget can degrade gracefully without them.

5. License

Distributed under the Apache License 2.0. See LICENSE for the full text.

The ASCII logos are the ones popularised by neofetch and screenfetch. All libraries listed above remain under their own licenses.

About

A terminal widget app coded in C using the ncurses library

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Contributors

Languages