This tool enables you to create and run fully customisable dashboards directly in your terminal.
Getting started •
Configuration •
Adding new widgets •
Contributing •
License
Note
A Python version is also available here.
macOS
brew install pkgconf ncurses curl libstatgrab cjson glib libmrss cmakeDebian / Ubuntu
sudo apt install libncurses-dev libcurl4-openssl-dev libstatgrab-dev libcjson-dev libglib2.0-dev libx11-dev libpci-dev libmrss0-dev cmakeArch
sudo pacman -S ncurses curl libstatgrab cjson glib2 libx11 pciutils cmake
yay -S libmrssFreeBSD
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.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
./build/twidgetsCMakeLists.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.
- 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.
| 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.
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.
twidgets has no runtime configuration file. Everything is configured with defines and rebuilt. That means you have to recompile after every configuration change.
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.
| 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 |
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.
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 |
update_funcruns on a separate thread and must never touch ncurses. Onlydraw_func,help_funcand the input callbacks may draw.internal_dataandupdate_func_called_onceare protected byself->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 aNULL-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 - passloadingwhileupdate_func_called_onceis stillfalseanderrorwhen the data could not be fetched.
#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;
}- Add
Widget* example_build_widget(void);toinclude/widgets/widgets.h. - Add
add_widget_to_builder(example_build_widget());tobuild_all_widgets()insrc/widgets/widgets.c. - Add
src/widgets/example_widget.cto the source list inCMakeLists.txt. - Make sure the position you chose does not overlap another widget.
Pull requests are welcome. A few things to keep in mind:
- The project builds with
-Wall -Wextra -Werror -Wpedanticand C11 - it has to stay warning-free on all supported operating systems macOS (macOS, Linux and FreeBSD). - Follow the existing style: four spaces,
constwherever possible, early returns,snake_casefor functions and variables,PascalCasefor 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.
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.
