From 0d6d3669554efaff6cf7a2975351c5b4fb123804 Mon Sep 17 00:00:00 2001 From: kofany Date: Fri, 22 Aug 2025 08:01:22 +0200 Subject: [PATCH] Add sidepanels feature with window and nicklist panels Introduces left and right sidepanels to the text UI, including new source files, theme formats, and main window layout logic. Adds mouse support, activity-based coloring, and dynamic panel reservation. Updates build scripts and headers to support the new feature. --- .gitignore | 2 + src/fe-text/gui-readline.c | 6 + src/fe-text/irssi.c | 6 + src/fe-text/mainwindows.c | 148 +++- src/fe-text/mainwindows.h | 5 + src/fe-text/meson.build | 2 + src/fe-text/module-formats.c | 17 + src/fe-text/module-formats.h | 25 +- src/fe-text/sidepanels.c | 1601 ++++++++++++++++++++++++++++++++++ src/fe-text/sidepanels.h | 13 + tests/fe-text/meson.build | 2 + themes/default.theme | 13 + 12 files changed, 1824 insertions(+), 16 deletions(-) create mode 100644 src/fe-text/sidepanels.c create mode 100644 src/fe-text/sidepanels.h diff --git a/.gitignore b/.gitignore index ea2cd7ba..6f8a9101 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,5 @@ subprojects/* Irssi-Dist setup.cfg *.egg-info +src/.DS_Store +.DS_Store diff --git a/src/fe-text/gui-readline.c b/src/fe-text/gui-readline.c index a53e4151..91fefae3 100644 --- a/src/fe-text/gui-readline.c +++ b/src/fe-text/gui-readline.c @@ -35,6 +35,7 @@ #include #include #include +#include #include #include @@ -544,6 +545,11 @@ static void sig_gui_key_pressed(gpointer keyp) key = GPOINTER_TO_INT(keyp); + /* sidepanels: let mouse parser consume if applicable */ + if (sidepanels_try_parse_mouse_key(key)) { + return; + } + if (redir != NULL && redir->flags & ENTRY_REDIRECT_FLAG_HOTKEY) { handle_key_redirect(key); return; diff --git a/src/fe-text/irssi.c b/src/fe-text/irssi.c index 9952f748..8d8ad0c0 100644 --- a/src/fe-text/irssi.c +++ b/src/fe-text/irssi.c @@ -42,6 +42,7 @@ #include #include #include +#include #include #include @@ -64,6 +65,9 @@ void mainwindow_activity_deinit(void); void mainwindows_layout_init(void); void mainwindows_layout_deinit(void); +void sidepanels_init(void); +void sidepanels_deinit(void); + static int dirty, full_redraw; static GMainLoop *main_loop; @@ -186,6 +190,7 @@ static void textui_finish_init(void) mainwindow_activity_init(); mainwindows_layout_init(); gui_windows_init(); + sidepanels_init(); /* Temporarily raise the fatal level to abort on config errors. */ loglev = critical_fatal_section_begin(); statusbar_init(); @@ -244,6 +249,7 @@ static void textui_deinit(void) lastlog_deinit(); statusbar_deinit(); + sidepanels_deinit(); gui_entry_deinit(); gui_printtext_deinit(); gui_readline_deinit(); diff --git a/src/fe-text/mainwindows.c b/src/fe-text/mainwindows.c index b9f77c0a..447bb0ea 100644 --- a/src/fe-text/mainwindows.c +++ b/src/fe-text/mainwindows.c @@ -39,6 +39,7 @@ MAIN_WINDOW_BORDER_REC *clrtoeol_info; int screen_reserved_top, screen_reserved_bottom; int screen_reserved_left, screen_reserved_right; static int screen_width, screen_height; +static int screen_collapsed = 0; /* see mainwindows_resize */ #define mainwindow_create_screen(window) \ term_window_create((window)->first_column + (window)->statusbar_columns_left, \ @@ -103,7 +104,16 @@ static void mainwindow_resize_windows(MAIN_WINDOW_REC *window) GSList *tmp; int resized; - mainwindow_set_screen_size(window); + /* Clamp screen window size to at least 1x1 to keep text views valid */ + { + int sx = window->first_column + window->statusbar_columns_left; + int sy = window->first_line + window->statusbar_lines_top; + int sw = window->width - window->statusbar_columns; + int sh = window->height - window->statusbar_lines; + if (sw < 1) sw = 1; + if (sh < 1) sh = 1; + term_window_move(window->screen_win, sx, sy, sw, sh); + } resized = FALSE; for (tmp = windows; tmp != NULL; tmp = tmp->next) { @@ -112,9 +122,14 @@ static void mainwindow_resize_windows(MAIN_WINDOW_REC *window) if (rec->gui_data != NULL && WINDOW_GUI(rec)->parent == window && !window_size_equals(rec, window)) { - resized = TRUE; - gui_window_resize(rec, MAIN_WINDOW_TEXT_WIDTH(window), - MAIN_WINDOW_TEXT_HEIGHT(window)); + { + int tw = MAIN_WINDOW_TEXT_WIDTH(window); + int th = MAIN_WINDOW_TEXT_HEIGHT(window); + if (tw < 1) tw = 1; + if (th < 1) th = 1; + resized = TRUE; + gui_window_resize(rec, tw, th); + } } } @@ -724,15 +739,20 @@ static void mainwindows_rresize_line(int xdiff, MAIN_WINDOW_REC *win) int windows, i, extra_width, next_column, shrunk; int *widths; GSList *line, *tmp; + int new_avail, old_avail, width_mod; line = mainwindows_get_line(win); windows = g_slist_length(line); widths = g_new0(int, windows); - extra_width = screen_width - windows + 1; + /* Available text width on this line should respect globally reserved columns */ + new_avail = screen_width - screen_reserved_left - screen_reserved_right - windows + 1; + old_avail = (screen_width - xdiff) - screen_reserved_left - screen_reserved_right - windows + 1; + extra_width = new_avail; for (tmp = line, i = 0; tmp != NULL; tmp = tmp->next, i++) { MAIN_WINDOW_REC *rec = tmp->data; - widths[i] = (MAIN_WINDOW_TEXT_WIDTH(rec) * (screen_width - windows + 1)) / (screen_width - xdiff - windows + 1); + /* Scale each window proportionally based on available text widths (excluding reserved columns) */ + widths[i] = old_avail > 0 ? (MAIN_WINDOW_TEXT_WIDTH(rec) * new_avail) / old_avail : MAIN_WINDOW_TEXT_WIDTH(rec); extra_width -= widths[i] + rec->statusbar_columns; } shrunk = FALSE; @@ -744,18 +764,31 @@ static void mainwindows_rresize_line(int xdiff, MAIN_WINDOW_REC *win) } } - next_column = 0; + /* Start after reserved left columns */ + next_column = screen_reserved_left; -#define extra ( (i >= screen_width % windows && i < extra_width + (screen_width % windows)) \ - || i + windows < extra_width + (screen_width % windows) ? 1 : 0 ) + /* Distribute any leftover width across windows; base modulo on available width */ + width_mod = new_avail % windows; +#define extra ( (i >= width_mod && i < extra_width + width_mod) \ + || i + windows < extra_width + width_mod ? 1 : 0 ) for (tmp = line, i = 0; tmp != NULL; tmp = tmp->next, i++) { MAIN_WINDOW_REC *rec = tmp->data; + gboolean is_last = (tmp->next == NULL); rec->first_column = next_column; - rec->last_column = rec->first_column + widths[i] + rec->statusbar_columns + extra - 1; - next_column = rec->last_column + 2; - mainwindow_resize(rec, widths[i] + rec->statusbar_columns + extra - rec->width, 0); + if (is_last) { + /* Anchor rightmost window to reserved-right boundary to avoid gap/lag */ + rec->last_column = screen_width - 1 - screen_reserved_right; + mainwindow_resize(rec, (rec->last_column - rec->first_column + 1) - rec->width, 0); + } else { + rec->last_column = rec->first_column + widths[i] + rec->statusbar_columns + extra - 1; + /* Ensure we never write past the globally reserved right columns */ + if (rec->last_column > screen_width - 1 - screen_reserved_right) + rec->last_column = screen_width - 1 - screen_reserved_right; + mainwindow_resize(rec, widths[i] + rec->statusbar_columns + extra - rec->width, 0); + } rec->size_dirty = TRUE; + next_column = rec->last_column + 2; } #undef extra @@ -772,6 +805,38 @@ void mainwindows_resize(int width, int height) screen_width = width; screen_height = height; + /* Collapse mode: allow resize down to 1x1 and avoid destructive layout. */ + { + int avail_w = screen_width - screen_reserved_left - screen_reserved_right; + int avail_h = screen_height - screen_reserved_top - screen_reserved_bottom; + if (avail_w <= 1 || avail_h <= 1) { + GSList *tmp; + if (!screen_collapsed) screen_collapsed = 1; + for (tmp = mainwindows; tmp != NULL; tmp = tmp->next) { + MAIN_WINDOW_REC *rec = tmp->data; + rec->first_column = screen_reserved_left; + rec->last_column = screen_reserved_left; + rec->width = 1; + rec->first_line = screen_reserved_top; + rec->last_line = screen_reserved_top; + rec->height = 1; + rec->size_dirty = TRUE; + rec->dirty = TRUE; + } + signal_emit("terminal resized", 0); + irssi_redraw(); + return; + } else if (screen_collapsed) { + /* Recover from collapsed state: reflow horizontally. */ + MAIN_WINDOW_REC *win; + screen_collapsed = 0; + for (win = mainwindows_find_lower(NULL); win != NULL; win = mainwindows_find_lower(win)) { + mainwindows_rresize_line(0, win); + } + irssi_set_dirty(); + } + } + if (ydiff > 0) { /* algorithm: enlarge bottom window */ MAIN_WINDOW_REC *rec; @@ -808,10 +873,11 @@ void mainwindows_resize(int width, int height) GSList *line, *tmp; line = mainwindows_get_line(win); - max_windows = (screen_width + 1) / (NEW_WINDOW_WIDTH + 1); + /* Respect globally reserved columns when computing capacity */ + max_windows = (screen_width - screen_reserved_left - screen_reserved_right + 1) / (NEW_WINDOW_WIDTH + 1); if (max_windows < 1) max_windows = 1; - last_column = screen_width - 1; + last_column = screen_width - 1 - screen_reserved_right; for (tmp = line, i = 0; tmp != NULL; tmp = tmp->next, i++) { MAIN_WINDOW_REC *rec = tmp->data; if (i >= max_windows) @@ -822,7 +888,7 @@ void mainwindows_resize(int width, int height) win = line->data; g_slist_free(line); - mainwindows_rresize_line(screen_width - last_column + 1, win); + mainwindows_rresize_line(screen_width - screen_reserved_right - last_column + 1, win); } } @@ -2006,3 +2072,55 @@ void mainwindows_deinit(void) command_unbind("window move down", (SIGNAL_FUNC) cmd_window_move_down); signal_remove("window print info", (SIGNAL_FUNC) sig_window_print_info); } + +int mainwindows_reserve_columns(int left, int right) +{ + MAIN_WINDOW_REC *window; + int ret = -1; + if (left != 0) { + GSList *list, *tmp; + g_return_val_if_fail(left > 0 || screen_reserved_left > left, -1); + ret = screen_reserved_left; + screen_reserved_left += left; + list = mainwindows_get_line(mainwindows_find_lower_right(NULL)); + for (tmp = list; tmp != NULL; tmp = tmp->next) { + window = tmp->data; + window->first_column += left; + mainwindow_resize(window, -left, 0); + } + g_slist_free(list); + } + if (right != 0) { + GSList *list, *tmp; + g_return_val_if_fail(right > 0 || screen_reserved_right > right, -1); + ret = screen_reserved_right; + screen_reserved_right += right; + list = mainwindows_get_line(mainwindows_find_left_upper(NULL)); + for (tmp = list; tmp != NULL; tmp = tmp->next) { + window = tmp->data; + window->last_column -= right; + mainwindow_resize(window, -right, 0); + } + g_slist_free(list); + } + return ret; +} + +int mainwindow_set_statusbar_columns(MAIN_WINDOW_REC *window, + int left, int right) +{ + int ret = -1; + if (left != 0) { + ret = window->statusbar_columns_left; + window->statusbar_columns_left += left; + window->statusbar_columns += left; + } + if (right != 0) { + ret = window->statusbar_columns_right; + window->statusbar_columns_right += right; + window->statusbar_columns += right; + } + if (left+right != 0) + window->size_dirty = TRUE; + return ret; +} diff --git a/src/fe-text/mainwindows.h b/src/fe-text/mainwindows.h index 52cf2df3..432c1c57 100644 --- a/src/fe-text/mainwindows.h +++ b/src/fe-text/mainwindows.h @@ -68,6 +68,11 @@ int mainwindow_set_statusbar_lines(MAIN_WINDOW_REC *window, int top, int bottom); void mainwindows_redraw_dirty(void); +/* Reserve columns at left/right of a main window (for side panels). */ +int mainwindows_reserve_columns(int left, int right); +int mainwindow_set_statusbar_columns(MAIN_WINDOW_REC *window, + int left, int right); + GSList *mainwindows_get_sorted(int reverse); GSList *mainwindows_get_line(MAIN_WINDOW_REC *rec); diff --git a/src/fe-text/meson.build b/src/fe-text/meson.build index 94990c82..27f0fc0a 100644 --- a/src/fe-text/meson.build +++ b/src/fe-text/meson.build @@ -18,6 +18,7 @@ executable('irssi', 'mainwindows-layout.c', 'mainwindows.c', 'module-formats.c', + 'sidepanels.c', 'statusbar-config.c', 'statusbar-items.c', 'statusbar.c', @@ -50,6 +51,7 @@ install_headers( 'gui-printtext.h', 'gui-windows.h', 'mainwindows.h', + 'sidepanels.h', 'statusbar-item.h', 'statusbar.h', 'term.h', diff --git a/src/fe-text/module-formats.c b/src/fe-text/module-formats.c index 6c2ba0a9..e1bbc1fa 100644 --- a/src/fe-text/module-formats.c +++ b/src/fe-text/module-formats.c @@ -102,6 +102,23 @@ FORMAT_REC gui_text_formats[] = { "- - - - - - - - - - - - - - - - - - - - - - - - - - - -", 0 }, { "welcome_init_settings", "The following settings were initialized", 0 }, + /* ---- */ + { NULL, "Sidepanels", 0 }, + { "sidepanel_header", "%K$0", 1, { 0 } }, + { "sidepanel_item", "$0", 1, { 0 } }, + { "sidepanel_item_selected", "%U$0%U", 1, { 0 } }, + { "sidepanel_item_nick_mention", "%M$0", 1, { 0 } }, + { "sidepanel_item_query_msg", "%M$0", 1, { 0 } }, + { "sidepanel_item_activity", "%y$0", 1, { 0 } }, + { "sidepanel_item_events", "%g$0", 1, { 0 } }, + { "sidepanel_item_highlight", "%R$0", 1, { 0 } }, + { "sidepanel_nick_op", "%Y$0", 1, { 0 } }, + { "sidepanel_nick_voice", "%C$0", 1, { 0 } }, + { "sidepanel_nick_normal", "$0", 1, { 0 } }, + { "sidepanel_nick_op_status", "%Y$0%n$1", 2, { 0, 0 } }, + { "sidepanel_nick_voice_status", "%C$0%n$1", 2, { 0, 0 } }, + { "sidepanel_nick_normal_status", "$0$1", 2, { 0, 0 } }, + { NULL, NULL, 0 } /* clang-format on */ }; diff --git a/src/fe-text/module-formats.h b/src/fe-text/module-formats.h index 5392705f..394d9232 100644 --- a/src/fe-text/module-formats.h +++ b/src/fe-text/module-formats.h @@ -1,7 +1,12 @@ +#ifndef IRSSI_FE_TEXT_MODULE_FORMATS_H +#define IRSSI_FE_TEXT_MODULE_FORMATS_H + #include +/* This mirrors indices in module-formats.c order; keep in sync if changed. */ + enum { - TXT_MODULE_NAME, + TXT_MODULE_NAME = 0, TXT_FILL_1, @@ -64,4 +69,22 @@ enum { TXT_COUNT }; +/* Sidepanel format indices appended at the end of gui_text_formats: we declare explicit macros. */ +#define TXT_SIDEPANEL_HEADER format_find_tag(MODULE_NAME, "sidepanel_header") +#define TXT_SIDEPANEL_ITEM format_find_tag(MODULE_NAME, "sidepanel_item") +#define TXT_SIDEPANEL_ITEM_SELECTED format_find_tag(MODULE_NAME, "sidepanel_item_selected") +#define TXT_SIDEPANEL_ITEM_ACTIVITY format_find_tag(MODULE_NAME, "sidepanel_item_activity") +#define TXT_SIDEPANEL_ITEM_NICK_MENTION format_find_tag(MODULE_NAME, "sidepanel_item_nick_mention") +#define TXT_SIDEPANEL_ITEM_QUERY_MSG format_find_tag(MODULE_NAME, "sidepanel_item_query_msg") +#define TXT_SIDEPANEL_ITEM_EVENTS format_find_tag(MODULE_NAME, "sidepanel_item_events") +#define TXT_SIDEPANEL_ITEM_HIGHLIGHT format_find_tag(MODULE_NAME, "sidepanel_item_highlight") +#define TXT_SIDEPANEL_NICK_OP format_find_tag(MODULE_NAME, "sidepanel_nick_op") +#define TXT_SIDEPANEL_NICK_VOICE format_find_tag(MODULE_NAME, "sidepanel_nick_voice") +#define TXT_SIDEPANEL_NICK_NORMAL format_find_tag(MODULE_NAME, "sidepanel_nick_normal") +#define TXT_SIDEPANEL_NICK_OP_STATUS format_find_tag(MODULE_NAME, "sidepanel_nick_op_status") +#define TXT_SIDEPANEL_NICK_VOICE_STATUS format_find_tag(MODULE_NAME, "sidepanel_nick_voice_status") +#define TXT_SIDEPANEL_NICK_NORMAL_STATUS format_find_tag(MODULE_NAME, "sidepanel_nick_normal_status") + extern FORMAT_REC gui_text_formats[TXT_COUNT+1]; + +#endif diff --git a/src/fe-text/sidepanels.c b/src/fe-text/sidepanels.c new file mode 100644 index 00000000..e976776c --- /dev/null +++ b/src/fe-text/sidepanels.c @@ -0,0 +1,1601 @@ +#include "module.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Custom data level for channel events (join/part/quit/nick) */ +#define DATA_LEVEL_EVENT 10 + +/* UTF-8 character reading function based on textbuffer-view.c */ +static inline unichar read_unichar(const unsigned char *data, const unsigned char **next, int *width) +{ + unichar chr = g_utf8_get_char_validated((const char *) data, -1); + if (chr & 0x80000000) { + chr = 0xfffd; /* replacement character for invalid UTF-8 */ + *next = data + 1; + *width = 1; + } else { + *next = (unsigned char *)g_utf8_next_char(data); + *width = unichar_isprint(chr) ? i_wcwidth(chr) : 1; + if (*width < 0) *width = 1; + } + return chr; +} + +/* Forward declarations for static functions used before definition (C89) */ +static void apply_reservations_all(void); +static void apply_and_redraw(void); +static void enable_mouse_tracking(void); +static void disable_mouse_tracking(void); +static void sp_logf(const char *fmt, ...); + +static void update_left_selection_to_active(void); +static void sig_window_item_changed(WINDOW_REC *w, WI_ITEM_REC *item); +static void sig_nicklist_new(CHANNEL_REC *ch, NICK_REC *nick); +static void clear_window_full(TERM_WINDOW *tw, int width, int height); +static void renumber_windows_by_position(void); + +/* Settings */ +static int sp_left_width; +static int sp_right_width; +static int sp_enable_left; +static int sp_enable_right; +static int sp_auto_hide_right; +static int sp_enable_mouse; +static int sp_debug; + +/* Window Priority State - Simpler approach */ +typedef struct { + WINDOW_REC *window; + int current_priority; /* 0=none, 1=events, 2=highlight, 3=activity, 4=nick/query */ +} window_priority_state; + +static GHashTable *window_priorities = NULL; + +static int esc_pending; +static int esc_timeout_tag = -1; /* Timer tag for ESC timeout */ +static int reemit_guard; + +static gboolean esc_timeout_callback(gpointer data) +{ + (void)data; + if (esc_pending && !reemit_guard) { + reemit_guard = 1; + signal_emit("gui key pressed", 1, GINT_TO_POINTER(0x1b)); + reemit_guard = 0; + esc_pending = 0; + } + esc_timeout_tag = -1; + return FALSE; +} + +static FILE *sp_log; +static void sp_log_open(void) { if (!sp_log) sp_log = fopen("/tmp/irssi_sidepanels.log", "a"); } +static void sp_logf(const char *fmt, ...) +{ + va_list ap; + if (!sp_debug) return; + sp_log_open(); + if (!sp_log) return; + va_start(ap, fmt); + vfprintf(sp_log, fmt, ap); + fprintf(sp_log, "\n"); + va_end(ap); + fflush(sp_log); +} + +static void read_settings(void) +{ + int old_mouse = sp_enable_mouse; + + sp_left_width = settings_get_int("sidepanel_left_width"); + sp_right_width = settings_get_int("sidepanel_right_width"); + sp_enable_left = settings_get_bool("sidepanel_left"); + sp_enable_right = settings_get_bool("sidepanel_right"); + sp_auto_hide_right = settings_get_bool("sidepanel_right_auto_hide"); + sp_enable_mouse = TRUE; /* always on natively */ + sp_debug = settings_get_bool("sidepanel_debug"); + + /* Nick mention color is now handled through theme formats */ + + apply_reservations_all(); + apply_and_redraw(); + if (!old_mouse) enable_mouse_tracking(); +} + +static void apply_reservations_all(void) +{ + GSList *tmp; + for (tmp = mainwindows; tmp != NULL; tmp = tmp->next) { + MAIN_WINDOW_REC *mw = tmp->data; + /* reset previous reservations if any by setting negative, then apply new */ + if (mw->statusbar_columns_left) + mainwindow_set_statusbar_columns(mw, -mw->statusbar_columns_left, 0); + if (mw->statusbar_columns_right) + mainwindow_set_statusbar_columns(mw, 0, -mw->statusbar_columns_right); + /* Left panel reservations are now handled in position_tw() for better control */ + /* Don't reserve right space here - let auto-hide logic in position_tw() decide */ + } +} + +static void sig_mainwindow_created(MAIN_WINDOW_REC *mw) +{ + /* Panel reservations are now handled dynamically in position_tw() for better control */ + (void)mw; +} + +typedef struct { + TERM_WINDOW *left_tw; + TERM_WINDOW *right_tw; + int left_w; + int right_w; + /* selection and scroll state */ + int left_selected_index; + int left_scroll_offset; + int right_selected_index; + int right_scroll_offset; + /* cached geometry for hit-test and drawing */ + int left_x; int left_y; int left_h; + int right_x; int right_y; int right_h; + /* ordered nick pointers matching rendered order */ + GSList *right_order; +} SP_MAINWIN_CTX; + +static GHashTable *mw_to_ctx; + +static SP_MAINWIN_CTX *get_ctx(MAIN_WINDOW_REC *mw, gboolean create) +{ + SP_MAINWIN_CTX *ctx = g_hash_table_lookup(mw_to_ctx, mw); + if (!ctx && create) { + ctx = g_new0(SP_MAINWIN_CTX, 1); + g_hash_table_insert(mw_to_ctx, mw, ctx); + } + return ctx; +} + +static void destroy_ctx(MAIN_WINDOW_REC *mw) +{ + SP_MAINWIN_CTX *ctx = g_hash_table_lookup(mw_to_ctx, mw); + if (!ctx) return; + if (ctx->left_tw) { term_window_destroy(ctx->left_tw); ctx->left_tw = NULL; } + if (ctx->right_tw) { term_window_destroy(ctx->right_tw); ctx->right_tw = NULL; } + if (ctx->right_order) { g_slist_free(ctx->right_order); ctx->right_order = NULL; } + g_hash_table_remove(mw_to_ctx, mw); + g_free(ctx); +} + +static void position_tw(MAIN_WINDOW_REC *mw, SP_MAINWIN_CTX *ctx) +{ + int y; + int h; + int x; + int w; + gboolean show_right; + WINDOW_REC *aw; + y = mw->first_line + mw->statusbar_lines_top; + h = mw->height - mw->statusbar_lines; + if (sp_enable_left && ctx->left_w > 0) { + /* Left panel is always at x=0, regardless of main window position */ + x = 0; + w = ctx->left_w; + if (ctx->left_tw) { + /* Panel already exists, just move to correct position */ + term_window_move(ctx->left_tw, x, y, w, h); + } else { + /* Reserve space for left panel - this shifts main window right */ + mainwindows_reserve_columns(ctx->left_w, 0); + ctx->left_tw = term_window_create(x, y, w, h); + /* Force statusbar redraw to fix input box positioning */ + signal_emit("mainwindow resized", 1, mw); + } + ctx->left_x = x; ctx->left_y = y; ctx->left_h = h; + } + else if (ctx->left_tw) { + /* Clear the left panel area before destroying */ + clear_window_full(ctx->left_tw, ctx->left_w, ctx->left_h); + term_window_destroy(ctx->left_tw); + ctx->left_tw = NULL; + ctx->left_h = 0; + /* Free reserved space - this shifts main window back left */ + mainwindows_reserve_columns(-ctx->left_w, 0); + /* Force complete recreation of mainwindows to clear artifacts */ + mainwindows_recreate(); + /* Force statusbar redraw to fix input box positioning */ + signal_emit("mainwindow resized", 1, mw); + } + /* Auto hide right if enabled and active item doesn't contain # (not a channel) */ + show_right = sp_enable_right && ctx->right_w > 0; + aw = mw->active; + if (sp_auto_hide_right) { + if (!aw || !aw->active || !aw->active->visible_name || !strchr(aw->active->visible_name, '#')) { + show_right = FALSE; + } + } + if (show_right) { + w = ctx->right_w; + if (ctx->right_tw) { + /* Panel already exists, space already reserved, use current last_column */ + x = mw->last_column + 1; + term_window_move(ctx->right_tw, x, y, w, h); + } else { + /* Reserve space for right panel - this shrinks main window */ + mainwindows_reserve_columns(0, ctx->right_w); + /* After reservation, right panel should be at the new last_column + 1 */ + x = mw->last_column + 1; + ctx->right_tw = term_window_create(x, y, w, h); + /* Force statusbar redraw to fix input box positioning */ + signal_emit("mainwindow resized", 1, mw); + } + ctx->right_x = x; ctx->right_y = y; ctx->right_h = h; + } + else if (ctx->right_tw) { + /* Clear the right panel area before destroying */ + clear_window_full(ctx->right_tw, ctx->right_w, ctx->right_h); + term_window_destroy(ctx->right_tw); + ctx->right_tw = NULL; + ctx->right_h = 0; + /* Free reserved space - this expands main window */ + mainwindows_reserve_columns(0, -ctx->right_w); + /* Force complete recreation of mainwindows to clear artifacts */ + mainwindows_recreate(); + /* Force statusbar redraw to fix input box positioning */ + signal_emit("mainwindow resized", 1, mw); + } +} + +static void draw_border_vertical(TERM_WINDOW *tw, int width, int height, int left) +{ + (void)tw; (void)width; (void)height; (void)left; +} + +static void draw_main_window_borders(MAIN_WINDOW_REC *mw) +{ + SP_MAINWIN_CTX *ctx = get_ctx(mw, FALSE); + if (!ctx) return; + + /* Draw left border (between left panel and main window) */ + if (ctx->left_tw && ctx->left_h > 0) { + int border_x = mw->first_column + mw->statusbar_columns_left - 1; + for (int y = 0; y < ctx->left_h; y++) { + gui_printtext_window_border(border_x, mw->first_line + mw->statusbar_lines_top + y); + } + } + + /* Draw right border (between main window and right panel) */ + if (ctx->right_tw && ctx->right_h > 0) { + int border_x = mw->last_column + 1; + for (int y = 0; y < ctx->right_h; y++) { + gui_printtext_window_border(border_x, mw->first_line + mw->statusbar_lines_top + y); + } + } +} + + +/* Get or create window priority state */ +static window_priority_state *get_window_priority_state(WINDOW_REC *window) +{ + window_priority_state *state; + + if (!window_priorities) + window_priorities = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_free); + + state = g_hash_table_lookup(window_priorities, window); + if (!state) { + state = g_new0(window_priority_state, 1); + state->window = window; + state->current_priority = 0; + g_hash_table_insert(window_priorities, window, state); + } + return state; +} + +/* Determine priority for new activity */ +static int get_new_activity_priority(WINDOW_REC *window, int data_level) +{ + WI_ITEM_REC *item = window ? window->active : NULL; + + /* Check specific data levels in order */ + if (data_level == DATA_LEVEL_EVENT) { + /* Channel events (join/part/quit/nick) get priority 1 (green) */ + return 1; /* Channel events */ + } + + if (data_level == DATA_LEVEL_HILIGHT) { + /* Nick mentions get priority 4 (magenta) */ + return 4; /* Nick mention */ + } + + if (data_level == DATA_LEVEL_MSG) { + /* Private messages always get priority 4 (magenta) */ + return 4; /* Query messages */ + } + + if (data_level == DATA_LEVEL_TEXT) { + if (item && IS_QUERY(item)) { + /* Query events get priority 1 (green) */ + return 1; /* Query events */ + } else { + /* Channel text activity gets priority 3 (yellow) */ + return 3; /* Channel activity */ + } + } + + return 0; /* No activity */ +} + +/* Handle new activity - core logic */ +static void handle_new_activity(WINDOW_REC *window, int data_level) +{ + window_priority_state *state; + int new_priority; + const char *data_level_name; + + if (!window) return; + + /* Debug: data level name */ + switch (data_level) { + case DATA_LEVEL_NONE: data_level_name = "NONE"; break; + case DATA_LEVEL_TEXT: data_level_name = "TEXT"; break; + case DATA_LEVEL_MSG: data_level_name = "MSG"; break; + case DATA_LEVEL_HILIGHT: data_level_name = "HILIGHT"; break; + case DATA_LEVEL_EVENT: data_level_name = "EVENT"; break; + default: data_level_name = "UNKNOWN"; break; + } + + state = get_window_priority_state(window); + new_priority = get_new_activity_priority(window, data_level); + + /* Debug: Track all activity events - useful for diagnosing priority conflicts */ + sp_logf("ACTIVITY: Window %d data_level=%s(%d) current_priority=%d new_priority=%d", + window->refnum, data_level_name, data_level, state->current_priority, new_priority); + + /* Only update if new priority is higher */ + if (new_priority > state->current_priority) { + state->current_priority = new_priority; + /* Debug: Priority changes - essential for activity system debugging */ + sp_logf("PRIORITY: Window %d priority updated from %d to %d (type=%s)", + window->refnum, state->current_priority, new_priority, data_level_name); + } else { + /* Debug: Priority ignored - shows when lower priority events are filtered */ + sp_logf("PRIORITY: Window %d ignored - current=%d >= new=%d (type=%s)", + window->refnum, state->current_priority, new_priority, data_level_name); + } +} + +/* Reset window priority when user opens it */ +static void reset_window_priority(WINDOW_REC *window) +{ + window_priority_state *state; + + if (!window) return; + + state = get_window_priority_state(window); + if (state->current_priority > 0) { + /* Debug: Priority reset - tracks when user opens windows */ + sp_logf("RESET: Window %d priority reset from %d to 0 (opened)", window->refnum, state->current_priority); + state->current_priority = 0; + } else { + /* Debug: Already reset - useful for detecting unnecessary resets */ + sp_logf("RESET: Window %d already at priority 0", window->refnum); + } +} + +static void clear_window_full(TERM_WINDOW *tw, int width, int height) +{ + int y; + int x; + if (!tw) return; + term_set_color(tw, ATTR_RESET); + for (y = 0; y < height; y++) { + term_move(tw, 0, y); + for (x = 0; x < width; x++) term_addch(tw, ' '); + } +} + +/* Color processing functions copied from textbuffer-view.c for proper color rendering */ +#define FGATTR (ATTR_NOCOLORS | ATTR_RESETFG | FG_MASK | ATTR_FGCOLOR24) +#define BGATTR (ATTR_NOCOLORS | ATTR_RESETBG | BG_MASK | ATTR_BGCOLOR24) + +static void unformat_24bit_line_color(const unsigned char **ptr, int off, int *flags, unsigned int *fg, unsigned int *bg) +{ + unsigned int color; + unsigned char rgbx[4]; + unsigned int i; + for (i = 0; i < 4; ++i) { + if ((*ptr)[i + off] == '\0') + return; + rgbx[i] = (*ptr)[i + off]; + } + rgbx[3] -= 0x20; + *ptr += 4; + for (i = 0; i < 3; ++i) { + if (rgbx[3] & (0x10 << i)) + rgbx[i] -= 0x20; + } + color = rgbx[0] << 16 | rgbx[1] << 8 | rgbx[2]; + if (rgbx[3] & 0x1) { + *flags = (*flags & FGATTR) | ATTR_BGCOLOR24; + *bg = color; + } + else { + *flags = (*flags & BGATTR) | ATTR_FGCOLOR24; + *fg = color; + } +} + +static inline void unformat(const unsigned char **ptr, int *color, unsigned int *fg24, unsigned int *bg24) +{ + switch (**ptr) { + case FORMAT_STYLE_BLINK: + *color ^= ATTR_BLINK; + break; + case FORMAT_STYLE_UNDERLINE: + *color ^= ATTR_UNDERLINE; + break; + case FORMAT_STYLE_BOLD: + *color ^= ATTR_BOLD; + break; + case FORMAT_STYLE_REVERSE: + *color ^= ATTR_REVERSE; + break; + case FORMAT_STYLE_ITALIC: + *color ^= ATTR_ITALIC; + break; + case FORMAT_STYLE_MONOSPACE: + /* *color ^= ATTR_MONOSPACE; */ + break; + case FORMAT_STYLE_DEFAULTS: + *color = ATTR_RESET; + break; + case FORMAT_STYLE_CLRTOEOL: + break; +#define SET_COLOR_EXT_FG_BITS(base, pc) \ + *color &= ~ATTR_FGCOLOR24; \ + *color = (*color & BGATTR) | (base + *pc - FORMAT_COLOR_NOCHANGE) +#define SET_COLOR_EXT_BG_BITS(base, pc) \ + *color &= ~ATTR_BGCOLOR24; \ + *color = (*color & FGATTR) | ((base + *pc - FORMAT_COLOR_NOCHANGE) << BG_SHIFT) + case FORMAT_COLOR_EXT1: + SET_COLOR_EXT_FG_BITS(0x10, ++*ptr); + break; + case FORMAT_COLOR_EXT1_BG: + SET_COLOR_EXT_BG_BITS(0x10, ++*ptr); + break; + case FORMAT_COLOR_EXT2: + SET_COLOR_EXT_FG_BITS(0x60, ++*ptr); + break; + case FORMAT_COLOR_EXT2_BG: + SET_COLOR_EXT_BG_BITS(0x60, ++*ptr); + break; + case FORMAT_COLOR_EXT3: + SET_COLOR_EXT_FG_BITS(0xb0, ++*ptr); + break; + case FORMAT_COLOR_EXT3_BG: + SET_COLOR_EXT_BG_BITS(0xb0, ++*ptr); + break; +#undef SET_COLOR_EXT_BG_BITS +#undef SET_COLOR_EXT_FG_BITS + case FORMAT_COLOR_24: + unformat_24bit_line_color(ptr, 1, color, fg24, bg24); + break; + default: + if (**ptr != FORMAT_COLOR_NOCHANGE) { + if (**ptr == (unsigned char) 0xff) { + *color = (*color & BGATTR) | ATTR_RESETFG; + } else { + *color = (*color & BGATTR) | (((unsigned char) **ptr - '0') & 0xf); + } + } + if ((*ptr)[1] == '\0') + break; + + (*ptr)++; + if (**ptr != FORMAT_COLOR_NOCHANGE) { + if (**ptr == (unsigned char) 0xff) { + *color = (*color & FGATTR) | ATTR_RESETBG; + } else { + *color = (*color & FGATTR) | + ((((unsigned char) **ptr - '0') & 0xf) << BG_SHIFT); + } + } + } + if (**ptr == '\0') + return; +} + +static void draw_str_themed(TERM_WINDOW *tw, int x, int y, WINDOW_REC *wctx, int format_id, const char *text) +{ + TEXT_DEST_REC dest; + THEME_REC *theme; + char *out, *expanded; + const unsigned char *ptr; + const unsigned char *next_ptr; + int color; + int char_width; + unsigned int fg24, bg24; + unichar chr; + + format_create_dest(&dest, NULL, NULL, 0, wctx); + theme = window_get_theme(wctx); + out = format_get_text_theme(theme, MODULE_NAME, &dest, format_id, text); + /* Debug: Uncomment to trace theme format rendering issues */ + // sp_logf("THEME_DEBUG: format_id=%d text='%s' out='%s'", format_id, text ? text : "(null)", out ? out : "(null)"); + + if (out != NULL && *out != '\0') { + /* Convert theme color codes and render with proper color handling */ + expanded = format_string_expand(out, NULL); + + /* Initialize color state */ + color = ATTR_RESET; + fg24 = bg24 = UINT_MAX; + ptr = (const unsigned char *)expanded; + + term_move(tw, x, y); + term_set_color(tw, ATTR_RESET); + + /* Process each character with color codes (like textbuffer-view.c) */ + while (*ptr != '\0') { + if (*ptr == 4) { + /* Format code - process color change */ + ptr++; + if (*ptr == '\0') break; + unformat(&ptr, &color, &fg24, &bg24); + term_set_color2(tw, color, fg24, bg24); + ptr++; + continue; + } + + /* Regular character - read UTF-8 properly */ + chr = read_unichar(ptr, &next_ptr, &char_width); + + if (unichar_isprint(chr)) { + term_add_unichar(tw, chr); + } + ptr = next_ptr; + } + + g_free(expanded); + } else { + /* Fallback: display plain text if theme formatting fails */ + term_move(tw, x, y); + term_addstr(tw, text ? text : ""); + } + g_free(out); +} + +/* + * NEW DUAL-PARAMETER THEME FORMATS FOR NICKLIST CUSTOMIZATION + * + * The new *_status formats allow separate styling of status symbols and nicks: + * + * Example theme customization: + * + * "fe-text" = { + * # Different colors for @ symbol vs nick: + * sidepanel_nick_op_status = "%R$0%Y$1"; # Red @ + Yellow nick + * + * # Decorative brackets around status: + * sidepanel_nick_op_status = "%Y[$0]%N$1"; # [@]nick + * + * # Hide status completely (just colorized nick): + * sidepanel_nick_op_status = "%Y$1"; # Only yellow nick, no @ + * + * # Custom symbols instead of @ and +: + * sidepanel_nick_op_status = "%R⚡%N%Y$1"; # ⚡nick instead of @nick + * sidepanel_nick_voice_status = "%C◆%N%c$1"; # ◆nick instead of +nick + * }; + */ +static void draw_str_themed_2params(TERM_WINDOW *tw, int x, int y, WINDOW_REC *wctx, int format_id, const char *param1, const char *param2) +{ + TEXT_DEST_REC dest; + THEME_REC *theme; + char *out, *expanded; + const unsigned char *ptr; + const unsigned char *next_ptr; + int color; + int char_width; + unsigned int fg24, bg24; + unichar chr; + char *args[3]; + + format_create_dest(&dest, NULL, NULL, 0, wctx); + theme = window_get_theme(wctx); + + /* Create args array for format_get_text_theme_charargs */ + args[0] = (char*)param1; + args[1] = (char*)param2; + args[2] = NULL; + + out = format_get_text_theme_charargs(theme, MODULE_NAME, &dest, format_id, args); + /* Debug: Uncomment to trace 2-parameter theme format rendering */ + // sp_logf("THEME_DEBUG_2P: format_id=%d param1='%s' param2='%s' out='%s'", + // format_id, param1 ? param1 : "(null)", param2 ? param2 : "(null)", out ? out : "(null)"); + + if (out != NULL && *out != '\0') { + /* Convert theme color codes and render with proper color handling */ + expanded = format_string_expand(out, NULL); + + /* Initialize color state */ + color = ATTR_RESET; + fg24 = bg24 = UINT_MAX; + ptr = (const unsigned char *)expanded; + + term_move(tw, x, y); + term_set_color(tw, ATTR_RESET); + + /* Process each character with color codes (like textbuffer-view.c) */ + while (*ptr != '\0') { + if (*ptr == 4) { + /* Format code - process color change */ + ptr++; + if (*ptr == '\0') break; + unformat(&ptr, &color, &fg24, &bg24); + term_set_color2(tw, color, fg24, bg24); + ptr++; + continue; + } + + /* Regular character - read UTF-8 properly */ + chr = read_unichar(ptr, &next_ptr, &char_width); + + if (unichar_isprint(chr)) { + term_add_unichar(tw, chr); + } + ptr = next_ptr; + } + + g_free(expanded); + } else { + /* Fallback: display plain text if theme formatting fails */ + term_move(tw, x, y); + term_addstr(tw, param1 ? param1 : ""); + term_addstr(tw, param2 ? param2 : ""); + } + g_free(out); +} + + + + + +/* Renumber all windows according to sorted display order */ +typedef struct { + WINDOW_REC *win; + int sort_group; /* 0=Notices, 1=server, 2=channel, 3=query, 4=named_orphan, 5=unnamed_orphan */ + char *sort_key; /* For alphabetical sorting within group */ + SERVER_REC *server; /* Server for grouping */ +} WINDOW_SORT_REC; + +static gint compare_window_sort_items(gconstpointer a, gconstpointer b) +{ + WINDOW_SORT_REC *w1 = (WINDOW_SORT_REC*)a; + WINDOW_SORT_REC *w2 = (WINDOW_SORT_REC*)b; + const char *net1, *net2; + int server_cmp; + + /* 1. Notices always comes first */ + if (w1->sort_group == 0) return -1; /* w1 is Notices */ + if (w2->sort_group == 0) return 1; /* w2 is Notices */ + + /* 2. Sort by server (alphabetically) */ + if (w1->server && w2->server) { + net1 = w1->server->connrec && w1->server->connrec->chatnet ? w1->server->connrec->chatnet : + (w1->server->connrec ? w1->server->connrec->address : "server"); + net2 = w2->server->connrec && w2->server->connrec->chatnet ? w2->server->connrec->chatnet : + (w2->server->connrec ? w2->server->connrec->address : "server"); + server_cmp = g_ascii_strcasecmp(net1 ? net1 : "", net2 ? net2 : ""); + if (server_cmp != 0) return server_cmp; + } else if (w1->server && !w2->server) { + return -1; /* Server windows come before non-server windows */ + } else if (!w1->server && w2->server) { + return 1; /* Server windows come before non-server windows */ + } + + /* 3. Within same server, sort by type: server status < channels < queries < orphans */ + if (w1->sort_group != w2->sort_group) { + return w1->sort_group - w2->sort_group; + } + + /* 4. Within same type, sort alphabetically by name */ + return g_ascii_strcasecmp(w1->sort_key ? w1->sort_key : "", w2->sort_key ? w2->sort_key : ""); +} + +/* Build sorted list of windows according to user rules - shared by all functions */ +static GSList *build_sorted_window_list(void) +{ + GSList *w, *sort_list = NULL; + + /* Create sorted list of all windows according to user rules */ + for (w = windows; w; w = w->next) { + WINDOW_REC *win = w->data; + WINDOW_SORT_REC *sort_rec = g_new0(WINDOW_SORT_REC, 1); + const char *win_name; + + sort_rec->win = win; + /* Try to find server for this window */ + if (win->active && win->active->server) { + sort_rec->server = win->active->server; + } else { + /* For server status windows, try to find server by servertag */ + sort_rec->server = win->servertag ? server_find_tag(win->servertag) : NULL; + } + + win_name = window_get_active_name(win); + + /* Determine sort group and key according to user rules */ + if (win_name && g_ascii_strcasecmp(win_name, "Notices") == 0) { + /* 1. Notices window - always first */ + sort_rec->sort_group = 0; + sort_rec->sort_key = g_strdup("Notices"); + } else if (sort_rec->server && !win->active) { + /* 2. Server status windows - no active channel/query */ + const char *net; + sort_rec->sort_group = 1; + net = sort_rec->server->connrec && sort_rec->server->connrec->chatnet ? + sort_rec->server->connrec->chatnet : + (sort_rec->server->connrec ? sort_rec->server->connrec->address : "server"); + sort_rec->sort_key = g_strdup(net ? net : "server"); + } else if (win->active && win->active->server) { + /* Windows with active channel/query items */ + WI_ITEM_REC *item = win->active; + sort_rec->server = item->server; /* Use server from active item */ + if (IS_CHANNEL(item)) { + /* 3. Channels - alphabetically within server */ + sort_rec->sort_group = 2; + sort_rec->sort_key = g_strdup(item->name ? item->name : "channel"); + } else if (IS_QUERY(item)) { + /* 4. Queries - alphabetically within server */ + sort_rec->sort_group = 3; + sort_rec->sort_key = g_strdup(item->name ? item->name : "query"); + } else { + /* Other server-related items */ + sort_rec->sort_group = 2; + sort_rec->sort_key = g_strdup(item->name ? item->name : "item"); + } + } else if (win_name && win_name[0] != '\0') { + /* 5. Named orphan windows (not connected to any server) */ + sort_rec->sort_group = 4; + sort_rec->sort_key = g_strdup(win_name); + } else { + /* 6. Unnamed orphan windows */ + sort_rec->sort_group = 5; + sort_rec->sort_key = g_strdup_printf("window_%d", win->refnum); + } + + sort_list = g_slist_append(sort_list, sort_rec); + } + + /* Sort according to our rules */ + sort_list = g_slist_sort(sort_list, compare_window_sort_items); + + return sort_list; +} + +/* Free sorted window list */ +static void free_sorted_window_list(GSList *sort_list) +{ + GSList *s; + for (s = sort_list; s; s = s->next) { + WINDOW_SORT_REC *sort_rec = s->data; + g_free(sort_rec->sort_key); + g_free(sort_rec); + } + g_slist_free(sort_list); +} + +static void renumber_windows_by_position(void) +{ + GSList *sort_list, *s; + int position = 1; + + /* Get sorted list using shared function */ + sort_list = build_sorted_window_list(); + + /* Renumber all windows according to sorted order */ + for (s = sort_list; s; s = s->next) { + WINDOW_SORT_REC *sort_rec = s->data; + WINDOW_REC *win = sort_rec->win; + + if (win->refnum != position) { + window_set_refnum(win, position); + } + position++; + } + + /* Clean up */ + free_sorted_window_list(sort_list); +} + + +static gint ci_nick_compare(gconstpointer a, gconstpointer b) +{ + NICK_REC *n1 = (NICK_REC*)a; + NICK_REC *n2 = (NICK_REC*)b; + if (!n1 || !n1->nick) return 1; + if (!n2 || !n2->nick) return -1; + return g_ascii_strcasecmp(n1->nick, n2->nick); +} + +static void draw_left_contents(MAIN_WINDOW_REC *mw, SP_MAINWIN_CTX *ctx) +{ + TERM_WINDOW *tw; + int row; + int skip; + int height; + GSList *sort_list, *s; + int list_index; + + if (!ctx) return; + tw = ctx->left_tw; + if (!tw) return; + clear_window_full(tw, ctx->left_w, ctx->left_h); + + row = 0; + skip = ctx->left_scroll_offset; + height = ctx->left_h; + + /* Get sorted list using shared function */ + sort_list = build_sorted_window_list(); + + /* Draw windows in sorted order */ + list_index = 0; + for (s = sort_list; s && row < height; s = s->next) { + WINDOW_SORT_REC *sort_rec = s->data; + WINDOW_REC *win = sort_rec->win; + const char *display_name = sort_rec->sort_key; + int activity = win->data_level; + int format; + + /* Skip items before our scroll offset */ + if (list_index++ < skip) continue; + + /* Determine format based on selection and activity */ + if (win->refnum - 1 == ctx->left_selected_index) { + format = TXT_SIDEPANEL_ITEM_SELECTED; + } else if (sort_rec->sort_group == 0 || sort_rec->sort_group == 1) { + /* Notices and server status windows use header format unless selected */ + if (activity >= DATA_LEVEL_HILIGHT) { + /* Check if this is a nick mention (has hilight_color indicating nick mention) */ + if (win->hilight_color != NULL) { + format = TXT_SIDEPANEL_ITEM_NICK_MENTION; + } else { + format = TXT_SIDEPANEL_ITEM_HIGHLIGHT; + } + } else if (activity > DATA_LEVEL_NONE) { + format = TXT_SIDEPANEL_ITEM_ACTIVITY; + } else { + format = TXT_SIDEPANEL_HEADER; + } + } else { + /* Channels, queries, and other windows - SIMPLE PRIORITY SYSTEM */ + window_priority_state *state = get_window_priority_state(win); + int current_priority = state->current_priority; + + switch (current_priority) { + case 4: /* PRIORITY 4: Nick mention OR Query messages (magenta) */ + /* Use QUERY_MSG only for query windows, NICK_MENTION for channel mentions */ + if (win->active && IS_QUERY(win->active)) { + format = TXT_SIDEPANEL_ITEM_QUERY_MSG; + } else { + format = TXT_SIDEPANEL_ITEM_NICK_MENTION; + } + break; + case 3: /* PRIORITY 3: Channel activity (yellow) */ + format = TXT_SIDEPANEL_ITEM_ACTIVITY; + break; + case 2: /* PRIORITY 2: Keyword highlights (red) */ + format = TXT_SIDEPANEL_ITEM_HIGHLIGHT; + break; + case 1: /* PRIORITY 1: Events (green) */ + format = TXT_SIDEPANEL_ITEM_EVENTS; + break; + default: /* PRIORITY 0: No activity (normal) */ + format = TXT_SIDEPANEL_ITEM; + break; + } + } + + /* Draw the item */ + term_move(tw, 0, row); + draw_str_themed(tw, 0, row, mw->active, format, display_name ? display_name : "window"); + row++; + } + + /* Clean up */ + free_sorted_window_list(sort_list); + + /* Only draw border if right panel is also visible */ + if (ctx->right_tw && ctx->right_h > 0) { + draw_border_vertical(tw, ctx->left_w, ctx->left_h, 1); + } + irssi_set_dirty(); +} + +static void draw_right_contents(MAIN_WINDOW_REC *mw, SP_MAINWIN_CTX *ctx) +{ + TERM_WINDOW *tw; + WINDOW_REC *aw; + int height; + int skip; + int index; + int row; + GSList *nt; + if (!ctx) return; + tw = ctx->right_tw; + if (!tw) return; + clear_window_full(tw, ctx->right_w, ctx->right_h); + aw = mw->active; + height = ctx->right_h; + skip = ctx->right_scroll_offset; + index = 0; row = 0; + if (ctx->right_order) { g_slist_free(ctx->right_order); ctx->right_order = NULL; } + + /* If no channel active (no # in name), just draw border and return */ + if (!aw || !aw->active || !aw->active->visible_name || !strchr(aw->active->visible_name, '#')) { + draw_border_vertical(tw, ctx->right_w, ctx->right_h, 0); + irssi_set_dirty(); + return; + } + { + CHANNEL_REC *ch = CHANNEL(aw->active); + GSList *nicks = nicklist_getnicks(ch); + GSList *ops = NULL, *voices = NULL, *normal = NULL; + GSList *cur; + /* Split nicks by status */ + for (nt = nicks; nt; nt = nt->next) { + NICK_REC *nick = nt->data; + if (!nick || !nick->nick) continue; + if (nick->op) ops = g_slist_prepend(ops, nick); + else if (nick->voice) voices = g_slist_prepend(voices, nick); + else normal = g_slist_prepend(normal, nick); + } + /* Sort each group alphabetically */ + ops = g_slist_sort(ops, ci_nick_compare); + voices = g_slist_sort(voices, ci_nick_compare); + normal = g_slist_sort(normal, ci_nick_compare); + /* Build ordered list and render */ + for (cur = ops; cur && row < height; cur = cur->next) { + NICK_REC *nick = cur->data; + int format; + ctx->right_order = g_slist_append(ctx->right_order, nick); + if (index++ < skip) continue; + term_move(tw, 1, row); + format = TXT_SIDEPANEL_NICK_OP_STATUS; + draw_str_themed_2params(tw, 1, row, mw->active, format, "@", nick->nick); + row++; + } + for (cur = voices; cur && row < height; cur = cur->next) { + NICK_REC *nick = cur->data; + int format; + ctx->right_order = g_slist_append(ctx->right_order, nick); + if (index++ < skip) continue; + term_move(tw, 1, row); + format = TXT_SIDEPANEL_NICK_VOICE_STATUS; + draw_str_themed_2params(tw, 1, row, mw->active, format, "+", nick->nick); + row++; + } + for (cur = normal; cur && row < height; cur = cur->next) { + NICK_REC *nick = cur->data; + int format; + ctx->right_order = g_slist_append(ctx->right_order, nick); + if (index++ < skip) continue; + term_move(tw, 1, row); + format = TXT_SIDEPANEL_NICK_NORMAL_STATUS; + draw_str_themed_2params(tw, 1, row, mw->active, format, "", nick->nick); + row++; + } + g_slist_free(ops); + g_slist_free(voices); + g_slist_free(normal); + } + draw_border_vertical(tw, ctx->right_w, ctx->right_h, 0); + irssi_set_dirty(); +} + +static void redraw_one(MAIN_WINDOW_REC *mw) +{ + SP_MAINWIN_CTX *ctx = get_ctx(mw, FALSE); + if (!ctx) return; + position_tw(mw, ctx); + draw_left_contents(mw, ctx); + /* Only draw right contents if right panel is actually shown */ + if (ctx->right_tw && ctx->right_h > 0) { + draw_right_contents(mw, ctx); + } + draw_main_window_borders(mw); + irssi_set_dirty(); + term_refresh(NULL); +} + +static void redraw_all(void) +{ + GSList *t; + for (t = mainwindows; t; t = t->next) redraw_one(t->data); +} + +static void sig_mainwindow_resized(MAIN_WINDOW_REC *mw) +{ + redraw_one(mw); +} + +static void sig_window_changed(WINDOW_REC *w) +{ + /* Reset priority when user opens/switches to window */ + if (w) { + WI_ITEM_REC *item = w ? w->active : NULL; + const char *item_name = item ? item->visible_name : "NULL"; + /* Debug: Window switching - tracks user navigation */ + sp_logf("SIGNAL: window_changed from %d to %d '%s' (USER SWITCHED)", + active_win ? active_win->refnum : -1, w->refnum, item_name); + reset_window_priority(w); + } + update_left_selection_to_active(); + redraw_all(); +} + +static void sig_window_item_changed(WINDOW_REC *w, WI_ITEM_REC *item) +{ + const char *item_name = item ? item->visible_name : "NULL"; + const char *item_type = item ? (IS_QUERY(item) ? "QUERY" : IS_CHANNEL(item) ? "CHANNEL" : "OTHER") : "NONE"; + + /* Debug: Window item changes - tracks channel/query switching */ + sp_logf("SIGNAL: window_item_changed Window %d item '%s' (%s) active_win=%d", + w ? w->refnum : -1, item_name, item_type, active_win ? active_win->refnum : -1); + + update_left_selection_to_active(); + redraw_all(); +} + +static void sig_window_created(WINDOW_REC *window) +{ + (void)window; + renumber_windows_by_position(); + redraw_all(); +} + +static void sig_window_destroyed(WINDOW_REC *window) +{ + (void)window; + renumber_windows_by_position(); + redraw_all(); +} + +static void sig_window_item_new(WINDOW_REC *window, WI_ITEM_REC *item) +{ + (void)window; + (void)item; + renumber_windows_by_position(); + redraw_all(); +} + +static void sig_query_created(QUERY_REC *query, int automatic) +{ + (void)query; + (void)automatic; + renumber_windows_by_position(); + redraw_all(); +} + +static void sig_channel_joined(CHANNEL_REC *channel) +{ + (void)channel; + renumber_windows_by_position(); + redraw_all(); +} + +static void sig_nicklist_changed(CHANNEL_REC *channel, NICK_REC *nick, const char *old_nick) +{ + /* Nicklist changes don't trigger activity - just redraw */ + (void)channel; (void)nick; (void)old_nick; + redraw_all(); +} + +static void sig_nicklist_new(CHANNEL_REC *channel, NICK_REC *nick) +{ + /* Nicklist changes don't trigger activity - just redraw */ + (void)channel; (void)nick; + redraw_all(); +} + +/* Print text signal handler - get accurate data levels */ +static void sig_print_text(TEXT_DEST_REC *dest, const char *msg) +{ + WINDOW_REC *window; + WI_ITEM_REC *item; + int data_level; + const char *item_name, *item_type; + + if (!dest || !dest->window) return; + + window = dest->window; + + /* Skip if this is the active window */ + if (window == active_win) return; + + /* Filter out message levels that are handled by dedicated signals to avoid duplicates */ + if (dest->level & (MSGLEVEL_JOINS | MSGLEVEL_PARTS | MSGLEVEL_QUITS | MSGLEVEL_KICKS | + MSGLEVEL_MODES | MSGLEVEL_TOPICS | MSGLEVEL_NICKS | MSGLEVEL_ACTIONS)) { + /* These are handled by message_join/part/quit/nick signals - skip to avoid duplicates */ + return; + } + + /* Determine actual data level based on message level - same logic as window-activity.c */ + if (dest->level & MSGLEVEL_HILIGHT) { + data_level = DATA_LEVEL_HILIGHT; + } else if (dest->level & (MSGLEVEL_MSGS | MSGLEVEL_NOTICES)) { + /* Check if this is a query or channel */ + item = window->active; + if (item && IS_QUERY(item)) { + data_level = DATA_LEVEL_MSG; /* Query messages */ + } else { + data_level = DATA_LEVEL_TEXT; /* Channel messages */ + } + } else if (dest->level & MSGLEVEL_PUBLIC) { + data_level = DATA_LEVEL_TEXT; /* Public channel text */ + } else { + data_level = DATA_LEVEL_TEXT; /* Default to text level */ + } + + /* Debug logging */ + item = window->active; + item_name = item ? item->visible_name : "NULL"; + item_type = item ? (IS_QUERY(item) ? "QUERY" : IS_CHANNEL(item) ? "CHANNEL" : "OTHER") : "NONE"; + + /* Debug: Text messages - essential for activity classification debugging */ + sp_logf("SIGNAL: print_text Window %d '%s' (%s) msg_level=0x%x data_level=%d active_win=%d", + window->refnum, item_name, item_type, dest->level, data_level, + active_win ? active_win->refnum : -1); + + handle_new_activity(window, data_level); + redraw_all(); +} + + + +/* Event signal handlers for join/part/quit/nick - priority 1 events */ +static void sig_message_join(SERVER_REC *server, const char *channel, const char *nick, const char *address, const char *account, const char *realname) +{ + WINDOW_REC *window = window_find_item(server, channel); + if (window) { + /* Debug: Join events - tracks channel join activity */ + sp_logf("SIGNAL: message_join %s on %s (Window %d) active_win=%d", + nick, channel, window->refnum, active_win ? active_win->refnum : -1); + handle_new_activity(window, DATA_LEVEL_EVENT); + } + redraw_all(); +} + +static void sig_message_part(SERVER_REC *server, const char *channel, const char *nick, const char *address, const char *reason) +{ + WINDOW_REC *window = window_find_item(server, channel); + if (window) { + /* Debug: Part events - tracks channel part activity */ + sp_logf("SIGNAL: message_part %s from %s (Window %d) active_win=%d", + nick, channel, window->refnum, active_win ? active_win->refnum : -1); + handle_new_activity(window, DATA_LEVEL_EVENT); + } + redraw_all(); +} + +static void sig_message_quit(SERVER_REC *server, const char *nick, const char *address, const char *reason) +{ + /* Handle quit for all windows where this nick was present */ + GSList *tmp; + for (tmp = windows; tmp != NULL; tmp = tmp->next) { + WINDOW_REC *window = tmp->data; + if (window->active && window->active->server == server) { + handle_new_activity(window, DATA_LEVEL_EVENT); + } + } + redraw_all(); +} + +static void sig_message_nick(SERVER_REC *server, const char *newnick, const char *oldnick, const char *address) +{ + /* Handle nick change for all windows on this server */ + GSList *tmp; + for (tmp = windows; tmp != NULL; tmp = tmp->next) { + WINDOW_REC *window = tmp->data; + if (window->active && window->active->server == server) { + handle_new_activity(window, DATA_LEVEL_EVENT); + } + } + redraw_all(); +} + + + + +static void setup_ctx_for(MAIN_WINDOW_REC *mw) +{ + SP_MAINWIN_CTX *ctx; + ctx = get_ctx(mw, TRUE); + ctx->left_w = (sp_enable_left ? sp_left_width : 0); + ctx->right_w = (sp_enable_right ? sp_right_width : 0); + position_tw(mw, ctx); +} + +static void update_left_selection_to_active(void) +{ + GSList *tmp; + + for (tmp = mainwindows; tmp; tmp = tmp->next) { + MAIN_WINDOW_REC *mw = tmp->data; + SP_MAINWIN_CTX *ctx = get_ctx(mw, FALSE); + WINDOW_REC *aw = mw->active; + + if (!ctx || !aw) continue; + + /* Simple: selection index = active window refnum - 1 (0-based indexing) */ + ctx->left_selected_index = aw->refnum - 1; + } +} + +static void apply_and_redraw(void) +{ + GSList *tmp; + for (tmp = mainwindows; tmp; tmp = tmp->next) { + MAIN_WINDOW_REC *mw = tmp->data; + setup_ctx_for(mw); + } + redraw_all(); +} + +/* Simple mouse parser state for SGR (1006) mode: ESC [ < btn ; x ; y M/m */ +static gboolean mouse_tracking_enabled = FALSE; +static int mouse_state = 0; /* 0 idle, >0 reading sequence */ +static char mouse_buf[64]; +static int mouse_len = 0; + +static gboolean handle_click_at(int x, int y, int button) +{ + GSList *mt; + /* Debug: Mouse clicks - useful for mouse interaction debugging */ + sp_logf("MOUSE_CLICK: at x=%d y=%d button=%d", x, y, button); + for (mt = mainwindows; mt; mt = mt->next) { + MAIN_WINDOW_REC *mw = mt->data; + SP_MAINWIN_CTX *ctx = get_ctx(mw, FALSE); + if (!ctx) continue; + if (ctx->left_tw) { + int px = ctx->left_x, py = ctx->left_y, pw = ctx->left_w, ph = ctx->left_h; + if (x >= px && x < px+pw && y >= py && y < py+ph) { + int row = y - py; + int target_index = row + ctx->left_scroll_offset; + int idx = 0; + GSList *sort_list, *s; + + /* Get sorted list using shared function */ + sort_list = build_sorted_window_list(); + + /* Find the window at target_index */ + for (s = sort_list; s; s = s->next) { + if (idx++ == target_index) { + WINDOW_SORT_REC *sort_rec = s->data; + WINDOW_REC *win = sort_rec->win; + + /* Debug: Window switches via mouse - tracks click navigation */ + sp_logf("MOUSE_CLICK: switching to window %d (was %d)", + win ? win->refnum : -1, active_win ? active_win->refnum : -1); + ctx->left_selected_index = target_index; + if (win) window_set_active(win); + redraw_one(mw); + irssi_set_dirty(); + free_sorted_window_list(sort_list); + return TRUE; + } + } + + free_sorted_window_list(sort_list); + } + } + if (ctx->right_tw) { + int px = ctx->right_x, py = ctx->right_y, pw = ctx->right_w, ph = ctx->right_h; + if (x >= px && x < px+pw && y >= py && y < py+ph) { + int row = y - py; + WINDOW_REC *aw = mw->active; + if (aw && IS_CHANNEL(aw->active)) { + CHANNEL_REC *ch = CHANNEL(aw->active); + int target_index = ctx->right_scroll_offset + row; + int count = g_slist_length(ctx->right_order); + if (target_index >= 0 && target_index < count) { + NICK_REC *nick = g_slist_nth_data(ctx->right_order, target_index); + ctx->right_selected_index = target_index; + if (nick && nick->nick) + signal_emit("command query", 3, nick->nick, ch->server, ch); + redraw_one(mw); + irssi_set_dirty(); + return TRUE; + } + } + } + } + } + return FALSE; +} + +gboolean sidepanels_try_parse_mouse_key(unichar key) +{ + char *s; + char *sc1; + char *sc2; + char *end; + char last; + int braw; + int x; + int y; + gboolean press; + GSList *mt; + + if (!sp_enable_mouse) return FALSE; + if (reemit_guard) return FALSE; /* Don't process re-emitted keys */ + if (mouse_state == 0) { + if (key == 0x1b) { + mouse_state = 1; + mouse_len = 0; + esc_pending = 1; + /* Start timeout to distinguish fast mouse ESC from user ESC+key */ + if (esc_timeout_tag != -1) g_source_remove(esc_timeout_tag); + esc_timeout_tag = g_timeout_add(50, esc_timeout_callback, NULL); + return TRUE; + } + return FALSE; + } else if (mouse_state == 1) { + if (key == '[') { + /* Cancel timeout - might be mouse sequence or arrow keys */ + if (esc_timeout_tag != -1) { g_source_remove(esc_timeout_tag); esc_timeout_tag = -1; } + mouse_state = 2; + esc_pending = 0; /* clear pending since we'll handle this */ + return TRUE; + } + if (key == 'O') { + /* This is ESC O - application mode arrow keys, re-emit immediately */ + if (esc_timeout_tag != -1) { g_source_remove(esc_timeout_tag); esc_timeout_tag = -1; } + mouse_state = 3; /* special state for ESC O sequences */ + esc_pending = 0; /* clear pending since we'll handle this immediately */ + return TRUE; + } + /* Not SGR - cancel timeout and re-emit ESC */ + if (esc_timeout_tag != -1) { g_source_remove(esc_timeout_tag); esc_timeout_tag = -1; } + mouse_state = 0; mouse_len = 0; + if (esc_pending && !reemit_guard) { reemit_guard = 1; signal_emit("gui key pressed", 1, GINT_TO_POINTER(0x1b)); reemit_guard = 0; esc_pending = 0; } + return FALSE; + } else if (mouse_state == 3) { + /* ESC O sequence - re-emit ESC O and current key */ + mouse_state = 0; mouse_len = 0; esc_pending = 0; + /* Cancel any pending timeout */ + if (esc_timeout_tag != -1) { g_source_remove(esc_timeout_tag); esc_timeout_tag = -1; } + if (!reemit_guard) { + reemit_guard = 1; + signal_emit("gui key pressed", 1, GINT_TO_POINTER(0x1b)); + signal_emit("gui key pressed", 1, GINT_TO_POINTER('O')); + signal_emit("gui key pressed", 1, GINT_TO_POINTER(key)); + reemit_guard = 0; + } + return TRUE; + } else if (mouse_state >= 2) { + if (mouse_len < (int)sizeof(mouse_buf)-1) mouse_buf[mouse_len++] = (char)key; + mouse_buf[mouse_len] = '\0'; + s = mouse_buf; + /* Check if this is arrow keys (A/B/C/D) or other ESC sequences */ + if (mouse_len == 1 && (key == 'A' || key == 'B' || key == 'C' || key == 'D' || + key == 'H' || key == 'F' || key == '1' || key == '2' || + key == '3' || key == '4' || key == '5' || key == '6')) { + /* This is arrow key or function key - re-emit ESC[ and current key */ + mouse_state = 0; mouse_len = 0; esc_pending = 0; + if (esc_timeout_tag != -1) { g_source_remove(esc_timeout_tag); esc_timeout_tag = -1; } + if (!reemit_guard) { + reemit_guard = 1; + signal_emit("gui key pressed", 1, GINT_TO_POINTER(0x1b)); + signal_emit("gui key pressed", 1, GINT_TO_POINTER('[')); + signal_emit("gui key pressed", 1, GINT_TO_POINTER(key)); + reemit_guard = 0; + } + return TRUE; + } + if (*s != '<') { /* not SGR mouse - cancel */ + mouse_state = 0; mouse_len = 0; esc_pending = 0; + if (esc_timeout_tag != -1) { g_source_remove(esc_timeout_tag); esc_timeout_tag = -1; } + return TRUE; + } + sc1 = strchr(s, ';'); if (!sc1) return TRUE; + sc2 = strchr(sc1+1, ';'); if (!sc2) return TRUE; + end = sc2+1; if (*end == '\0') return TRUE; + last = end[(int)strlen(end)-1]; + if (last != 'M' && last != 'm') return TRUE; + braw = atoi(s+1); + x = atoi(sc1+1); + y = atoi(sc2+1); + x -= 1; y -= 1; + press = (last == 'M'); + mouse_state = 0; mouse_len = 0; esc_pending = 0; + /* Cancel timeout if still active */ + if (esc_timeout_tag != -1) { g_source_remove(esc_timeout_tag); esc_timeout_tag = -1; } + if ((braw & 64) && press) { + int dir; + int delta; + int px, py, pw, ph; + dir = braw - 64; + delta = (dir == 0 ? -3 : 3); + for (mt = mainwindows; mt; mt = mt->next) { + MAIN_WINDOW_REC *mw = mt->data; + SP_MAINWIN_CTX *ctx = get_ctx(mw, FALSE); + if (!ctx) continue; + if (ctx->left_tw) { + px = ctx->left_x; py = ctx->left_y; pw = ctx->left_w; ph = ctx->left_h; + if (x >= px && x < px+pw && y >= py && y < py+ph) { + ctx->left_scroll_offset = MAX(0, ctx->left_scroll_offset + delta); + redraw_one(mw); + irssi_set_dirty(); + return TRUE; + } + } + if (ctx->right_tw) { + px = ctx->right_x; py = ctx->right_y; pw = ctx->right_w; ph = ctx->right_h; + if (x >= px && x < px+pw && y >= py && y < py+ph) { + ctx->right_scroll_offset = MAX(0, ctx->right_scroll_offset + delta); + redraw_one(mw); + irssi_set_dirty(); + return TRUE; + } + } + } + return TRUE; + } + { + int button; + button = (braw & 3) + 1; + if (press && button == 1) { + /* consume click always */ + (void)handle_click_at(x, y, button); + return TRUE; + } + } + return TRUE; + } + return FALSE; +} + +static void enable_mouse_tracking(void) +{ + fputs("\x1b[?1000h", stdout); + fputs("\x1b[?1006h", stdout); + fflush(stdout); + mouse_tracking_enabled = TRUE; +} + +static void disable_mouse_tracking(void) +{ + fputs("\x1b[?1006l", stdout); + fputs("\x1b[?1000l", stdout); + fflush(stdout); + mouse_tracking_enabled = FALSE; +} + +static void sig_irssi_init_finished(void) +{ + /* Force debug on to see what's happening */ + sp_debug = 1; + /* Renumber windows to ensure proper order */ + renumber_windows_by_position(); + apply_reservations_all(); + apply_and_redraw(); +} + +void sidepanels_init(void) +{ + settings_add_bool("lookandfeel", "sidepanel_left", TRUE); + settings_add_bool("lookandfeel", "sidepanel_right", TRUE); + settings_add_int("lookandfeel", "sidepanel_left_width", 18); + settings_add_int("lookandfeel", "sidepanel_right_width", 18); + settings_add_bool("lookandfeel", "sidepanel_right_auto_hide", TRUE); + + settings_add_bool("lookandfeel", "sidepanel_debug", FALSE); + sp_enable_mouse = TRUE; /* force native */ + read_settings(); + mw_to_ctx = g_hash_table_new(g_direct_hash, g_direct_equal); + /* Apply reservations but don't redraw yet - wait for irssi init finished */ + apply_reservations_all(); + enable_mouse_tracking(); + esc_pending = 0; reemit_guard = 0; + signal_add("irssi init finished", (SIGNAL_FUNC) sig_irssi_init_finished); + signal_add("mainwindow created", (SIGNAL_FUNC) sig_mainwindow_created); + signal_add("setup changed", (SIGNAL_FUNC) read_settings); + signal_add("mainwindow resized", (SIGNAL_FUNC) sig_mainwindow_resized); + signal_add("window changed", (SIGNAL_FUNC) sig_window_changed); + signal_add("window item changed", (SIGNAL_FUNC) sig_window_item_changed); + signal_add("window created", (SIGNAL_FUNC) sig_window_created); + signal_add("window destroyed", (SIGNAL_FUNC) sig_window_destroyed); + signal_add("window item new", (SIGNAL_FUNC) sig_window_item_new); + signal_add("query created", (SIGNAL_FUNC) sig_query_created); + signal_add("channel joined", (SIGNAL_FUNC) sig_channel_joined); + signal_add("nicklist changed", (SIGNAL_FUNC) sig_nicklist_changed); + signal_add("nicklist new", (SIGNAL_FUNC) sig_nicklist_new); + signal_add("nicklist remove", (SIGNAL_FUNC) redraw_all); + signal_add("nicklist gone changed", (SIGNAL_FUNC) redraw_all); + signal_add("nicklist serverop changed", (SIGNAL_FUNC) redraw_all); + signal_add("nicklist host changed", (SIGNAL_FUNC) redraw_all); + signal_add("nicklist account changed", (SIGNAL_FUNC) redraw_all); + /* Message events for live nicklist updates */ + signal_add("message join", (SIGNAL_FUNC) sig_message_join); + signal_add("message part", (SIGNAL_FUNC) sig_message_part); + signal_add("message quit", (SIGNAL_FUNC) sig_message_quit); + signal_add("message kick", (SIGNAL_FUNC) redraw_all); + signal_add("message nick", (SIGNAL_FUNC) sig_message_nick); + signal_add("message own_nick", (SIGNAL_FUNC) redraw_all); + /* Channel state changes */ + signal_add("channel created", (SIGNAL_FUNC) redraw_all); + signal_add("channel destroyed", (SIGNAL_FUNC) redraw_all); + signal_add("channel topic changed", (SIGNAL_FUNC) redraw_all); + signal_add("channel sync", (SIGNAL_FUNC) redraw_all); + signal_add("channel wholist", (SIGNAL_FUNC) redraw_all); + /* Live activity notifications */ + signal_add("print text", (SIGNAL_FUNC) sig_print_text); +} + +void sidepanels_deinit(void) +{ + GSList *tmp; + signal_remove("irssi init finished", (SIGNAL_FUNC) sig_irssi_init_finished); + signal_remove("mainwindow created", (SIGNAL_FUNC) sig_mainwindow_created); + signal_remove("setup changed", (SIGNAL_FUNC) read_settings); + signal_remove("mainwindow resized", (SIGNAL_FUNC) sig_mainwindow_resized); + signal_remove("window changed", (SIGNAL_FUNC) sig_window_changed); + signal_remove("window item changed", (SIGNAL_FUNC) sig_window_item_changed); + signal_remove("window created", (SIGNAL_FUNC) sig_window_created); + signal_remove("window destroyed", (SIGNAL_FUNC) sig_window_destroyed); + signal_remove("window item new", (SIGNAL_FUNC) sig_window_item_new); + signal_remove("query created", (SIGNAL_FUNC) sig_query_created); + signal_remove("channel joined", (SIGNAL_FUNC) sig_channel_joined); + signal_remove("nicklist changed", (SIGNAL_FUNC) sig_nicklist_changed); + signal_remove("nicklist new", (SIGNAL_FUNC) sig_nicklist_new); + signal_remove("nicklist remove", (SIGNAL_FUNC) redraw_all); + signal_remove("nicklist gone changed", (SIGNAL_FUNC) redraw_all); + signal_remove("nicklist serverop changed", (SIGNAL_FUNC) redraw_all); + signal_remove("nicklist host changed", (SIGNAL_FUNC) redraw_all); + signal_remove("nicklist account changed", (SIGNAL_FUNC) redraw_all); + /* Message events for live nicklist updates */ + signal_remove("message join", (SIGNAL_FUNC) sig_message_join); + signal_remove("message part", (SIGNAL_FUNC) sig_message_part); + signal_remove("message quit", (SIGNAL_FUNC) sig_message_quit); + signal_remove("message kick", (SIGNAL_FUNC) redraw_all); + signal_remove("message nick", (SIGNAL_FUNC) sig_message_nick); + signal_remove("message own_nick", (SIGNAL_FUNC) redraw_all); + /* Channel state changes */ + signal_remove("channel created", (SIGNAL_FUNC) redraw_all); + signal_remove("channel destroyed", (SIGNAL_FUNC) redraw_all); + signal_remove("channel topic changed", (SIGNAL_FUNC) redraw_all); + signal_remove("channel sync", (SIGNAL_FUNC) redraw_all); + signal_remove("channel wholist", (SIGNAL_FUNC) redraw_all); + /* Live activity notifications */ + signal_remove("print text", (SIGNAL_FUNC) sig_print_text); + /* Remove reservations */ + for (tmp = mainwindows; tmp != NULL; tmp = tmp->next) { + MAIN_WINDOW_REC *mw = tmp->data; + destroy_ctx(mw); + if (mw->statusbar_columns_left) + mainwindow_set_statusbar_columns(mw, -mw->statusbar_columns_left, 0); + if (mw->statusbar_columns_right) + mainwindow_set_statusbar_columns(mw, 0, -mw->statusbar_columns_right); + } + if (mw_to_ctx) { g_hash_table_destroy(mw_to_ctx); mw_to_ctx = NULL; } + if (window_priorities) { g_hash_table_destroy(window_priorities); window_priorities = NULL; } + disable_mouse_tracking(); + /* Clean up timeout */ + if (esc_timeout_tag != -1) { g_source_remove(esc_timeout_tag); esc_timeout_tag = -1; } + if (sp_log) { fclose(sp_log); sp_log = NULL; } +} \ No newline at end of file diff --git a/src/fe-text/sidepanels.h b/src/fe-text/sidepanels.h new file mode 100644 index 00000000..93481877 --- /dev/null +++ b/src/fe-text/sidepanels.h @@ -0,0 +1,13 @@ +#ifndef IRSSI_FE_TEXT_SIDEPANELS_H +#define IRSSI_FE_TEXT_SIDEPANELS_H + +#include +#include + +void sidepanels_init(void); +void sidepanels_deinit(void); + +/* Feed one key (unichar) from sig_gui_key_pressed; returns TRUE if consumed by mouse parser. */ +gboolean sidepanels_try_parse_mouse_key(unichar key); + +#endif \ No newline at end of file diff --git a/tests/fe-text/meson.build b/tests/fe-text/meson.build index 397bd39f..319708b5 100644 --- a/tests/fe-text/meson.build +++ b/tests/fe-text/meson.build @@ -7,6 +7,7 @@ test_test_paste_join_multiline = executable('test-paste-join-multiline', '../../src/fe-text/term-terminfo.c', '../../src/fe-text/term.c', '../../src/fe-text/terminfo-core.c', + '../../src/fe-text/sidepanels.c', '../../src/fe-text/textbuffer-formats.c', '../../src/fe-text/textbuffer-view.c', '../../src/fe-text/textbuffer.c', @@ -25,6 +26,7 @@ test_test_paste_join_multiline = executable('test-paste-join-multiline', implicit_include_directories : false, dependencies : dep + textui_dep, ) + test('test-paste-join-multiline test', test_test_paste_join_multiline, args : ['--tap'], protocol : 'tap') diff --git a/themes/default.theme b/themes/default.theme index a08caefa..e425ac66 100644 --- a/themes/default.theme +++ b/themes/default.theme @@ -294,3 +294,16 @@ abstracts = { # hilight with specified color, $0 = color, $1 = text sb_act_hilight_color = "$0$1-%n"; }; +formats = { + "fe-text" = { + # Sidepanel formatting - clean default theme with hex colors + sidepanel_header = "%z404040%_%_$0%_%_%N"; + sidepanel_item = "%zCCCCCC$0%N"; + sidepanel_item_selected = "%zFFFFFF%_%_$0%_%_%N"; + sidepanel_item_activity = "%zFFFF00$0%N"; + sidepanel_item_highlight = "%zFF0000%_%_$0%_%_%N"; + sidepanel_nick_op = "%zFFFF00@%N%zCCCCCC$0%N"; + sidepanel_nick_voice = "%z00FFFF+%N%zCCCCCC$0%N"; + sidepanel_nick_normal = "%zCCCCCC$0%N"; + }; +};