/* * statusbar.c * vim: expandtab:ts=4:sts=4:sw=4 * * Copyright (C) 2012 - 2019 James Booth * Copyright (C) 2019 - 2026 Michael Vetter * * SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception */ #include "config.h" #include #include #include #include "log.h" #ifdef HAVE_NCURSESW_NCURSES_H #include #elif HAVE_NCURSES_H #include #elif HAVE_CURSES_H #include #endif #include "config/theme.h" #include "config/preferences.h" #include "database.h" #include "ui/ui.h" #include "ui/statusbar.h" #include "ui/inputwin.h" #include "ui/screen.h" #include "xmpp/roster_list.h" #include "xmpp/contact.h" typedef struct _status_bar_tab_t { win_type_t window_type; char* identifier; gboolean highlight; char* display_name; } StatusBarTab; typedef struct _status_bar_t { gchar* time; char* prompt; char* fulljid; GHashTable* tabs; guint current_tab; } StatusBar; static GTimeZone* tz; static StatusBar* statusbar; static WINDOW* statusbar_win; #define CONSOLE_TAB_ID 10 // Console tab ID in status bar hash table void _get_range_bounds(guint* start, guint* end, gboolean is_static); static guint _status_bar_draw_time(guint pos); static guint _status_bar_draw_maintext(guint pos); static guint _status_bar_draw_dbbackend(guint pos); static guint _status_bar_draw_bracket(gboolean current, guint pos, const char* ch); static guint _status_bar_draw_extended_tabs(guint pos, gboolean prefix, guint start, guint end, gboolean is_static); static guint _status_bar_draw_tab(StatusBarTab* tab, guint pos, guint num, gboolean include_brackets); static guint _status_bar_draw_tabs(guint pos); static void _destroy_tab(StatusBarTab* tab); static guint _tabs_width(guint start, guint end); static guint _count_digits(guint number); static guint _count_digits_in_range(guint start, guint end); static char* _display_name(StatusBarTab* tab); static gboolean _tabmode_is_actlist(void); void status_bar_init(void) { tz = g_time_zone_new_local(); statusbar = g_new0(StatusBar, 1); statusbar->time = NULL; statusbar->prompt = NULL; statusbar->fulljid = NULL; statusbar->tabs = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, (GDestroyNotify)_destroy_tab); StatusBarTab* console = g_new0(StatusBarTab, 1); console->window_type = WIN_CONSOLE; console->identifier = strdup("console"); console->display_name = NULL; g_hash_table_insert(statusbar->tabs, GINT_TO_POINTER(1), console); statusbar->current_tab = 1; int row = screen_statusbar_row(); int cols = getmaxx(stdscr); if (cols <= 0) { log_warning("status_bar_init: invalid cols %d, defaulting to 1", cols); cols = 1; } statusbar_win = newwin(1, cols, row, 0); if (statusbar_win) { status_bar_draw(); } } void status_bar_close(void) { if (statusbar_win) { delwin(statusbar_win); statusbar_win = NULL; } if (statusbar) { if (statusbar->time) { g_free(statusbar->time); } if (statusbar->prompt) { free(statusbar->prompt); } if (statusbar->fulljid) { free(statusbar->fulljid); } if (statusbar->tabs) { g_hash_table_destroy(statusbar->tabs); } FREE_SET_NULL(statusbar); } if (tz) { g_time_zone_unref(tz); tz = NULL; } } void status_bar_resize(void) { if (!statusbar_win) { return; } int cols = getmaxx(stdscr); if (cols <= 0) { log_warning("status_bar_resize: invalid cols %d, defaulting to 1", cols); cols = 1; } werase(statusbar_win); int row = screen_statusbar_row(); wresize(statusbar_win, 1, cols); mvwin(statusbar_win, row, 0); status_bar_draw(); } void status_bar_set_all_inactive(void) { g_hash_table_remove_all(statusbar->tabs); } void status_bar_current(guint i) { if (i == 0) { statusbar->current_tab = CONSOLE_TAB_ID; } else { statusbar->current_tab = i; } status_bar_draw(); } void status_bar_inactive(const int win) { g_hash_table_remove(statusbar->tabs, GINT_TO_POINTER((win == 0) ? CONSOLE_TAB_ID : win)); status_bar_draw(); } void _create_tab(const int win, win_type_t wintype, char* identifier, gboolean highlight) { StatusBarTab* tab = g_new0(StatusBarTab, 1); tab->identifier = strdup(identifier); tab->highlight = highlight; tab->window_type = wintype; tab->display_name = NULL; if (tab->window_type == WIN_CHAT) { PContact contact = NULL; if (roster_exists()) { contact = roster_get_contact(tab->identifier); } const char* pcontact_name = contact ? p_contact_name(contact) : NULL; auto_gchar gchar* pref = prefs_get_string(PREF_STATUSBAR_CHAT); if (g_strcmp0("user", pref) == 0) { if (pcontact_name) { tab->display_name = strdup(pcontact_name); } else { auto_jid Jid* jidp = jid_create(tab->identifier); if (jidp) { tab->display_name = jidp->localpart != NULL ? strdup(jidp->localpart) : strdup(jidp->barejid); } else { tab->display_name = strdup(tab->identifier); } } } else { tab->display_name = strdup(tab->identifier); } } g_hash_table_replace(statusbar->tabs, GINT_TO_POINTER((win == 0) ? CONSOLE_TAB_ID : win), tab); status_bar_draw(); } void status_bar_active(const int win, win_type_t wintype, char* identifier) { _create_tab(win, wintype, identifier, FALSE); } void status_bar_new(const int win, win_type_t wintype, char* identifier) { _create_tab(win, wintype, identifier, TRUE); } void status_bar_set_prompt(const char* const prompt) { if (statusbar->prompt) { free(statusbar->prompt); statusbar->prompt = NULL; } statusbar->prompt = strdup(prompt); status_bar_draw(); } void status_bar_clear_prompt(void) { if (statusbar->prompt) { free(statusbar->prompt); statusbar->prompt = NULL; } status_bar_draw(); } void status_bar_set_fulljid(const char* const fulljid) { if (statusbar->fulljid) { free(statusbar->fulljid); statusbar->fulljid = NULL; } statusbar->fulljid = strdup(fulljid); status_bar_draw(); } void status_bar_clear_fulljid(void) { if (statusbar->fulljid) { free(statusbar->fulljid); statusbar->fulljid = NULL; } status_bar_draw(); } void status_bar_draw(void) { if (!statusbar_win) { return; } werase(statusbar_win); wbkgd(statusbar_win, theme_attrs(THEME_STATUS_TEXT)); guint max_tabs = prefs_get_statusbartabs(); guint pos = 1; pos = _status_bar_draw_time(pos); pos = _status_bar_draw_maintext(pos); pos = _status_bar_draw_dbbackend(pos); if (max_tabs != 0) pos = _status_bar_draw_tabs(pos); wnoutrefresh(statusbar_win); inp_put_back(); } static guint _status_bar_draw_tabs(guint pos) { auto_gchar gchar* tabmode = prefs_get_string(PREF_STATUSBAR_TABMODE); if (g_strcmp0(tabmode, "actlist") != 0) { guint start, end; gboolean is_static = g_strcmp0(tabmode, "dynamic") != 0; _get_range_bounds(&start, &end, is_static); guint tabs_w = _tabs_width(start, end); int max_x = getmaxx(stdscr); if (max_x > 0 && (guint)max_x > tabs_w) { pos = (guint)max_x - tabs_w; } else { pos = 0; } pos = _status_bar_draw_extended_tabs(pos, TRUE, start, end, is_static); for (guint i = start; i <= end; i++) { StatusBarTab* tab = g_hash_table_lookup(statusbar->tabs, GINT_TO_POINTER(i)); if (tab) { pos = _status_bar_draw_tab(tab, pos, i, TRUE); } } pos = _status_bar_draw_extended_tabs(pos, FALSE, start, end, is_static); } else { pos++; guint print_act = 0; guint tabnum = g_hash_table_size(statusbar->tabs); for (guint i = 1; i <= tabnum; ++i) { StatusBarTab* tab = g_hash_table_lookup(statusbar->tabs, GINT_TO_POINTER(i)); if (tab && tab->highlight) { print_act++; } } if (print_act) { pos = _status_bar_draw_bracket(FALSE, pos, "["); mvwprintw(statusbar_win, 0, pos, "Act: "); pos += 5; int status_attrs = theme_attrs(THEME_STATUS_NEW); wattron(statusbar_win, status_attrs); for (guint i = 1; i <= tabnum && print_act; ++i) { StatusBarTab* tab = g_hash_table_lookup(statusbar->tabs, GINT_TO_POINTER(i)); if (tab && tab->highlight) { pos = _status_bar_draw_tab(tab, pos, i, FALSE); if (print_act > 1) { mvwprintw(statusbar_win, 0, pos, ","); pos++; } print_act--; } } wattroff(statusbar_win, status_attrs); pos = _status_bar_draw_bracket(FALSE, pos, "]"); } } return pos; } // Checks if there are highlighted (unread) messages on the (left side if `left_side` is true, else right side) // within the current displayed tabs range. static gboolean _has_new_msgs_beyond_range_on_side(gboolean left_side, int display_tabs_start, int display_tabs_end) { guint max_tabs = prefs_get_statusbartabs(); int tabs_count = g_hash_table_size(statusbar->tabs); if ((guint)tabs_count <= max_tabs) { return FALSE; } int start = left_side ? 1 : display_tabs_end + 1; int end = left_side ? display_tabs_start - 1 : tabs_count; for (int i = start; i <= end; i++) { StatusBarTab* tab = g_hash_table_lookup(statusbar->tabs, GINT_TO_POINTER(i)); if (tab && tab->highlight) { return TRUE; } } return FALSE; } static guint _status_bar_draw_extended_tabs(guint pos, gboolean prefix, guint start, guint end, gboolean is_static) { guint max_tabs = prefs_get_statusbartabs(); if (max_tabs == 0) { return pos; } guint opened_tabs = g_hash_table_size(statusbar->tabs); if (g_hash_table_size(statusbar->tabs) <= max_tabs) { return pos; } if (prefix && start < 2) { return pos; } // Guard against underflow when opened_tabs == 0 (reachable if the // earlier `opened_tabs <= max_tabs` early-return no longer holds). if (!prefix && end >= opened_tabs) { return pos; } gboolean is_current = is_static && statusbar->current_tab > max_tabs; pos = _status_bar_draw_bracket(is_current, pos, "["); int status_attrs; if (_has_new_msgs_beyond_range_on_side(prefix, start, end)) { // Apply theme attributes for a tab with new messages. status_attrs = theme_attrs(THEME_STATUS_NEW); } else { // Apply theme attributes for a tab without new messages. status_attrs = theme_attrs(THEME_STATUS_ACTIVE); } wattron(statusbar_win, status_attrs); mvwprintw(statusbar_win, 0, pos, prefix ? "<" : ">"); wattroff(statusbar_win, status_attrs); pos++; pos = _status_bar_draw_bracket(is_current, pos, "]"); return pos; } static guint _status_bar_draw_tab(StatusBarTab* tab, guint pos, guint num, gboolean include_brackets) { gboolean is_current = num == statusbar->current_tab; gboolean show_number = prefs_get_boolean(PREF_STATUSBAR_SHOW_NUMBER); gboolean show_name = prefs_get_boolean(PREF_STATUSBAR_SHOW_NAME); gboolean show_read = prefs_get_boolean(PREF_STATUSBAR_SHOW_READ); // dont show this if (!show_read && !is_current && !tab->highlight) return pos; if (include_brackets) { pos = _status_bar_draw_bracket(is_current, pos, "["); } int status_attrs; if (is_current) { status_attrs = theme_attrs(THEME_STATUS_CURRENT); } else if (tab->highlight) { status_attrs = theme_attrs(THEME_STATUS_NEW); } else { status_attrs = theme_attrs(THEME_STATUS_ACTIVE); } wattron(statusbar_win, status_attrs); if (show_number) { mvwprintw(statusbar_win, 0, pos, "%d", num); // calculate number of digits pos += _count_digits(num); } if (show_number && show_name) { mvwprintw(statusbar_win, 0, pos, ":"); pos++; } if (show_name) { auto_char char* display_name = _display_name(tab); mvwprintw(statusbar_win, 0, pos, "%s", display_name); pos += utf8_display_len(display_name); } wattroff(statusbar_win, status_attrs); if (include_brackets) { pos = _status_bar_draw_bracket(is_current, pos, "]"); } return pos; } static guint _status_bar_draw_bracket(gboolean current, guint pos, const char* ch) { int bracket_attrs = theme_attrs(THEME_STATUS_BRACKET); wattron(statusbar_win, bracket_attrs); if (current) { mvwprintw(statusbar_win, 0, pos, "-"); } else { mvwprintw(statusbar_win, 0, pos, "%s", ch); } wattroff(statusbar_win, bracket_attrs); pos++; return pos; } static guint _status_bar_draw_time(guint pos) { auto_gchar gchar* time_pref = prefs_get_string(PREF_TIME_STATUSBAR); if (g_strcmp0(time_pref, "off") == 0) { return pos; } if (statusbar->time) { g_free(statusbar->time); statusbar->time = NULL; } GDateTime* datetime = g_date_time_new_now(tz); statusbar->time = g_date_time_format(datetime, time_pref); assert(statusbar->time != NULL); g_date_time_unref(datetime); int bracket_attrs = theme_attrs(THEME_STATUS_BRACKET); int time_attrs = theme_attrs(THEME_STATUS_TIME); guint len = (guint)strlen(statusbar->time); wattron(statusbar_win, bracket_attrs); mvwaddch(statusbar_win, 0, pos, '['); pos++; wattroff(statusbar_win, bracket_attrs); wattron(statusbar_win, time_attrs); mvwprintw(statusbar_win, 0, pos, "%s", statusbar->time); pos += len; wattroff(statusbar_win, time_attrs); wattron(statusbar_win, bracket_attrs); mvwaddch(statusbar_win, 0, pos, ']'); wattroff(statusbar_win, bracket_attrs); pos += 2; return pos; } static gboolean _tabmode_is_actlist(void) { auto_gchar gchar* tabmode = prefs_get_string(PREF_STATUSBAR_TABMODE); return g_strcmp0(tabmode, "actlist") == 0; } static guint _status_bar_draw_maintext(guint pos) { const char* maintext = NULL; auto_jid Jid* jidp = NULL; auto_gchar gchar* self = prefs_get_string(PREF_STATUSBAR_SELF); if (statusbar->prompt) { mvwprintw(statusbar_win, 0, pos, "%s", statusbar->prompt); return utf8_display_len(statusbar->prompt); } if (g_strcmp0(self, "off") == 0) { return pos; } if (statusbar->fulljid == NULL) { return pos; } jidp = jid_create(statusbar->fulljid); if (!jidp) return pos; if (g_strcmp0(self, "user") == 0) { maintext = jidp->localpart; } else if (g_strcmp0(self, "barejid") == 0) { maintext = jidp->barejid; } else { maintext = statusbar->fulljid; } gboolean actlist_tabmode = _tabmode_is_actlist(); auto_gchar gchar* maintext_ = NULL; if (actlist_tabmode) { pos = _status_bar_draw_bracket(FALSE, pos, "["); maintext_ = g_strdup_printf("%d:%s", statusbar->current_tab, maintext); maintext = maintext_; } mvwprintw(statusbar_win, 0, pos, "%s", maintext); pos += utf8_display_len(maintext); if (actlist_tabmode) { pos = _status_bar_draw_bracket(FALSE, pos, "]"); } return pos; } static guint _status_bar_draw_dbbackend(guint pos) { if (!prefs_get_boolean(PREF_STATUSBAR_SHOW_DBBACKEND)) return pos; if (!active_db_backend || !active_db_backend->name) return pos; auto_gchar gchar* label = g_strdup_printf(" [%s]", active_db_backend->name); mvwprintw(statusbar_win, 0, pos, "%s", label); return pos + utf8_display_len(label); } static void _destroy_tab(StatusBarTab* tab) { if (tab) { if (tab->identifier) { free(tab->identifier); } if (tab->display_name) { free(tab->display_name); } free(tab); } } static guint _tabs_width(guint start, guint end) { gboolean show_number = prefs_get_boolean(PREF_STATUSBAR_SHOW_NUMBER); gboolean show_name = prefs_get_boolean(PREF_STATUSBAR_SHOW_NAME); gboolean show_read = prefs_get_boolean(PREF_STATUSBAR_SHOW_READ); guint max_tabs = prefs_get_statusbartabs(); guint opened_tabs = g_hash_table_size(statusbar->tabs); int width = start < 2 ? 1 : 4; width += (end < opened_tabs) ? 3 : 0; if (show_name && show_number) { for (guint i = start; i <= end; i++) { StatusBarTab* tab = g_hash_table_lookup(statusbar->tabs, GINT_TO_POINTER(i)); if (tab) { gboolean is_current = i == statusbar->current_tab; // dont calculate this in because not shown if (!show_read && !is_current && !tab->highlight) continue; auto_char char* display_name = _display_name(tab); width += utf8_display_len(display_name); width += 3 + _count_digits(i); } } return width; } if (show_name && !show_number) { for (guint i = start; i <= end; i++) { StatusBarTab* tab = g_hash_table_lookup(statusbar->tabs, GINT_TO_POINTER(i)); if (tab) { gboolean is_current = i == statusbar->current_tab; // dont calculate this in because not shown if (!show_read && !is_current && !tab->highlight) continue; auto_char char* display_name = _display_name(tab); width += utf8_display_len(display_name); width += 2; } } return width; } if (opened_tabs > max_tabs) { width += _count_digits_in_range(start, end); width += start > end ? 0 : (end - start) * 3; return width; } return opened_tabs * 3 + 1; } static char* _display_name(StatusBarTab* tab) { char* fullname = NULL; if (tab->window_type == WIN_CONSOLE) { fullname = strdup("console"); } else if (tab->window_type == WIN_XML) { fullname = strdup("xmlconsole"); } else if (tab->window_type == WIN_PLUGIN) { fullname = strdup(tab->identifier); } else if (tab->window_type == WIN_CHAT) { if (tab && tab->display_name) { fullname = strdup(tab->display_name); } } else if (tab->window_type == WIN_MUC) { auto_gchar gchar* mucwin_title = mucwin_generate_title(tab->identifier, PREF_STATUSBAR_ROOM_TITLE); fullname = strdup(mucwin_title); } else if (tab->window_type == WIN_CONFIG) { auto_gchar gchar* mucwin_title = mucwin_generate_title(tab->identifier, PREF_STATUSBAR_ROOM_TITLE); fullname = g_strconcat(mucwin_title, " conf", NULL); } else if (tab->window_type == WIN_PRIVATE) { auto_jid Jid* jid = jid_create(tab->identifier); if (jid) { auto_gchar gchar* mucwin_title = mucwin_generate_title(jid->barejid, PREF_STATUSBAR_ROOM_TITLE); fullname = g_strconcat(mucwin_title, "/", jid->resourcepart, NULL); } else { // Fallback: use identifier directly if JID parsing failed fullname = strdup(tab->identifier); } } else { fullname = strdup("window"); } gint tablen = prefs_get_statusbartablen(); if (tablen == 0) { return fullname; } int namelen = utf8_display_len(fullname); if (namelen < tablen) { return fullname; } auto_gchar gchar* trimmed = g_utf8_substring(fullname, 0, tablen); char* trimmedname = strdup(trimmed); free(fullname); return trimmedname; } void _get_range_bounds(guint* start, guint* end, gboolean is_static) { int current_tab = statusbar->current_tab; gint display_range = prefs_get_statusbartabs(); int total_tabs = g_hash_table_size(statusbar->tabs); int side_range = display_range / 2; if (total_tabs <= display_range) { *start = 1; *end = total_tabs; } else if (current_tab - side_range <= 1 || is_static) { *start = 1; *end = display_range; } else if (current_tab + side_range >= total_tabs) { *start = total_tabs - display_range + 1; *end = total_tabs; } else { *start = current_tab - side_range; *end = current_tab + side_range; } } // Counts amount of digits in a number static guint _count_digits(guint number) { guint digits_count = 0; do { number /= 10; digits_count++; } while (number != 0); return digits_count; } // Counts the total number of digits in a range of numbers, inclusive. // Example: _count_digits_in_range(2, 3) returns 2, _count_digits_in_range(2, 922) returns 2657 static guint _count_digits_in_range(guint start, guint end) { guint total_digits = 0; for (guint i = start; i <= end; i++) { total_digits += _count_digits(i); } return total_digits; }