1 /*
2  * statusbar.c
3  * vim: expandtab:ts=4:sts=4:sw=4
4  *
5  * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
6  * Copyright (C) 2019 - 2021 Michael Vetter <jubalh@iodoru.org>
7  *
8  * This file is part of Profanity.
9  *
10  * Profanity is free software: you can redistribute it and/or modify
11  * it under the terms of the GNU General Public License as published by
12  * the Free Software Foundation, either version 3 of the License, or
13  * (at your option) any later version.
14  *
15  * Profanity is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU General Public License for more details.
19  *
20  * You should have received a copy of the GNU General Public License
21  * along with Profanity.  If not, see <https://www.gnu.org/licenses/>.
22  *
23  * In addition, as a special exception, the copyright holders give permission to
24  * link the code of portions of this program with the OpenSSL library under
25  * certain conditions as described in each individual source file, and
26  * distribute linked combinations including the two.
27  *
28  * You must obey the GNU General Public License in all respects for all of the
29  * code used other than OpenSSL. If you modify file(s) with this exception, you
30  * may extend this exception to your version of the file(s), but you are not
31  * obligated to do so. If you do not wish to do so, delete this exception
32  * statement from your version. If you delete this exception statement from all
33  * source files in the program, then also delete it here.
34  *
35  */
36 
37 #include "config.h"
38 
39 #include <assert.h>
40 #include <string.h>
41 #include <stdlib.h>
42 
43 #ifdef HAVE_NCURSESW_NCURSES_H
44 #include <ncursesw/ncurses.h>
45 #elif HAVE_NCURSES_H
46 #include <ncurses.h>
47 #elif HAVE_CURSES_H
48 #include <curses.h>
49 #endif
50 
51 #include "config/theme.h"
52 #include "config/preferences.h"
53 #include "ui/ui.h"
54 #include "ui/statusbar.h"
55 #include "ui/inputwin.h"
56 #include "ui/screen.h"
57 #include "xmpp/roster_list.h"
58 #include "xmpp/contact.h"
59 
60 typedef struct _status_bar_tab_t
61 {
62     win_type_t window_type;
63     char* identifier;
64     gboolean highlight;
65     char* display_name;
66 } StatusBarTab;
67 
68 typedef struct _status_bar_t
69 {
70     gchar* time;
71     char* prompt;
72     char* fulljid;
73     GHashTable* tabs;
74     int current_tab;
75 } StatusBar;
76 
77 static GTimeZone* tz;
78 static StatusBar* statusbar;
79 static WINDOW* statusbar_win;
80 
81 static int _status_bar_draw_time(int pos);
82 static void _status_bar_draw_maintext(int pos);
83 static int _status_bar_draw_bracket(gboolean current, int pos, char* ch);
84 static int _status_bar_draw_extended_tabs(int pos);
85 static int _status_bar_draw_tab(StatusBarTab* tab, int pos, int num);
86 static void _destroy_tab(StatusBarTab* tab);
87 static int _tabs_width(void);
88 static char* _display_name(StatusBarTab* tab);
89 static gboolean _extended_new(void);
90 
91 void
status_bar_init(void)92 status_bar_init(void)
93 {
94     tz = g_time_zone_new_local();
95 
96     statusbar = malloc(sizeof(StatusBar));
97     statusbar->time = NULL;
98     statusbar->prompt = NULL;
99     statusbar->fulljid = NULL;
100     statusbar->tabs = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, (GDestroyNotify)_destroy_tab);
101     StatusBarTab* console = calloc(1, sizeof(StatusBarTab));
102     console->window_type = WIN_CONSOLE;
103     console->identifier = strdup("console");
104     console->display_name = NULL;
105     g_hash_table_insert(statusbar->tabs, GINT_TO_POINTER(1), console);
106     statusbar->current_tab = 1;
107 
108     int row = screen_statusbar_row();
109     int cols = getmaxx(stdscr);
110     statusbar_win = newwin(1, cols, row, 0);
111 
112     status_bar_draw();
113 }
114 
115 void
status_bar_close(void)116 status_bar_close(void)
117 {
118     if (tz) {
119         g_time_zone_unref(tz);
120     }
121     if (statusbar) {
122         if (statusbar->time) {
123             g_free(statusbar->time);
124         }
125         if (statusbar->prompt) {
126             free(statusbar->prompt);
127         }
128         if (statusbar->fulljid) {
129             free(statusbar->fulljid);
130         }
131         if (statusbar->tabs) {
132             g_hash_table_destroy(statusbar->tabs);
133         }
134         free(statusbar);
135     }
136 }
137 
138 void
status_bar_resize(void)139 status_bar_resize(void)
140 {
141     int cols = getmaxx(stdscr);
142     werase(statusbar_win);
143     int row = screen_statusbar_row();
144     wresize(statusbar_win, 1, cols);
145     mvwin(statusbar_win, row, 0);
146 
147     status_bar_draw();
148 }
149 
150 void
status_bar_set_all_inactive(void)151 status_bar_set_all_inactive(void)
152 {
153     g_hash_table_remove_all(statusbar->tabs);
154 }
155 
156 void
status_bar_current(int i)157 status_bar_current(int i)
158 {
159     if (i == 0) {
160         statusbar->current_tab = 10;
161     } else {
162         statusbar->current_tab = i;
163     }
164 
165     status_bar_draw();
166 }
167 
168 void
status_bar_inactive(const int win)169 status_bar_inactive(const int win)
170 {
171     int true_win = win;
172     if (true_win == 0) {
173         true_win = 10;
174     }
175 
176     g_hash_table_remove(statusbar->tabs, GINT_TO_POINTER(true_win));
177 
178     status_bar_draw();
179 }
180 
181 void
_create_tab(const int win,win_type_t wintype,char * identifier,gboolean highlight)182 _create_tab(const int win, win_type_t wintype, char* identifier, gboolean highlight)
183 {
184     int true_win = win;
185     if (true_win == 0) {
186         true_win = 10;
187     }
188 
189     StatusBarTab* tab = malloc(sizeof(StatusBarTab));
190     tab->identifier = strdup(identifier);
191     tab->highlight = highlight;
192     tab->window_type = wintype;
193     tab->display_name = NULL;
194 
195     if (tab->window_type == WIN_CHAT) {
196         PContact contact = NULL;
197         if (roster_exists()) {
198             contact = roster_get_contact(tab->identifier);
199         }
200         if (contact && p_contact_name(contact)) {
201             tab->display_name = strdup(p_contact_name(contact));
202         } else {
203             char* pref = prefs_get_string(PREF_STATUSBAR_CHAT);
204             if (g_strcmp0("user", pref) == 0) {
205                 Jid* jidp = jid_create(tab->identifier);
206                 if (jidp) {
207                     tab->display_name = jidp->localpart != NULL ? strdup(jidp->localpart) : strdup(jidp->barejid);
208                     jid_destroy(jidp);
209                 } else {
210                     tab->display_name = strdup(tab->identifier);
211                 }
212             } else {
213                 tab->display_name = strdup(tab->identifier);
214             }
215             g_free(pref);
216         }
217     }
218 
219     g_hash_table_replace(statusbar->tabs, GINT_TO_POINTER(true_win), tab);
220 
221     status_bar_draw();
222 }
223 
224 void
status_bar_active(const int win,win_type_t wintype,char * identifier)225 status_bar_active(const int win, win_type_t wintype, char* identifier)
226 {
227     _create_tab(win, wintype, identifier, FALSE);
228 }
229 
230 void
status_bar_new(const int win,win_type_t wintype,char * identifier)231 status_bar_new(const int win, win_type_t wintype, char* identifier)
232 {
233     _create_tab(win, wintype, identifier, TRUE);
234 }
235 
236 void
status_bar_set_prompt(const char * const prompt)237 status_bar_set_prompt(const char* const prompt)
238 {
239     if (statusbar->prompt) {
240         free(statusbar->prompt);
241         statusbar->prompt = NULL;
242     }
243     statusbar->prompt = strdup(prompt);
244 
245     status_bar_draw();
246 }
247 
248 void
status_bar_clear_prompt(void)249 status_bar_clear_prompt(void)
250 {
251     if (statusbar->prompt) {
252         free(statusbar->prompt);
253         statusbar->prompt = NULL;
254     }
255 
256     status_bar_draw();
257 }
258 
259 void
status_bar_set_fulljid(const char * const fulljid)260 status_bar_set_fulljid(const char* const fulljid)
261 {
262     if (statusbar->fulljid) {
263         free(statusbar->fulljid);
264         statusbar->fulljid = NULL;
265     }
266     statusbar->fulljid = strdup(fulljid);
267 
268     status_bar_draw();
269 }
270 
271 void
status_bar_clear_fulljid(void)272 status_bar_clear_fulljid(void)
273 {
274     if (statusbar->fulljid) {
275         free(statusbar->fulljid);
276         statusbar->fulljid = NULL;
277     }
278 
279     status_bar_draw();
280 }
281 
282 void
status_bar_draw(void)283 status_bar_draw(void)
284 {
285     werase(statusbar_win);
286     wbkgd(statusbar_win, theme_attrs(THEME_STATUS_TEXT));
287 
288     int pos = 1;
289 
290     pos = _status_bar_draw_time(pos);
291 
292     _status_bar_draw_maintext(pos);
293 
294     pos = getmaxx(stdscr) - _tabs_width();
295     if (pos < 0) {
296         pos = 0;
297     }
298     gint max_tabs = prefs_get_statusbartabs();
299     for (int i = 1; i <= max_tabs; i++) {
300         StatusBarTab* tab = g_hash_table_lookup(statusbar->tabs, GINT_TO_POINTER(i));
301         if (tab) {
302             pos = _status_bar_draw_tab(tab, pos, i);
303         }
304     }
305 
306     _status_bar_draw_extended_tabs(pos);
307 
308     wnoutrefresh(statusbar_win);
309     inp_put_back();
310 }
311 
312 static gboolean
_extended_new(void)313 _extended_new(void)
314 {
315     gint max_tabs = prefs_get_statusbartabs();
316     int tabs_count = g_hash_table_size(statusbar->tabs);
317     if (tabs_count <= max_tabs) {
318         return FALSE;
319     }
320 
321     for (int i = max_tabs + 1; i <= tabs_count; i++) {
322         StatusBarTab* tab = g_hash_table_lookup(statusbar->tabs, GINT_TO_POINTER(i));
323         if (tab && tab->highlight) {
324             return TRUE;
325         }
326     }
327 
328     return FALSE;
329 }
330 
331 static int
_status_bar_draw_extended_tabs(int pos)332 _status_bar_draw_extended_tabs(int pos)
333 {
334     gint max_tabs = prefs_get_statusbartabs();
335     if (max_tabs == 0) {
336         return pos;
337     }
338 
339     if (g_hash_table_size(statusbar->tabs) > max_tabs) {
340         gboolean is_current = statusbar->current_tab > max_tabs;
341 
342         pos = _status_bar_draw_bracket(is_current, pos, "[");
343 
344         int status_attrs;
345         if (is_current) {
346             // currently selected
347             status_attrs = theme_attrs(THEME_STATUS_CURRENT);
348         } else if (_extended_new()) {
349             // new one
350             status_attrs = theme_attrs(THEME_STATUS_NEW);
351         } else {
352             // all other
353             status_attrs = theme_attrs(THEME_STATUS_ACTIVE);
354         }
355         wattron(statusbar_win, status_attrs);
356         mvwprintw(statusbar_win, 0, pos, ">");
357         wattroff(statusbar_win, status_attrs);
358         pos++;
359 
360         pos = _status_bar_draw_bracket(is_current, pos, "]");
361     }
362 
363     return pos;
364 }
365 
366 static int
_status_bar_draw_tab(StatusBarTab * tab,int pos,int num)367 _status_bar_draw_tab(StatusBarTab* tab, int pos, int num)
368 {
369     int display_num = num == 10 ? 0 : num;
370     gboolean is_current = num == statusbar->current_tab;
371 
372     gboolean show_number = prefs_get_boolean(PREF_STATUSBAR_SHOW_NUMBER);
373     gboolean show_name = prefs_get_boolean(PREF_STATUSBAR_SHOW_NAME);
374     gboolean show_read = prefs_get_boolean(PREF_STATUSBAR_SHOW_READ);
375 
376     // dont show this
377     if (!show_read && !is_current && !tab->highlight)
378         return pos;
379 
380     pos = _status_bar_draw_bracket(is_current, pos, "[");
381 
382     int status_attrs;
383     if (is_current) {
384         status_attrs = theme_attrs(THEME_STATUS_CURRENT);
385     } else if (tab->highlight) {
386         status_attrs = theme_attrs(THEME_STATUS_NEW);
387     } else {
388         status_attrs = theme_attrs(THEME_STATUS_ACTIVE);
389     }
390     wattron(statusbar_win, status_attrs);
391     if (show_number) {
392         mvwprintw(statusbar_win, 0, pos, "%d", display_num);
393         pos++;
394     }
395     if (show_number && show_name) {
396         mvwprintw(statusbar_win, 0, pos, ":");
397         pos++;
398     }
399     if (show_name) {
400         char* display_name = _display_name(tab);
401         mvwprintw(statusbar_win, 0, pos, display_name);
402         pos += utf8_display_len(display_name);
403         free(display_name);
404     }
405     wattroff(statusbar_win, status_attrs);
406 
407     pos = _status_bar_draw_bracket(is_current, pos, "]");
408 
409     return pos;
410 }
411 
412 static int
_status_bar_draw_bracket(gboolean current,int pos,char * ch)413 _status_bar_draw_bracket(gboolean current, int pos, char* ch)
414 {
415     int bracket_attrs = theme_attrs(THEME_STATUS_BRACKET);
416     wattron(statusbar_win, bracket_attrs);
417     if (current) {
418         mvwprintw(statusbar_win, 0, pos, "-");
419     } else {
420         mvwprintw(statusbar_win, 0, pos, ch);
421     }
422     wattroff(statusbar_win, bracket_attrs);
423     pos++;
424 
425     return pos;
426 }
427 
428 static int
_status_bar_draw_time(int pos)429 _status_bar_draw_time(int pos)
430 {
431     char* time_pref = prefs_get_string(PREF_TIME_STATUSBAR);
432     if (g_strcmp0(time_pref, "off") == 0) {
433         g_free(time_pref);
434         return pos;
435     }
436 
437     if (statusbar->time) {
438         g_free(statusbar->time);
439         statusbar->time = NULL;
440     }
441 
442     GDateTime* datetime = g_date_time_new_now(tz);
443     statusbar->time = g_date_time_format(datetime, time_pref);
444     assert(statusbar->time != NULL);
445     g_date_time_unref(datetime);
446 
447     int bracket_attrs = theme_attrs(THEME_STATUS_BRACKET);
448     int time_attrs = theme_attrs(THEME_STATUS_TIME);
449 
450     size_t len = strlen(statusbar->time);
451     wattron(statusbar_win, bracket_attrs);
452     mvwaddch(statusbar_win, 0, pos, '[');
453     pos++;
454     wattroff(statusbar_win, bracket_attrs);
455     wattron(statusbar_win, time_attrs);
456     mvwprintw(statusbar_win, 0, pos, statusbar->time);
457     pos += len;
458     wattroff(statusbar_win, time_attrs);
459     wattron(statusbar_win, bracket_attrs);
460     mvwaddch(statusbar_win, 0, pos, ']');
461     wattroff(statusbar_win, bracket_attrs);
462     pos += 2;
463 
464     g_free(time_pref);
465 
466     return pos;
467 }
468 
469 static void
_status_bar_draw_maintext(int pos)470 _status_bar_draw_maintext(int pos)
471 {
472     if (statusbar->prompt) {
473         mvwprintw(statusbar_win, 0, pos, statusbar->prompt);
474         return;
475     }
476 
477     gboolean stop = FALSE;
478 
479     if (statusbar->fulljid) {
480         char* pref = prefs_get_string(PREF_STATUSBAR_SELF);
481 
482         if (g_strcmp0(pref, "off") == 0) {
483             stop = true;
484         } else if (g_strcmp0(pref, "user") == 0) {
485             Jid* jidp = jid_create(statusbar->fulljid);
486             mvwprintw(statusbar_win, 0, pos, jidp->localpart);
487             jid_destroy(jidp);
488             stop = true;
489         } else if (g_strcmp0(pref, "barejid") == 0) {
490             Jid* jidp = jid_create(statusbar->fulljid);
491             mvwprintw(statusbar_win, 0, pos, jidp->barejid);
492             jid_destroy(jidp);
493             stop = true;
494         }
495 
496         g_free(pref);
497         if (stop) {
498             return;
499         }
500         mvwprintw(statusbar_win, 0, pos, statusbar->fulljid);
501     }
502 }
503 
504 static void
_destroy_tab(StatusBarTab * tab)505 _destroy_tab(StatusBarTab* tab)
506 {
507     if (tab) {
508         if (tab->identifier) {
509             free(tab->identifier);
510         }
511         if (tab->display_name) {
512             free(tab->display_name);
513         }
514         free(tab);
515     }
516 }
517 
518 static int
_tabs_width(void)519 _tabs_width(void)
520 {
521     gboolean show_number = prefs_get_boolean(PREF_STATUSBAR_SHOW_NUMBER);
522     gboolean show_name = prefs_get_boolean(PREF_STATUSBAR_SHOW_NAME);
523     gboolean show_read = prefs_get_boolean(PREF_STATUSBAR_SHOW_READ);
524     gint max_tabs = prefs_get_statusbartabs();
525 
526     if (show_name && show_number) {
527         int width = g_hash_table_size(statusbar->tabs) > max_tabs ? 4 : 1;
528         for (int i = 1; i <= max_tabs; i++) {
529             StatusBarTab* tab = g_hash_table_lookup(statusbar->tabs, GINT_TO_POINTER(i));
530             if (tab) {
531                 gboolean is_current = i == statusbar->current_tab;
532                 // dont calculate this in because not shown
533                 if (!show_read && !is_current && !tab->highlight)
534                     continue;
535 
536                 char* display_name = _display_name(tab);
537                 width += utf8_display_len(display_name);
538                 width += 4;
539                 free(display_name);
540             }
541         }
542         return width;
543     }
544 
545     if (show_name && !show_number) {
546         int width = g_hash_table_size(statusbar->tabs) > max_tabs ? 4 : 1;
547         for (int i = 1; i <= max_tabs; i++) {
548             StatusBarTab* tab = g_hash_table_lookup(statusbar->tabs, GINT_TO_POINTER(i));
549             if (tab) {
550                 gboolean is_current = i == statusbar->current_tab;
551                 // dont calculate this in because not shown
552                 if (!show_read && !is_current && !tab->highlight)
553                     continue;
554 
555                 char* display_name = _display_name(tab);
556                 width += utf8_display_len(display_name);
557                 width += 2;
558                 free(display_name);
559             }
560         }
561         return width;
562     }
563 
564     if (g_hash_table_size(statusbar->tabs) > max_tabs) {
565         return max_tabs * 3 + (g_hash_table_size(statusbar->tabs) > max_tabs ? 4 : 1);
566     }
567     return g_hash_table_size(statusbar->tabs) * 3 + (g_hash_table_size(statusbar->tabs) > max_tabs ? 4 : 1);
568 }
569 
570 static char*
_display_name(StatusBarTab * tab)571 _display_name(StatusBarTab* tab)
572 {
573     char* fullname = NULL;
574 
575     if (tab->window_type == WIN_CONSOLE) {
576         fullname = strdup("console");
577     } else if (tab->window_type == WIN_XML) {
578         fullname = strdup("xmlconsole");
579     } else if (tab->window_type == WIN_PLUGIN) {
580         fullname = strdup(tab->identifier);
581     } else if (tab->window_type == WIN_CHAT) {
582         if (tab && tab->display_name) {
583             fullname = strdup(tab->display_name);
584         }
585     } else if (tab->window_type == WIN_MUC) {
586         char* pref = prefs_get_string(PREF_STATUSBAR_ROOM);
587         if (g_strcmp0("room", pref) == 0) {
588             Jid* jidp = jid_create(tab->identifier);
589             char* room = strdup(jidp->localpart);
590             jid_destroy(jidp);
591             fullname = room;
592         } else {
593             fullname = strdup(tab->identifier);
594         }
595         g_free(pref);
596     } else if (tab->window_type == WIN_CONFIG) {
597         char* pref = prefs_get_string(PREF_STATUSBAR_ROOM);
598         GString* display_str = g_string_new("");
599 
600         if (g_strcmp0("room", pref) == 0) {
601             Jid* jidp = jid_create(tab->identifier);
602             g_string_append(display_str, jidp->localpart);
603             jid_destroy(jidp);
604         } else {
605             g_string_append(display_str, tab->identifier);
606         }
607 
608         g_free(pref);
609         g_string_append(display_str, " conf");
610         char* result = strdup(display_str->str);
611         g_string_free(display_str, TRUE);
612         fullname = result;
613     } else if (tab->window_type == WIN_PRIVATE) {
614         char* pref = prefs_get_string(PREF_STATUSBAR_ROOM);
615         if (g_strcmp0("room", pref) == 0) {
616             GString* display_str = g_string_new("");
617             Jid* jidp = jid_create(tab->identifier);
618             g_string_append(display_str, jidp->localpart);
619             g_string_append(display_str, "/");
620             g_string_append(display_str, jidp->resourcepart);
621             jid_destroy(jidp);
622             char* result = strdup(display_str->str);
623             g_string_free(display_str, TRUE);
624             fullname = result;
625         } else {
626             fullname = strdup(tab->identifier);
627         }
628         g_free(pref);
629     } else {
630         fullname = strdup("window");
631     }
632 
633     gint tablen = prefs_get_statusbartablen();
634     if (tablen == 0) {
635         return fullname;
636     }
637 
638     int namelen = utf8_display_len(fullname);
639     if (namelen < tablen) {
640         return fullname;
641     }
642 
643     gchar* trimmed = g_utf8_substring(fullname, 0, tablen);
644     free(fullname);
645     char* trimmedname = strdup(trimmed);
646     g_free(trimmed);
647 
648     return trimmedname;
649 }
650