Skip to content

Instantly share code, notes, and snippets.

@un-def
Created June 10, 2017 13:11
Show Gist options
  • Save un-def/890d1cea9fc0765f253971516d3954ad to your computer and use it in GitHub Desktop.
Save un-def/890d1cea9fc0765f253971516d3954ad to your computer and use it in GitHub Desktop.
Tool to create (and optionally install) Debian packages of i3 with window icons patch
#!/bin/bash -e
# Tool to create (and optionally install) Debian packages of i3 with
# window icons patch.
# Based on i3-gaps-deb by Gerhard A. Dittes
# Copyright (C) 2017 Gerhard A. Dittes
#
# 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 3 of the License, or
# 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, see <http://www.gnu.org/licenses/>.
# Namespace: igd
# Variables
igd_BASEDIR="$(pwd)"
igd_GITDIR="${igd_BASEDIR}/i3"
igd_GITURL="https://github.com/i3/i3/"
igd_TIMESTAMP="$(date +%Y%m%d%H%M%S)"
igd_JESSIE=-1
# Functions
igd_log()
{
local msg="${@}"
tput setaf 3; printf "[igd] " >&2; tput sgr0
tput setaf 7; printf "${msg}\n" >&2; tput sgr0
}
igd_readAndContinue()
{
local msg="${@}"
tput setaf 3; printf "[igd] "; tput sgr0
tput setaf 7; printf "${msg}"; tput sgr0
read -p " [RETURN]"
}
igd_question()
{
local result=1 # false
local question="${1}"
local default="n"
# ensure lowercase
if [ ${#} -eq 2 ] && [ "${2}" = "y" -o "${2}" = "Y" ]; then
default="y"
fi
tput setaf 3; printf "[igd] "; tput sgr0
tput setaf 7; printf "${question}"; tput sgr0
if [ ${default} = "y" ]; then
printf " (Y/n): "
else
printf " (y/N): "
fi
read answer
# ensure lowercase
if [ "${answer}" = "Y" ]; then
answer="y"
elif [ "${answer}" = "N" ]; then
answer="n"
fi
if [ ${default} = "y" ]; then
if [ "${answer}" != "n" ]; then
result=0 # true
fi
else
if [ "${answer}" = "y" ]; then
result=0 # true
fi
fi
return ${result}
}
igd_startupHint()
{
igd_readAndContinue "This tool creates (and optionally installs) Debian packages of i3 with window icons patch..."
igd_readAndContinue "This tool comes without any warranty and in the hope to be useful..."
}
igd_ensureI3ToBeInstalled()
{
igd_log "Checking i3 installation..."
if ! dpkg -s i3 &> /dev/null; then
igd_readAndContinue "This tool assumes that you have \"i3\" already installed. Please do so..."
exit 42
fi
}
igd_checkSourcesList()
{
igd_log "Checking sources.list(s) to contain sources (\"weak\")..."
if ! grep --quiet -r "^ *deb-src.*\(\(debian\)\|\(ubuntu\)\)[/ ].*main" /etc/apt/sources.list*; then
igd_readAndContinue "Please add necessary \"deb-src\" line(s) to your sources.list (and run an update)."
exit 1
fi
}
igd_detectDebianJessie()
{
if grep --quiet ^8 /etc/debian_version; then
igd_log "Debian jessie detected."
igd_JESSIE=0
else
igd_log "Debian version != jessie, ignoring special treatment..."
igd_JESSIE=1
fi
}
igd_isJessie()
{
if [ ${igd_JESSIE} -eq -1 ]; then
igd_detectDebianJessie
fi
return ${igd_JESSIE}
}
igd_ensurePotentialJessieBackportsAvailability()
{
igd_log "Checking sources.list(s) to contain jessie-backports (\"weak\")..."
if ! grep --quiet -r "^ *deb-src.*-backports" /etc/apt/sources.list*; then
igd_readAndContinue "Please add \"jessie-backport sources\" to your sources.list (and run an update)."
igd_log "You can use the following lines to achieve that:"
cat <<EOF
deb http://ftp.debian.org/debian/ jessie-backports main
deb-src http://ftp.debian.org/debian/ jessie-backports main
EOF
exit 3
fi
}
igd_handleJessieBackportsSpecialCase()
{
if igd_isJessie; then
igd_ensurePotentialJessieBackportsAvailability
fi
}
igd_installBuildDeps()
{
igd_readAndContinue "Installing (basic) build dependencies..."
sudo apt-get build-dep i3-wm
igd_readAndContinue "Installing (additional) build dependencies..."
sudo apt-get install \
devscripts dpkg-dev \
dh-autoreconf \
libxcb-xrm-dev \
libxcb-xkb-dev \
libxkbcommon-dev \
libxkbcommon-x11-dev
}
igd_ensureGitToBeInstalled()
{
igd_log "Checking git installation..."
if ! dpkg -s git &> /dev/null; then
if igd_question "This tool assumes that you have \"git\" already installed. Install now?" "y"; then
sudo apt-get install git
else
exit 44
fi
fi
}
igd_clone()
{
if [ ! -d ${igd_GITDIR} ]; then
igd_readAndContinue "Cloning i3 into \"${igd_GITDIR}\"..."
git clone ${igd_GITURL} ${igd_GITDIR}
else
igd_log "Skip cloning as ${igd_GITDIR} already exists."
fi
}
igd_switchToGitDir()
{
igd_log "Entering directory ${igd_GITDIR}..."
cd ${igd_GITDIR}
}
igd_cleanUpGitStuff()
{
igd_log "Cleaning up..."
git reset --hard HEAD
git clean -fdx
}
igd_prepareBranch()
{
local branch="master"
local alternative="next"
if ! igd_question "Use branch \"${branch}\"? (\"${alternative}\" otherwise)" "y"; then
branch=${alternative}
fi
git checkout ${branch}
git pull
}
igd_applyIconPatch()
{
igd_readAndContinue "Applying window-icons.patch..."
git apply "${igd_BASEDIR}/window-icons.patch"
}
igd_updateDebianChangelog()
{
igd_log "Updating Debian changelog..."
DEBEMAIL="[email protected]" DEBFULLNAME="Gerhard A. Dittes" \
debchange --preserve --dist=unstable --local=gerardo+${igd_TIMESTAMP} "New upstream."
}
igd_fixRules()
{
igd_log "Fix rules file..."
cat <<EOF >>debian/rules
override_dh_install:
override_dh_installdocs:
override_dh_installman:
dh_install -O--parallel
EOF
}
igd_buildDebianPackages()
{
igd_readAndContinue "Build Debian packages..."
dpkg-buildpackage -us -uc
}
igd_switchToBaseDir()
{
igd_log "Entering directory ${igd_BASEDIR}..."
cd ${igd_BASEDIR}
}
igd_installDebianPackages()
{
local debs=$(echo i3_*${igd_TIMESTAMP}*deb i3-wm_*${igd_TIMESTAMP}*deb)
igd_log "Result: ${debs}"
if igd_question "Install created Debian packages?" "y"; then
sudo dpkg -i ${debs}
fi
}
igd_ensurePotentialJessieDependencyStuff()
{
igd_readAndContinue "Correcting dependency stuff..."
local f1="${igd_GITDIR}/debian/control"
local f2="${igd_GITDIR}/configure.ac"
sed -i 's/libcairo2-dev (>= 1\.14\.[0-9]*)/libcairo2-dev (>= 1\.14\.0)/g' ${f1}
sed -i 's/cairo >= 1\.14\.[0-9]*/cairo >= 1\.14\.0/g' ${f2}
}
igd_handleJessieDependencySpecialCase()
{
if igd_isJessie; then
igd_ensurePotentialJessieDependencyStuff
fi
}
igd_cleanUp()
{
if igd_question "Remove created files?" "y"; then
rm -v *${igd_TIMESTAMP}*
fi
}
main()
{
igd_startupHint
igd_ensureI3ToBeInstalled
igd_checkSourcesList
igd_detectDebianJessie
igd_handleJessieBackportsSpecialCase
igd_installBuildDeps
igd_ensureGitToBeInstalled
igd_clone
igd_switchToGitDir
igd_cleanUpGitStuff
igd_prepareBranch
igd_applyIconPatch
igd_updateDebianChangelog
igd_fixRules
igd_handleJessieDependencySpecialCase
igd_buildDebianPackages
igd_switchToBaseDir
igd_installDebianPackages
igd_cleanUp
}
main ${@}
diff --git a/include/atoms_rest.xmacro b/include/atoms_rest.xmacro
index d461dc08..f32a7e1e 100644
--- a/include/atoms_rest.xmacro
+++ b/include/atoms_rest.xmacro
@@ -1,6 +1,7 @@
xmacro(_NET_WM_USER_TIME)
xmacro(_NET_STARTUP_ID)
xmacro(_NET_WORKAREA)
+xmacro(_NET_WM_ICON)
xmacro(WM_PROTOCOLS)
xmacro(WM_DELETE_WINDOW)
xmacro(UTF8_STRING)
diff --git a/include/data.h b/include/data.h
index a729b21e..faed55ee 100644
--- a/include/data.h
+++ b/include/data.h
@@ -440,6 +440,11 @@ struct Window {
/* aspect ratio from WM_NORMAL_HINTS (MPlayer uses this for example) */
double aspect_ratio;
+
+ /** Window icon, as array of ARGB pixels */
+ uint32_t* icon;
+ int icon_width;
+ int icon_height;
};
/**
diff --git a/include/libi3.h b/include/libi3.h
index 11ca3127..35174f2d 100644
--- a/include/libi3.h
+++ b/include/libi3.h
@@ -586,6 +586,11 @@ color_t draw_util_hex_to_color(const char *color);
void draw_util_text(i3String *text, surface_t *surface, color_t fg_color, color_t bg_color, int x, int y, int max_width);
/**
+ * Draw the given image using libi3.
+ */
+void draw_util_image(unsigned char *src, int src_width, int src_height, surface_t *surface, int x, int y, int width, int height);
+
+/**
* Draws a filled rectangle.
* This function is a convenience wrapper and takes care of flushing the
* surface as well as restoring the cairo state.
diff --git a/include/window.h b/include/window.h
index 77e3f48f..894ee9fd 100644
--- a/include/window.h
+++ b/include/window.h
@@ -89,3 +89,9 @@ void window_update_hints(i3Window *win, xcb_get_property_reply_t *prop, bool *ur
*
*/
void window_update_motif_hints(i3Window *win, xcb_get_property_reply_t *prop, border_style_t *motif_border_style);
+
+/**
+ * Updates the _NET_WM_ICON
+ *
+ */
+void window_update_icon(i3Window *win, xcb_get_property_reply_t *prop);
diff --git a/libi3/draw_util.c b/libi3/draw_util.c
index e471405b..329b6851 100644
--- a/libi3/draw_util.c
+++ b/libi3/draw_util.c
@@ -135,6 +135,42 @@ void draw_util_text(i3String *text, surface_t *surface, color_t fg_color, color_
cairo_surface_mark_dirty(surface->surface);
}
+
+/**
+ * Draw the given image using libi3.
+ * This function is a convenience wrapper and takes care of flushing the
+ * surface as well as restoring the cairo state.
+ *
+ */
+void draw_util_image(unsigned char *src, int src_width, int src_height, surface_t *surface, int x, int y, int width, int height) {
+ RETURN_UNLESS_SURFACE_INITIALIZED(surface);
+
+ double scale;
+
+ cairo_save(surface->cr);
+
+ cairo_surface_t *image;
+
+ image = cairo_image_surface_create_for_data(
+ src,
+ CAIRO_FORMAT_ARGB32,
+ src_width,
+ src_height,
+ src_width * 4);
+
+ cairo_translate(surface->cr, x, y);
+
+ scale = MIN((double)width / src_width, (double)height / src_height);
+ cairo_scale(surface->cr, scale, scale);
+
+ cairo_set_source_surface(surface->cr, image, 0, 0);
+ cairo_paint(surface->cr);
+
+ cairo_surface_destroy(image);
+
+ cairo_restore(surface->cr);
+}
+
/**
* Draws a filled rectangle.
* This function is a convenience wrapper and takes care of flushing the
diff --git a/src/handlers.c b/src/handlers.c
index 7dfacef7..37bd70cf 100644
--- a/src/handlers.c
+++ b/src/handlers.c
@@ -1287,6 +1287,19 @@ static bool handle_strut_partial_change(void *data, xcb_connection_t *conn, uint
return true;
}
+static bool handle_windowicon_change(void *data, xcb_connection_t *conn, uint8_t state,
+ xcb_window_t window, xcb_atom_t atom, xcb_get_property_reply_t *prop) {
+ Con *con;
+ if ((con = con_by_window_id(window)) == NULL || con->window == NULL)
+ return false;
+
+ window_update_icon(con->window, prop);
+
+ x_push_changes(croot);
+
+ return true;
+}
+
/* Returns false if the event could not be processed (e.g. the window could not
* be found), true otherwise */
typedef bool (*cb_property_handler_t)(void *data, xcb_connection_t *c, uint8_t state, xcb_window_t window, xcb_atom_t atom, xcb_get_property_reply_t *property);
@@ -1308,7 +1321,8 @@ static struct property_handler_t property_handlers[] = {
{0, 128, handle_class_change},
{0, UINT_MAX, handle_strut_partial_change},
{0, UINT_MAX, handle_window_type},
- {0, 5 * sizeof(uint64_t), handle_motif_hints_change}};
+ {0, 5 * sizeof(uint64_t), handle_motif_hints_change},
+ {0, UINT_MAX, handle_windowicon_change}};
#define NUM_HANDLERS (sizeof(property_handlers) / sizeof(struct property_handler_t))
/*
@@ -1330,6 +1344,7 @@ void property_handlers_init(void) {
property_handlers[8].atom = A__NET_WM_STRUT_PARTIAL;
property_handlers[9].atom = A__NET_WM_WINDOW_TYPE;
property_handlers[10].atom = A__MOTIF_WM_HINTS;
+ property_handlers[11].atom = A__NET_WM_ICON;
}
static void property_notify(uint8_t state, xcb_window_t window, xcb_atom_t atom) {
diff --git a/src/manage.c b/src/manage.c
index 81ee16fd..233333b1 100644
--- a/src/manage.c
+++ b/src/manage.c
@@ -91,6 +91,8 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki
role_cookie, startup_id_cookie, wm_hints_cookie,
wm_normal_hints_cookie, motif_wm_hints_cookie, wm_user_time_cookie, wm_desktop_cookie;
+ xcb_get_property_cookie_t wm_icon_cookie;
+
geomc = xcb_get_geometry(conn, d);
/* Check if the window is mapped (it could be not mapped when intializing and
@@ -162,6 +164,7 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki
motif_wm_hints_cookie = GET_PROPERTY(A__MOTIF_WM_HINTS, 5 * sizeof(uint64_t));
wm_user_time_cookie = GET_PROPERTY(A__NET_WM_USER_TIME, UINT32_MAX);
wm_desktop_cookie = GET_PROPERTY(A__NET_WM_DESKTOP, UINT32_MAX);
+ wm_icon_cookie = GET_PROPERTY(A__NET_WM_ICON, UINT32_MAX);
DLOG("Managing window 0x%08x\n", window);
@@ -177,6 +180,7 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki
window_update_class(cwindow, xcb_get_property_reply(conn, class_cookie, NULL), true);
window_update_name_legacy(cwindow, xcb_get_property_reply(conn, title_cookie, NULL), true);
window_update_name(cwindow, xcb_get_property_reply(conn, utf8_title_cookie, NULL), true);
+ window_update_icon(cwindow, xcb_get_property_reply(conn, wm_icon_cookie, NULL));
window_update_leader(cwindow, xcb_get_property_reply(conn, leader_cookie, NULL));
window_update_transient_for(cwindow, xcb_get_property_reply(conn, transient_cookie, NULL));
window_update_strut_partial(cwindow, xcb_get_property_reply(conn, strut_cookie, NULL));
@@ -185,6 +189,8 @@ void manage_window(xcb_window_t window, xcb_get_window_attributes_cookie_t cooki
window_update_hints(cwindow, xcb_get_property_reply(conn, wm_hints_cookie, NULL), &urgency_hint);
border_style_t motif_border_style = BS_NORMAL;
window_update_motif_hints(cwindow, xcb_get_property_reply(conn, motif_wm_hints_cookie, NULL), &motif_border_style);
+
+
xcb_size_hints_t wm_size_hints;
if (!xcb_icccm_get_wm_size_hints_reply(conn, wm_normal_hints_cookie, &wm_size_hints, NULL))
memset(&wm_size_hints, '\0', sizeof(xcb_size_hints_t));
diff --git a/src/render.c b/src/render.c
index 85548f26..6380f51a 100644
--- a/src/render.c
+++ b/src/render.c
@@ -125,6 +125,10 @@ void render_con(Con *con, bool render_fullscreen) {
/* find the height for the decorations */
params.deco_height = render_deco_height();
+ /* minimum decoration height to allow icon to fit
+ * not actually required, icon would be cropped otherwise */
+ params.deco_height = (params.deco_height < 16) ? 16 : params.deco_height;
+
/* precalculate the sizes to be able to correct rounding errors */
params.sizes = precalculate_sizes(con, &params);
diff --git a/src/window.c b/src/window.c
index db6215d0..5b694593 100644
--- a/src/window.c
+++ b/src/window.c
@@ -17,6 +17,7 @@ void window_free(i3Window *win) {
FREE(win->class_class);
FREE(win->class_instance);
i3string_free(win->name);
+ FREE(win->icon);
FREE(win->ran_assignments);
FREE(win);
}
@@ -365,3 +366,75 @@ void window_update_motif_hints(i3Window *win, xcb_get_property_reply_t *prop, bo
#undef MWM_DECOR_BORDER
#undef MWM_DECOR_TITLE
}
+
+void window_update_icon(i3Window *win, xcb_get_property_reply_t *prop)
+{
+ uint32_t *data = NULL;
+ uint64_t len = 0;
+
+ if (!prop || prop->type != XCB_ATOM_CARDINAL || prop->format != 32) {
+ DLOG("_NET_WM_ICON is not set\n");
+ FREE(prop);
+ return;
+ }
+
+ uint32_t prop_value_len = xcb_get_property_value_length(prop);
+ uint32_t *prop_value = (uint32_t *) xcb_get_property_value(prop);
+
+ /* Find the number of icons in the reply. */
+ while (prop_value_len > (sizeof(uint32_t) * 2) && prop_value &&
+ prop_value[0] && prop_value[1])
+ {
+ /* Check that the property is as long as it should be (in bytes),
+ handling integer overflow. "+2" to handle the width and height
+ fields. */
+ const uint64_t crt_len = prop_value[0] * (uint64_t) prop_value[1];
+ const uint64_t expected_len = (crt_len + 2) * 4;
+
+ if (expected_len > prop_value_len) {
+ break;
+ }
+
+ if (len == 0 || (crt_len >= 16*16 && crt_len < len)) {
+ len = crt_len;
+ data = prop_value;
+ }
+ if (len == 16*16) {
+ break; /* found 16 pixels icon */
+ }
+
+ /* Find pointer to next icon in the reply. */
+ prop_value_len -= expected_len;
+ prop_value = (uint32_t *) (((uint8_t *) prop_value) + expected_len);
+ }
+
+ if (!data) {
+ DLOG("Could not get _NET_WM_ICON\n");
+ FREE(prop);
+ return;
+ }
+
+ LOG("Got _NET_WM_ICON of size: (%d,%d)\n", data[0], data[1]);
+ win->name_x_changed = true; /* trigger a redraw */
+
+ win->icon_width = data[0];
+ win->icon_height = data[1];
+ win->icon = srealloc(win->icon, len * 4);
+
+ for (uint64_t i = 0; i < len; i++) {
+ uint8_t r, g, b, a;
+ a = (data[2 + i] >> 24) & 0xff;
+ r = (data[2 + i] >> 16) & 0xff;
+ g = (data[2 + i] >> 8) & 0xff;
+ b = (data[2 + i] >> 0) & 0xff;
+
+ /* Cairo uses premultiplied alpha */
+ r = (r * a) / 0xff;
+ g = (g * a) / 0xff;
+ b = (b * a) / 0xff;
+
+ win->icon[i] = (a << 24) | (r << 16) | (g << 8) | b;
+ }
+
+ FREE(prop);
+}
diff --git a/src/x.c b/src/x.c
index 8d7d3dd8..4dcd2c60 100644
--- a/src/x.c
+++ b/src/x.c
@@ -538,6 +538,7 @@ void x_draw_decoration(Con *con) {
/* 6: draw the title */
int text_offset_y = (con->deco_rect.height - config.font.height) / 2;
+ int text_offset_x = 0;
struct Window *win = con->window;
if (win == NULL) {
@@ -567,6 +568,9 @@ void x_draw_decoration(Con *con) {
if (win->name == NULL)
goto copy_pixmaps;
+ if (win->icon)
+ text_offset_x = 18;
+
int mark_width = 0;
if (config.show_marks && !TAILQ_EMPTY(&(con->marks_head))) {
char *formatted_mark = sstrdup("");
@@ -602,12 +606,30 @@ void x_draw_decoration(Con *con) {
i3String *title = con->title_format == NULL ? win->name : con_parse_title_format(con);
draw_util_text(title, &(parent->frame_buffer),
p->color->text, p->color->background,
- con->deco_rect.x + logical_px(2),
+ con->deco_rect.x + text_offset_x + logical_px(2),
con->deco_rect.y + text_offset_y,
- con->deco_rect.width - mark_width - 2 * logical_px(2));
+ con->deco_rect.width - text_offset_x - mark_width - 2 * logical_px(2));
if (con->title_format != NULL)
I3STRING_FREE(title);
+ /* Draw the icon */
+ if (win->icon) {
+ uint16_t width = 16;
+ uint16_t height = 16;
+
+ int icon_offset_y = (con->deco_rect.height - height) / 2;
+
+ draw_util_image(
+ (unsigned char *)win->icon,
+ win->icon_width,
+ win->icon_height,
+ &(parent->frame_buffer),
+ con->deco_rect.x + logical_px(2),
+ con->deco_rect.y + icon_offset_y,
+ width,
+ height);
+ }
+
after_title:
x_draw_decoration_after_title(con, p);
copy_pixmaps:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment