Embedding GTK applications via XEmbed - 2
In previous post, we learnt the basics of embedding a GTK application in another application. This works fine when the embedded application supports client server architecture like gVim. Note that in this case the client mode invocations are short lived processes while server process runs throughout the lifetime of the application.
https://gitlab.com/atamariya/emacs/-/blob/dev/src/xterm.h
In contrast, an application like VLC provides a remote control interface to run multiple operations in the same process. In this case, we need to obtain an input channel of the process and write our commands to the same.
embed.c
#include <gtk/gtkx.h>
gchar *string;
GMutex mutex;
void send_hello(GtkButton *btn)
{
/* Note the \n at the end */
string = g_strdup_printf("add /home/anand/Downloads/test.mp4\n");
}
static void
cb_child_watch( GPid pid,
gint status,
gpointer *data )
{
/* Close pid */
g_spawn_close_pid( pid );
}
static gboolean
cb_in_watch( GIOChannel *channel,
GIOCondition cond,
gpointer *data )
{
gsize size;
if( cond == G_IO_HUP )
{
g_io_channel_unref( channel );
return( FALSE );
}
while(gtk_events_pending()) gtk_main_iteration();
g_mutex_lock(&mutex);
if (string) {
g_io_channel_write_chars ( channel, string, -1, &size, NULL );
g_io_channel_flush ( channel, NULL);
g_warning (string);
string = NULL;
/* Channel frees the string when it's done */
/* g_free( string ); */
}
g_mutex_unlock(&mutex);
return( TRUE );
}
static gboolean
cb_out_watch( GIOChannel *channel,
GIOCondition cond,
gpointer *data )
{
gchar *string;
gsize size;
if( cond == G_IO_HUP )
{
g_io_channel_unref( channel );
return( FALSE );
}
while(gtk_events_pending()) gtk_main_iteration();
g_io_channel_read_line( channel, &string, &size, NULL, NULL );
g_warning (string);
g_free( string );
return( TRUE );
}
static gboolean
cb_err_watch( GIOChannel *channel,
GIOCondition cond,
gpointer *data )
{
gchar *string;
gsize size;
while(gtk_events_pending()) gtk_main_iteration();
if( cond == G_IO_HUP )
{
g_io_channel_unref( channel );
return( FALSE );
}
g_io_channel_read_line( channel, &string, &size, NULL, NULL );
g_error (string);
g_free( string );
return( TRUE );
}
void init(GtkWidget *sock)
{
/* Embed application */
Window id = gtk_socket_get_id(GTK_SOCKET(sock));
gchar *command = g_strdup_printf(
"/usr/bin/cvlc -I rc --drawable-xid %d /home/anand/Downloads/1.mp4", id);
GPid pid;
gchar **argv;
/* gchar *argv[] = { "/bin/ls", NULL }; */
gint out, in, err;
GIOChannel *out_ch, *in_ch, *err_ch;
gboolean ret;
gpointer data;
/* Spawn child process */
g_shell_parse_argv(command, NULL, &argv, NULL);
ret = g_spawn_async_with_pipes( NULL, argv, NULL,
G_SPAWN_DO_NOT_REAP_CHILD, NULL,
NULL, &pid, &in, &out, &err, NULL );
if( ! ret )
{
g_error( "SPAWN FAILED" );
return;
}
/* Add watch function to catch termination of the process. This function
* will clean any remnants of process. */
g_child_watch_add( pid, (GChildWatchFunc)cb_child_watch, data );
/* Create channels that will be used to read data from pipes. */
#ifdef G_OS_WIN32
in_ch = g_io_channel_win32_new_fd( in );
out_ch = g_io_channel_win32_new_fd( out );
err_ch = g_io_channel_win32_new_fd( err );
#else
in_ch = g_io_channel_unix_new( in );
out_ch = g_io_channel_unix_new( out );
err_ch = g_io_channel_unix_new( err );
#endif
/* Add watches to channels */
/* g_io_channel_set_encoding ( in_ch, NULL, NULL); */
g_io_add_watch( in_ch, G_IO_OUT| G_IO_HUP, (GIOFunc)cb_in_watch , data );
/* g_io_add_watch( out_ch, G_IO_IN | G_IO_HUP, (GIOFunc)cb_out_watch, data ); */
/* g_io_add_watch( err_ch, G_IO_IN | G_IO_HUP, (GIOFunc)cb_err_watch, data ); */
}
gint main(gint argc, gchar **argv)
{
gtk_init(&argc, &argv);
/* Create window */
GtkWidget *win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
GtkWidget *vbox = gtk_box_new(FALSE, 0);
GtkWidget *sock = gtk_socket_new();
GtkWidget *btn = gtk_button_new_with_label("Hello, World!");
g_signal_connect(sock, "plug-removed", gtk_main_quit, NULL);
g_signal_connect(win, "delete-event", gtk_main_quit, NULL);
g_signal_connect(btn, "clicked", G_CALLBACK(send_hello), NULL);
gtk_widget_set_size_request(sock, 200, 200);
gtk_box_pack_start(GTK_BOX(vbox), sock, TRUE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(vbox), btn, FALSE, TRUE, 0);
gtk_container_add(GTK_CONTAINER(win), vbox);
gtk_widget_show_all(win);
g_thread_new("thread", (gpointer) init, sock);
/* g_idle_add((gpointer)init, sock); */
/* Run */
gtk_main();
return 0;
}
gchar *string;
GMutex mutex;
void send_hello(GtkButton *btn)
{
/* Note the \n at the end */
string = g_strdup_printf("add /home/anand/Downloads/test.mp4\n");
}
static void
cb_child_watch( GPid pid,
gint status,
gpointer *data )
{
/* Close pid */
g_spawn_close_pid( pid );
}
static gboolean
cb_in_watch( GIOChannel *channel,
GIOCondition cond,
gpointer *data )
{
gsize size;
if( cond == G_IO_HUP )
{
g_io_channel_unref( channel );
return( FALSE );
}
while(gtk_events_pending()) gtk_main_iteration();
g_mutex_lock(&mutex);
if (string) {
g_io_channel_write_chars ( channel, string, -1, &size, NULL );
g_io_channel_flush ( channel, NULL);
g_warning (string);
string = NULL;
/* Channel frees the string when it's done */
/* g_free( string ); */
}
g_mutex_unlock(&mutex);
return( TRUE );
}
static gboolean
cb_out_watch( GIOChannel *channel,
GIOCondition cond,
gpointer *data )
{
gchar *string;
gsize size;
if( cond == G_IO_HUP )
{
g_io_channel_unref( channel );
return( FALSE );
}
while(gtk_events_pending()) gtk_main_iteration();
g_io_channel_read_line( channel, &string, &size, NULL, NULL );
g_warning (string);
g_free( string );
return( TRUE );
}
static gboolean
cb_err_watch( GIOChannel *channel,
GIOCondition cond,
gpointer *data )
{
gchar *string;
gsize size;
while(gtk_events_pending()) gtk_main_iteration();
if( cond == G_IO_HUP )
{
g_io_channel_unref( channel );
return( FALSE );
}
g_io_channel_read_line( channel, &string, &size, NULL, NULL );
g_error (string);
g_free( string );
return( TRUE );
}
void init(GtkWidget *sock)
{
/* Embed application */
Window id = gtk_socket_get_id(GTK_SOCKET(sock));
gchar *command = g_strdup_printf(
"/usr/bin/cvlc -I rc --drawable-xid %d /home/anand/Downloads/1.mp4", id);
GPid pid;
gchar **argv;
/* gchar *argv[] = { "/bin/ls", NULL }; */
gint out, in, err;
GIOChannel *out_ch, *in_ch, *err_ch;
gboolean ret;
gpointer data;
/* Spawn child process */
g_shell_parse_argv(command, NULL, &argv, NULL);
ret = g_spawn_async_with_pipes( NULL, argv, NULL,
G_SPAWN_DO_NOT_REAP_CHILD, NULL,
NULL, &pid, &in, &out, &err, NULL );
if( ! ret )
{
g_error( "SPAWN FAILED" );
return;
}
/* Add watch function to catch termination of the process. This function
* will clean any remnants of process. */
g_child_watch_add( pid, (GChildWatchFunc)cb_child_watch, data );
/* Create channels that will be used to read data from pipes. */
#ifdef G_OS_WIN32
in_ch = g_io_channel_win32_new_fd( in );
out_ch = g_io_channel_win32_new_fd( out );
err_ch = g_io_channel_win32_new_fd( err );
#else
in_ch = g_io_channel_unix_new( in );
out_ch = g_io_channel_unix_new( out );
err_ch = g_io_channel_unix_new( err );
#endif
/* Add watches to channels */
/* g_io_channel_set_encoding ( in_ch, NULL, NULL); */
g_io_add_watch( in_ch, G_IO_OUT| G_IO_HUP, (GIOFunc)cb_in_watch , data );
/* g_io_add_watch( out_ch, G_IO_IN | G_IO_HUP, (GIOFunc)cb_out_watch, data ); */
/* g_io_add_watch( err_ch, G_IO_IN | G_IO_HUP, (GIOFunc)cb_err_watch, data ); */
}
gint main(gint argc, gchar **argv)
{
gtk_init(&argc, &argv);
/* Create window */
GtkWidget *win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
GtkWidget *vbox = gtk_box_new(FALSE, 0);
GtkWidget *sock = gtk_socket_new();
GtkWidget *btn = gtk_button_new_with_label("Hello, World!");
g_signal_connect(sock, "plug-removed", gtk_main_quit, NULL);
g_signal_connect(win, "delete-event", gtk_main_quit, NULL);
g_signal_connect(btn, "clicked", G_CALLBACK(send_hello), NULL);
gtk_widget_set_size_request(sock, 200, 200);
gtk_box_pack_start(GTK_BOX(vbox), sock, TRUE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(vbox), btn, FALSE, TRUE, 0);
gtk_container_add(GTK_CONTAINER(win), vbox);
gtk_widget_show_all(win);
g_thread_new("thread", (gpointer) init, sock);
/* g_idle_add((gpointer)init, sock); */
/* Run */
gtk_main();
return 0;
}
Compile the file using following command to generate executable embed.
gcc embed.c $(pkg-config --libs --cflags gtk+-3.0) -o embed
Lessons Learnt
- Use complete path to executable.
- Use g_shell_parse_argv() to generate argv array from command string.
- Terminate the command sent to the external process via channel with a new line (\n).
- Follow the write operation with a g_io_channel_flush().
- If your channel callback is called too frequently, remember to update the UI using while(gtk_events_pending()) gtk_main_iteration();
- Use mutex to avoid race condition - g_mutex_lock(&mutex); & g_mutex_lock(&mutex);
- Use g_thread_new() for one time task and g_idle_add() for periodic idle tasks. If the function returns FALSE, the function added via
g_idle_add() will not be invoked again. - Be watchful of orphan processes generated after running the program.
- Remember to size the window using gtk_window_resize().
- gtk_widget_set_size_request() works only after widget is visible ie. after gtk_widget_show() call.
Multimedia dashboard in GNU Emacs
Using the above approach, you can generate a multimedia dashboard in GNU Emacs using VLC backend.
Create the widget and assign to variable w.
(require 'xwidget)
(insert " ")
(forward-char -1)
(setq w (xwidget-insert (point) 'socket "test" 400 400 '(:init "/usr/bin/cvlc -I rc --drawable-xid %lu")))
(insert " ")
(forward-char -1)
(setq w (xwidget-insert (point) 'socket "test" 400 400 '(:init "/usr/bin/cvlc -I rc --drawable-xid %lu")))
Send command to widget w to play file /home/anand/Downloads/test.mp4 .
(xwidget-socket-command w "add /home/anand/Downloads/test.mp4")
Code:
https://gitlab.com/atamariya/emacs/-/blob/dev/src/xwidget.chttps://gitlab.com/atamariya/emacs/-/blob/dev/src/xterm.h
https://gitlab.com/atamariya/emacs/-/blob/dev/src/xterm.c
https://gitlab.com/atamariya/emacs/-/blob/dev/lisp/xwidget.el
gVim in GNU Emacs
https://gitlab.com/atamariya/emacs/-/blob/dev/lisp/xwidget.el
gVim in GNU Emacs
You can also embed gVim in GNU Emacs (GTK build with X11 backend).
Create the widget and assign to variable w.
xterm in GNU Emacs
Create the widget and assign to variable w.
Surf browser in GNU Emacs
(require 'xwidget)
(insert " ")
(forward-char -1)
(setq w (xwidget-insert (point) 'socket "test" 400 400 '(:init "/usr/bin/gvim --servername emacs --socketid %lu")))
(insert " ")
(forward-char -1)
(setq w (xwidget-insert (point) 'socket "test" 400 400 '(:init "/usr/bin/gvim --servername emacs --socketid %lu")))
xterm in GNU Emacs
You can also embed xterm in GNU Emacs (GTK build with X11 backend).
(require 'xwidget)
(insert " ")
(forward-char -1)
(setq w (xwidget-insert (point) 'socket "test" 400 400 '(:init "/usr/bin/xterm -into %lu")))
(insert " ")
(forward-char -1)
(setq w (xwidget-insert (point) 'socket "test" 400 400 '(:init "/usr/bin/xterm -into %lu")))
Surf browser in GNU Emacs
You can also embed Surf browser in GNU Emacs (GTK build with X11 backend).
Create the widget and assign to variable w. (require 'xwidget)
(insert " ")
(forward-char -1)
(setq w (xwidget-insert (point) 'socket "test" 400 400 '(:init "/usr/local/bin/surf -e %lu https://surf.suckless.org")))
(insert " ")
(forward-char -1)
(setq w (xwidget-insert (point) 'socket "test" 400 400 '(:init "/usr/local/bin/surf -e %lu https://surf.suckless.org")))
Comments
Post a Comment