/* X-Chat * Copyright (C) 1998 Peter Zelezny. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA */ #include #include #include #include #include #include #include #include #ifdef WIN32 #include #else #include #include #endif #include "hexchat.h" #include "cfgfiles.h" #include "chanopt.h" #include "plugin.h" #include "fe.h" #include "server.h" #include "util.h" #include "outbound.h" #include "hexchatc.h" #include "text.h" #include "typedef.h" #ifdef WIN32 #include #endif #ifdef USE_LIBCANBERRA #include #endif const gchar* unicode_fallback_string = "\357\277\275"; /* The Unicode replacement character 0xFFFD */ const gchar* arbitrary_encoding_fallback_string = "?"; struct pevt_stage1 { int len; char *data; struct pevt_stage1 *next; }; #ifdef USE_LIBCANBERRA static ca_context *ca_con; #endif #define SCROLLBACK_MAX 32000 static void mkdir_p (char *filename); static char *log_create_filename (char *channame); static char * scrollback_get_filename (session *sess) { char *net, *chan, *buf, *ret = NULL; net = server_get_network (sess->server, FALSE); if (!net) return NULL; buf = g_strdup_printf ("%s" G_DIR_SEPARATOR_S "scrollback" G_DIR_SEPARATOR_S "%s" G_DIR_SEPARATOR_S "%s.txt", get_xdir (), net, ""); mkdir_p (buf); g_free (buf); chan = log_create_filename (sess->channel); if (chan[0]) buf = g_strdup_printf ("%s" G_DIR_SEPARATOR_S "scrollback" G_DIR_SEPARATOR_S "%s" G_DIR_SEPARATOR_S "%s.txt", get_xdir (), net, chan); else buf = NULL; g_free (chan); if (buf) { ret = g_filename_from_utf8 (buf, -1, NULL, NULL, NULL); g_free (buf); } return ret; } void scrollback_close (session *sess) { g_clear_object (&sess->scrollfile); } /* shrink the file to roughly prefs.hex_text_max_lines */ static void scrollback_shrink (session *sess) { char *buf, *p; gsize len; gint offset, lines = 0; const gint max_lines = MIN(prefs.hex_text_max_lines, SCROLLBACK_MAX); if (!g_file_load_contents (sess->scrollfile, NULL, &buf, &len, NULL, NULL)) return; /* count all lines */ p = buf; while (p != buf + len) { if (*p == '\n') lines++; p++; } offset = lines - max_lines; /* now just go back to where we want to start the file */ p = buf; lines = 0; while (p != buf + len) { if (*p == '\n') { lines++; if (lines == offset) { p++; break; } } p++; } if (g_file_replace_contents (sess->scrollfile, p, strlen(p), NULL, FALSE, G_FILE_CREATE_PRIVATE, NULL, NULL, NULL)) sess->scrollwritten = lines; g_free (buf); } static void scrollback_save (session *sess, char *text, time_t stamp) { GOutputStream *ostream; char *buf; if (sess->type == SESS_SERVER && prefs.hex_gui_tab_server == 1) return; if (sess->text_scrollback == SET_DEFAULT) { if (!prefs.hex_text_replay) return; } else { if (sess->text_scrollback != SET_ON) return; } if (!sess->scrollfile) { if ((buf = scrollback_get_filename (sess)) == NULL) return; sess->scrollfile = g_file_new_for_path (buf); g_free (buf); } else { /* Users can delete the folder after it's created... */ GFile *parent = g_file_get_parent (sess->scrollfile); g_file_make_directory_with_parents (parent, NULL, NULL); g_object_unref (parent); } ostream = G_OUTPUT_STREAM(g_file_append_to (sess->scrollfile, G_FILE_CREATE_PRIVATE, NULL, NULL)); if (!ostream) return; if (!stamp) stamp = time(0); if (sizeof (stamp) == 4) /* gcc will optimize one of these out */ buf = g_strdup_printf ("T %d ", (int) stamp); else buf = g_strdup_printf ("T %" G_GINT64_FORMAT " ", (gint64)stamp); g_output_stream_write (ostream, buf, strlen (buf), NULL, NULL); g_output_stream_write (ostream, text, strlen (text), NULL, NULL); if (!g_str_has_suffix (text, "\n")) g_output_stream_write (ostream, "\n", 1, NULL, NULL); g_free (buf); g_object_unref (ostream); sess->scrollwritten++; if ((sess->scrollwritten > prefs.hex_text_max_lines && prefs.hex_text_max_lines > 0) || sess->scrollwritten > SCROLLBACK_MAX) scrollback_shrink (sess); } void scrollback_load (session *sess) { GInputStream *stream; GDataInputStream *istream; gchar *buf, *text; gint lines = 0; time_t stamp; if (sess->text_scrollback == SET_DEFAULT) { if (!prefs.hex_text_replay) return; } else { if (sess->text_scrollback != SET_ON) return; } if (!sess->scrollfile) { if ((buf = scrollback_get_filename (sess)) == NULL) return; sess->scrollfile = g_file_new_for_path (buf); g_free (buf); } stream = G_INPUT_STREAM(g_file_read (sess->scrollfile, NULL, NULL)); if (!stream) return; istream = g_data_input_stream_new (stream); /* * This is to avoid any issues moving between windows/unix * but the docs mention an invalid \r without a following \n * can lock up the program... (Our write() always adds \n) */ g_data_input_stream_set_newline_type (istream, G_DATA_STREAM_NEWLINE_TYPE_ANY); g_object_unref (stream); while (1) { GError *err = NULL; gsize n_bytes; buf = g_data_input_stream_read_line_utf8 (istream, &n_bytes, NULL, &err); if (!err && buf) { /* * Some scrollback lines have three blanks after the timestamp and a newline * Some have only one blank and a newline * Some don't even have a timestamp * Some don't have any text at all */ if (buf[0] == 'T') { if (sizeof (time_t) == 4) stamp = g_ascii_strtoull (buf + 2, NULL, 10); else stamp = g_ascii_strtoull (buf + 2, NULL, 10); /* in case time_t is 64 bits */ text = strchr (buf + 3, ' '); if (text && text[1]) { if (prefs.hex_text_stripcolor_replay) { text = strip_color (text + 1, -1, STRIP_COLOR); } fe_print_text (sess, text, stamp, TRUE); if (prefs.hex_text_stripcolor_replay) { g_free (text); } } else { fe_print_text (sess, " ", stamp, TRUE); } } else { if (strlen (buf)) fe_print_text (sess, buf, 0, TRUE); else fe_print_text (sess, " ", 0, TRUE); } lines++; g_free (buf); } else if (err) { /* If its only an encoding error it may be specific to the line */ if (g_error_matches (err, G_CONVERT_ERROR, G_CONVERT_ERROR_ILLEGAL_SEQUENCE)) { g_warning ("Invalid utf8 in scrollback file\n"); g_clear_error (&err); continue; } /* For general errors just give up */ g_clear_error (&err); break; } else /* No new line */ { break; } } g_object_unref (istream); sess->scrollwritten = lines; if (lines) { text = ctime (&stamp); text[24] = 0; /* get rid of the \n */ buf = g_strdup_printf ("\n*\t%s %s\n\n", _("Loaded log from"), text); fe_print_text (sess, buf, 0, TRUE); g_free (buf); /*EMIT_SIGNAL (XP_TE_GENMSG, sess, "*", buf, NULL, NULL, NULL, 0);*/ } } void log_close (session *sess) { char obuf[512]; time_t currenttime; if (sess->logfd != -1) { currenttime = time (NULL); write (sess->logfd, obuf, g_snprintf (obuf, sizeof (obuf) - 1, _("**** ENDING LOGGING AT %s\n"), ctime (¤ttime))); close (sess->logfd); sess->logfd = -1; } } /* * filename should be in utf8 encoding and will be * converted to filesystem encoding automatically. */ static void mkdir_p (char *filename) { char *dirname, *dirname_fs; GError *err = NULL; dirname = g_path_get_dirname (filename); dirname_fs = g_filename_from_utf8 (dirname, -1, NULL, NULL, &err); if (!dirname_fs) { g_warning ("%s", err->message); g_error_free (err); g_free (dirname); return; } g_mkdir_with_parents (dirname_fs, 0700); g_free (dirname); g_free (dirname_fs); } static char * log_create_filename (char *channame) { char *tmp, *ret; int mbl; ret = tmp = g_strdup (channame); while (*tmp) { mbl = g_utf8_skip[((unsigned char *)tmp)[0]]; if (mbl == 1) { #ifndef WIN32 *tmp = rfc_tolower (*tmp); if (*tmp == '/') #else /* win32 can't handle filenames with \|/><:"*? characters */ if (*tmp == '\\' || *tmp == '|' || *tmp == '/' || *tmp == '>' || *tmp == '<' || *tmp == ':' || *tmp == '\"' || *tmp == '*' || *tmp == '?') #endif *tmp = '_'; } tmp += mbl; } return ret; } /* like strcpy, but % turns into %% */ static char * log_escape_strcpy (char *dest, char *src, char *end) { while (*src) { *dest = *src; if (dest + 1 == end) break; dest++; src++; if (*src == '%') { if (dest + 1 == end) break; dest[0] = '%'; dest++; } } dest[0] = 0; return dest - 1; } /* substitutes %c %n %s into buffer */ static void log_insert_vars (char *buf, int bufsize, char *fmt, char *c, char *n, char *s) { char *end = buf + bufsize; while (1) { switch (fmt[0]) { case 0: buf[0] = 0; return; case '%': fmt++; switch (fmt[0]) { case 'c': buf = log_escape_strcpy (buf, c, end); break; case 'n': buf = log_escape_strcpy (buf, n, end); break; case 's': buf = log_escape_strcpy (buf, s, end); break; default: buf[0] = '%'; buf++; buf[0] = fmt[0]; break; } break; default: buf[0] = fmt[0]; } fmt++; buf++; /* doesn't fit? */ if (buf == end) { buf[-1] = 0; return; } } } static char * log_create_pathname (char *servname, char *channame, char *netname) { char fname[384]; char fnametime[384]; time_t now; if (!netname) { netname = g_strdup ("NETWORK"); } else { netname = log_create_filename (netname); } /* first, everything is in UTF-8 */ if (!rfc_casecmp (channame, servname)) { channame = g_strdup ("server"); } else { channame = log_create_filename (channame); } servname = log_create_filename (servname); log_insert_vars (fname, sizeof (fname), prefs.hex_irc_logmask, channame, netname, servname); g_free (channame); g_free (netname); g_free (servname); /* insert time/date */ now = time (NULL); strftime_utf8 (fnametime, sizeof (fnametime), fname, now); /* If one uses log mask variables, such as "%c/...", %c will be empty upon * connecting since there's no channel name yet, so we have to make sure * we won't try to write to the FS root. */ if (g_path_is_absolute (prefs.hex_irc_logmask)) { g_snprintf (fname, sizeof (fname), "%s", fnametime); } else /* relative path */ { g_snprintf (fname, sizeof (fname), "%s" G_DIR_SEPARATOR_S "logs" G_DIR_SEPARATOR_S "%s", get_xdir (), fnametime); } /* create all the subdirectories */ mkdir_p (fname); return g_strdup (fname); } static int log_open_file (char *servname, char *channame, char *netname) { char buf[512]; int fd; char *file; time_t currenttime; file = log_create_pathname (servname, channame, netname); if (!file) return -1; fd = g_open (file, O_CREAT | O_APPEND | O_WRONLY | OFLAGS, 0644); g_free (file); if (fd == -1) return -1; currenttime = time (NULL); write (fd, buf, g_snprintf (buf, sizeof (buf), _("**** BEGIN LOGGING AT %s\n"), ctime (¤ttime))); return fd; } static void log_open (session *sess) { static gboolean log_error = FALSE; log_close (sess); sess->logfd = log_open_file (sess->server->servername, sess->channel, server_get_network (sess->server, FALSE)); if (!log_error && sess->logfd == -1) { char *filename = log_create_pathname (sess->server->servername, sess->channel, server_get_network (sess->server, FALSE)); char *message = g_strdup_printf (_("* Can't open log file(s) for writing. Check the\npermissions on %s"), filename); g_free (filename); fe_message (message, FE_MSG_WAIT | FE_MSG_ERROR); g_free (message); log_error = TRUE; } } void log_open_or_close (session *sess) { if (sess->text_logging == SET_DEFAULT) { if (prefs.hex_irc_logging) log_open (sess); else log_close (sess); } else { if (sess->text_logging) log_open (sess); else log_close (sess); } } int get_stamp_str (char *fmt, time_t tim, char **ret) { char dest[128]; gsize len_locale; gsize len_utf8; /* strftime requires the format string to be in locale encoding. */ fmt = g_locale_from_utf8 (fmt, -1, NULL, NULL, NULL); len_locale = strftime_validated (dest, sizeof (dest), fmt, localtime (&tim)); g_free (fmt); if (len_locale == 0) { return 0; } *ret = g_locale_to_utf8 (dest, len_locale, NULL, &len_utf8, NULL); if (*ret == NULL) { return 0; } return len_utf8; } static void log_write (session *sess, char *text, time_t ts) { char *temp; char *stamp; char *file; int len;
/* HexChat
 * Copyright (C) 1998-2010 Peter Zelezny.
 * Copyright (C) 2009-2013 Berke Viktor.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
 */

#ifndef HEXCHAT_XTEXT_H
#define HEXCHAT_XTEXT_H

#include <gtk/gtk.h>

#define GTK_TYPE_XTEXT              (gtk_xtext_get_type ())
#define GTK_XTEXT(object)           (G_TYPE_CHECK_INSTANCE_CAST ((object), GTK_TYPE_XTEXT, GtkXText))
#define GTK_XTEXT_CLASS(klass)      (G_TYPE_CHECK_CLASS_CAST ((klass), GTK_TYPE_XTEXT, GtkXTextClass))
#define GTK_IS_XTEXT(object)        (G_TYPE_CHECK_INSTANCE_TYPE ((object), GTK_TYPE_XTEXT))
#define GTK_IS_XTEXT_CLASS(klass)   (G_TYPE_CHECK_CLASS_TYPE ((klass), GTK_TYPE_XTEXT))
#define GTK_XTEXT_GET_CLASS(obj)    (G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_XTEXT, GtkXTextClass))

#define ATTR_BOLD			'\002'
#define ATTR_COLOR		'\003'
#define ATTR_BLINK		'\006'
#define ATTR_BEEP			'\007'
#define ATTR_HIDDEN		'\010'
#define ATTR_ITALICS2	'\011'
#define ATTR_RESET		'\017'
#define ATTR_REVERSE		'\026'
#define ATTR_ITALICS		'\035'
#define ATTR_UNDERLINE	'\037'

/* these match palette.h */
#define XTEXT_MIRC_COLS 32
#define XTEXT_COLS 37		/* 32 plus 5 for extra stuff below */
#define XTEXT_MARK_FG 32	/* for marking text */
#define XTEXT_MARK_BG 33
#define XTEXT_FG 34
#define XTEXT_BG 35
#define XTEXT_MARKER 36		/* for marker line */
#define XTEXT_MAX_COLOR 41

typedef struct _GtkXText GtkXText;
typedef struct _GtkXTextClass GtkXTextClass;
typedef struct textentry textentry;

/*
 * offsets_t is used for retaining search information.
 * It is stored in the 'data' member of a GList,
 * as chained from ent->marks.  It saves starting and
 * ending+1 offset of a found occurrence.
 */
typedef union offsets_u {
	struct offsets_s {
		guint16	start;
		guint16	end;
	} o;
	guint32 u;
} offsets_t;

typedef struct {
	GtkXText *xtext;					/* attached to this widget */

	gfloat old_value;					/* last known adj->value */
	textentry *text_first;
	textentry *text_last;
	guint16 grid_offset[256];

	textentry *last_ent_start;	  /* this basically describes the last rendered */
	textentry *last_ent_end;	  /* selection. */
	int last_offset_start;
	int last_offset_end;

	int last_pixel_pos;

	int pagetop_line;
	int pagetop_subline;
	textentry *pagetop_ent;			/* what's at xtext->adj->value */

	int num_lines;
	int indent;						  /* position of separator (pixels) from left */

	textentry *marker_pos;

	int window_width;				/* window size when last rendered. */
	int window_height;

	unsigned int time_stamp:1;
	unsigned int scrollbar_down:1;
	unsigned int needs_recalc:1;
	unsigned int grid_dirty:1;
	unsigned int marker_seen:1;
	unsigned int reset_marker_pos:1;

	GList *search_found;		/* list of textentries where search found strings */
	gchar *search_text;		/* desired text to search for */
	gchar *search_nee;		/* prepared needle to look in haystack for */
	gint search_lnee;		/* its length */
	gtk_xtext_search_flags search_flags;	/* match, bwd, highlight */
	GList *cursearch;			/* GList whose 'data' pts to current textentry */
	GList *curmark;			/* current item in ent->marks */
	offsets_t curdata;		/* current offset info, from *curmark */
	GRegex *search_re;		/* Compiled regular expression */
	textentry *hintsearch;	/* textentry found for last search */
} xtext_buffer;

struct _GtkXText
{
	GtkWidget widget;

	xtext_buffer *buffer;
	xtext_buffer *orig_buffer;
	xtext_buffer *selection_buffer;

	GtkAdjustment *adj;
	GdkPixmap *pixmap;				/* 0 = use palette[19] */
	GdkDrawable *draw_buf;			/* points to ->window */
	GdkCursor *hand_cursor;
	GdkCursor *resize_cursor;

	int pixel_offset;					/* amount of pixels the top line is chopped by */

	int last_win_x;
	int last_win_y;
	int last_win_h;
	int last_win_w;

	GdkGC *bgc;						  /* backing pixmap */
	GdkGC *fgc;						  /* text foreground color */
	GdkGC *light_gc;				  /* sep bar */
	GdkGC *dark_gc;
	GdkGC *thin_gc;
	GdkGC *marker_gc;
	GdkColor palette[XTEXT_COLS];

	gint io_tag;					  /* for delayed refresh events */
	gint add_io_tag;				  /* "" when adding new text */
	gint scroll_tag;				  /* marking-scroll timeout */
	gulong vc_signal_tag;        /* signal handler for "value_changed" adj */

	int select_start_adj;		  /* the adj->value when the selection started */
	int select_start_x;
	int select_start_y;
	int select_end_x;
	int select_end_y;

	int max_lines;

	int col_fore;
	int col_back;

	int depth;						  /* gdk window depth */

	char num[8];					  /* for parsing mirc color */
	int nc;							  /* offset into xtext->num */

	textentry *hilight_ent;
	int hilight_start;
	int hilight_end;

	guint16 fontwidth[128];	  /* each char's width, only the ASCII ones */

	struct pangofont
	{
		PangoFontDescription *font;
		int ascent;
		int descent;
	} *font, pango_font;
	PangoLayout *layout;

	int fontsize;
	int space_width;				  /* width (pixels) of the space " " character */
	int stamp_width;				  /* width of "[88:88:88]" */
	int max_auto_indent;

	unsigned char scratch_buffer[4096];

	int (*urlcheck_function) (GtkWidget * xtext, char *word);

	int jump_out_offset;	/* point at which to stop rendering */
	int jump_in_offset;	/* "" start rendering */

	int ts_x;			/* ts origin for ->bgc GC */
	int ts_y;

	int clip_x;			/* clipping (x directions) */
	int clip_x2;		/* from x to x2 */

	int clip_y;			/* clipping (y directions) */
	int clip_y2;		/* from y to y2 */

	/* current text states */
	unsigned int bold:1;
	unsigned int underline:1;
	unsigned int italics:1;
	unsigned int hidden:1;

	/* text parsing states */
	unsigned int parsing_backcolor:1;
	unsigned int parsing_color:1;
	unsigned int backcolor:1;

	/* various state information */
	unsigned int moving_separator:1;
	unsigned int word_or_line_select:1;
	unsigned int button_down:1;
	unsigned int hilighting:1;
	unsigned int dont_render:1;
	unsigned int dont_render2:1;
	unsigned int cursor_hand:1;
	unsigned int cursor_resize:1;
	unsigned int skip_border_fills:1;
	unsigned int skip_stamp:1;
	unsigned int mark_stamp:1;	/* Cut&Paste with stamps? */
	unsigned int force_stamp:1;	/* force redrawing it */
	unsigned