toreonify's notes

Tray icon for Evolution mail client

When I started working as a "foreign software substitution technician", I quickly discovered that mail client we chose on Linux doesn't have a tray icon. As always with Linux software even trivial tasks like this are not resolved or implemented in the "right way in our philosophy".

By the way, I'm using it in KDE environment, it has a proper system tray.

Step 1 - checking package repository

Nothing came up. But I found a bugreport where it says that "plugin was available in p6 version of distributive and it doesn't work correctly on p8". I am using the latest (at the moment of writing) p10. It definitely won't work with newer Evolution.

There was a link to an old plugin source code, but it didn't work.

Step 2 - search on GitHub

Quick search for "evolution tray" yielded about 9 results. Only one of them was recently updated - October 2021. So, I downloaded it, compiled and it worked!

evolution-systray-original

As you may have suggested after reading source code, Sergei may have wrote this module in a hurry, just to make it work and nothing else. It also contained debug messages that are not helpful in a GUI application.

There were some irritating flaws that I noticed. So, I decided to fix them.

Step 3 - modify existing plugin

Fix window hiding

Application was hidden by the tray icon, but not entirely. You could still select it from Alt-Tab switcher:

evolution-systray-alt-tab

And after you selected the window, it wouldn't appear in taskbar:

evolution-systray-alt-tab-after

This was happening because window was not hidden completly, it was only "hinted" to window manager to not show it in taskbar and pager and to minimize it. I think KDE doesn't respect hints correctly.

gboolean is_active = gtk_window_is_active(window);

g_print("is_active: %d\n", is_active);

if(is_active)
{
  gtk_window_iconify(window);
  gtk_window_set_skip_taskbar_hint(window, TRUE);
  gtk_window_set_skip_pager_hint(window, TRUE);
}
else
{
  gtk_window_present_with_time(window, gdk_x11_get_server_time(gtk_widget_get_window(GTK_WIDGET(window))));
  gtk_window_set_skip_taskbar_hint(window, FALSE);
  gtk_window_set_skip_pager_hint(window, FALSE);
}

New code completly removes window widget and it doesn't appear anywhere:

if (priv->window_active)
{
  gtk_widget_hide((GtkWidget*) priv->window);
}
else
{
  gtk_widget_show((GtkWidget*) priv->window);
  gtk_window_present_with_time(priv->window, gdk_x11_get_server_time(gtk_widget_get_window(GTK_WIDGET(priv->window))));
}

Blurry icon

I've tried to set a new icon by changing it to a vector icon, but it still remained blurry, as if always scaled to 16x16 size bitmap. Maybe it was not compatible with KDE tray implementation. So, I found statusnotifier that uses StatusNotifierItem (in FreeDesktop specification, by the way).

It has GObject bindings, so it easily integrated to a GNOME app module.

I've set the same KDE vector icon and voila! Now it is correctly displayed.

evolution-systray-new

Label translation

Labels for a tooltip were there. That's it. So, I added proper gettext support with labels in English and Russian.

For example, the message counter shows not only the number, but also phrase "unread messages". It accounts for plural forms, as it is easy to implement in .po files.

gchar *num = g_strdup_printf(_DN("%d unread message", "%d unread messages", total_unread_messages), total_unread_messages);

With no unread messages:

evolution-systray-new-popup

With unread messages:

evolution-systray-new-popup-unread

Window state

One thing that almost all apps do right - show minimized window when the tray icon is clicked. Otherwise minimized window already not visible to user would hide away from taskbar. That's confusing, because now you need to click twice to open an app.

This is done by checking window state flags with GDK functions. It differentiates between visible to user and minimized. GTK will tell that window "widget" is visible, because it is. It is not visible to user because of the window manager.

To update internal window state flag, I've used window-state-event signal that triggers when window becomes visible to user or minimizes.

In the subscribed function we are checking for one of three states to define that it is visible/active:

  • window is not withdrawn; exists at all, but maybe minimized
  • window is not minimized; maybe overlapped with other apps
  • window is focused; user is currently working with it

This allows us to change the label in popup menu and set internal flag to select performed action on window when user clicks the tray icon or select "show/hide" in popup menu.

static gboolean
on_window_state_event (GtkWidget *widget, GdkEventWindowState *event, ESystrayPrivate* priv)
{
    GdkWindow *gdk_window = gtk_widget_get_window(GTK_WIDGET(priv->window));
    GdkWindowState state = gdk_window_get_state(gdk_window);
    priv->window_active = !((state & GDK_WINDOW_STATE_WITHDRAWN) || (state & GDK_WINDOW_STATE_ICONIFIED) || !(state & GDK_WINDOW_STATE_FOCUSED));

    if (!priv->window_active)
    {
        gtk_menu_item_set_label(priv->visibility_item, _D("Show"));
    }
    else
    {
        gtk_menu_item_set_label(priv->visibility_item, _D("Minimize"));
    }
}

Unread count included drafts

As I was using the plugin, I noticed that when I saved a draft to send later it was counted as unread message. That's not right.

I've implemented a check that accounts for regular mailboxes and checks a boolean column in a folder model, but also for EWS (Exchange Web Services) mailboxes that uses a folder type flag in a different column.

// is_draft available only for normal accounts to test if it's a Drafts folder
// It's unread count equals to all drafts in folder
gtk_tree_model_get(model, iter, COL_BOOL_IS_DRAFT, &is_draft, -1);
// For EWS acoounts, we can check flags
gtk_tree_model_get(model, iter, COL_UINT_FLAGS, &flags, -1);

if (is_draft || (flags & CAMEL_FOLDER_TYPE_MASK) == CAMEL_FOLDER_TYPE_DRAFTS)
{
    return FALSE;
}

Step 4 - packing to RPM

It's easy. Until you want to package something as good as it can get to an official repository.

In ALT Linux there are lots of tools to help with testing and packaging. No examples or good documentation though. I couldn't get my head around how they managed their Git repositories for three months. It wasn't so complicated at the end, but without an example it was miserable.

Preparation

We'll need to install these packages to build our packages:

apt-get install git gear gear-remotes rpm-utils hasher hasher-priv

Configure hasher

hasher is a chroot on steroids. It was developed to make reproducible builds easier.

To allow our user to use hasher we need to add so-called "sattelite groups":

hasher-useradd <username>

Also, we need to allow /proc mountpoint inside hasher:

# Add to /etc/hasher-priv/fstab
proc    /proc           proc    rw,nosuid,nodev,noexec,gid=proc,hidepid=2 0 0

# Add to /etc/hasher-priv/system
allowed_mountpoints=/proc

In your user directory create two folders, one is for configuration, second is for build artifacts and cache:

mkdir ~/.hasher/
mkdir ~/hasher/

/proc will not be available until we allow it. We will place in in global user options:

# Add to ~/.hasher/config
known_mountpoints=/proc

After that, you need to log out and log back in to apply changes. Sometimes, that doesn't work, but a reboot does.

You can read more at ALT Linux wiki.

Configure Git

Set your username and email address. You won't be able to create commits without them.

git config --global user.name "user"
git config --global user.email user@example.com

Writing spec file

Things we need to sort out for a .spec file:

  • package version
  • runtime requirements
  • build requirements
  • build system

ALT Linux has a list of macros that can be used depending on the build system with all required flags to issue correct build command.

You can get an idea of what .spec must look like from official packages that use the same build system.

So, our .spec will look like this:

Name: evolution-systray
Version: 0.2
Release: alt1

Summary: Tray icon for Evolution
License: LGPL-2.1
Group: Office

Packager: Ivan Korytov <email@email.com>

Requires: evolution
Requires: libstatusnotifier >= 1.0.0-alt1

Source: %name-%version.tar

BuildArch: x86_64
BuildRequires: gcc
BuildRequires: evolution-devel >= 3.44.0-alt1
BuildRequires: libgtk+3-devel >= 3.24.32-alt1
BuildRequires: glib2-devel >= 2.68.4-alt4
BuildRequires: libstatusnotifier-devel >= 1.0.0-alt1

%description
Minimize-to-tray extension for Gnome Evolution email client

%files -f %name.lang
%_libdir/evolution/modules/module-systray.so

%prep
%setup

%build
%make_build

%install
mkdir -p %{?buildroot:%{buildroot}}%{_libdir}/evolution/modules
mkdir -p %{?buildroot:%{buildroot}}%{_datadir}/locale/ru/LC_MESSAGES
%makeinstall
%find_lang %name

%post

%postun

%changelog
* Tue Jan 30 2024 Ivan Korytov <email@email.com> 0.2-alt1
- Updated to v0.2

* Thu Dec 28 2023 Ivan Korytov <email@email.com> 0.1-alt1
- Initial build for ALT Linux

libstatusnotifier also was not available, so here's its .spec file:

Name: libstatusnotifier
Version: 1.0.0
Release: alt1

Summary: GObject Status Notifier Item
License: GPLv3+
Group: System/Libraries

Packager: Ivan Korytov <email@email.com>

Requires: libgtk+3 glib2

Source: %name-%version.tar

BuildArch: x86_64
BuildRequires: gcc
BuildRequires: gtk-doc >= 1.33.2-alt1.1
BuildRequires: libtool >= 2.4.2-alt7
BuildRequires: libgtk+3-devel >= 3.24.32-alt1
BuildRequires: glib2-devel >= 2.68.4-alt4

%description
This little library allows to easily create a GObject to manage
a StatusNotifierItem, handling all the DBus interface and
letting you simply deal with the object's properties and signals.

%package devel
Summary: Headers for %name
Group: Development/C
Requires: %name = %version-%release

%description devel
Headers for building software that uses %name

%files
%_libdir/libstatusnotifier.a
%_libdir/libstatusnotifier.so
%_libdir/libstatusnotifier.so.1
%_libdir/libstatusnotifier.so.1.0.0

%files devel
%_includedir/statusnotifier.h
%_includedir/statusnotifier-compat.h

%_pkgconfigdir/statusnotifier.pc

%prep
%setup

%build
./autogen.sh
%configure
%make_build

%install
%makeinstall_std

%post

%postun

%changelog
* Thu Dec 28 2023 Ivan Korytov <email@email.com> 1.0.0-alt1
- Initial release

Gear

Gear automates one important step - prepares sources for use with rpmbuild. To use it, we need to write rules. Rules will tell Gear from where to take source code and how to compress it.

For this project I wanted to replicate a "hidden source" variant of storing source code. It wasn't well documented as I've even messaged one of the maintainers to hear in exchange "just do this <insert command here>, duh".

First, we need to make a copy of our repository and rename master to sisyphus aka unstable. It will kind of represent a Git repository that distribution is storing on their servers.

git clone <upstream Git repo url>
cd <repo name>
# current branch may be called main or master
git branch -m main sisyphus
git remote remove origin

Then, we wil DELETE all of the files and commit changes. Yes, it is bizzare.

<remove everything, except .git>
git add .
git commit -m "Make empty branch for spec file"

Then, we will start preparing Gear files and settings. First, we will save original URL of a repository. It is needed to pull new versions and tags later as our new remote will be a mirror with drops of .spec files, Gear configuration and patches.

gear-remotes-set-from-url <upstream Git repo url>
gear-remotes-restore

Place a .spec file in root of the repository, create .gear folder. In .gear folder we will need rules file that describes how to package and obtain source code. In this case a tar archive will be created containing all files from a tag named v@version@. @version@ is replaced with a current version set in a .spec file. There are two most popular ways of tagging releases - just a version number or a version number with 'v' prefix.

This archive will contain clean sources without any files that we are creating right now.

# .gear/rules file
tar: v@version@:.  

After that we need to store a tag. Why? - you may ask. Tags are already there in a Git tree! Well, they are, but in official repositories for ALT Linux you will see that original upstream tags are missing. So, we are saving them to a .gear/tags/list file with their commit hash and name. If a tag contains a message, a separate file named with contents Git hash will be saved containing it. .gear/tags/list will contain this tag not with commit hash, but with a message hash that inside contain a commit hash.

gear-store-tags -avc
add_changelog <specfile>
git add .
gear-commit
git tag <version>-<release>

After that, we can build our package. But, wait! How we are gonna build the package if we deleted source code?

Ah, here we are going to use all of the mighty Git powers. Our code still exists, but in a different commit. And you can easily get data from this commit.

git show <release commit hash>

That trick is used by Gear. It extracts commit contents and packs them into an archive. Now we have clean commits with support files for building a package and clean upstream commit history intact.

Updates from upstream are done through git merge --ours. I won't be covering it in this post.

Building

After preparing Gear, we can run gear-hsh --no-sisyphus-check to build our package.

In ~/hasher/repo/x86_64/RPMS.hasher/ we will find our freshly built package. rpmbuild also splits debug symbols automatically.

Note that you must build libstatusnotifier first because evolution-systray depends on it. hasher will use ~/hasher/repo as a packages source when building other packages (sick!).

Installing

After all of that work, we can finally enjoy our plugin:

apt-get install evolution
apt-get install /home/toreonify/hasher/repo/x86_64/RPMS.hasher/libstatusnotifier-1.0.0-alt1.x86_64.rpm
apt-get install /home/toreonify/hasher/repo/x86_64/RPMS.hasher/evolution-systray-0.2-alt1.x86_64.rpm 

Conclusion

I've now have a working tray icon to monitor new messages and my taskbar is less clutered. Evolution is a fine alternative to Outlook on Linux. There are bugs and weird behavior, but they are everywhere.

Source code is available on GitHub.

Binary packages are available on GitHub in releases.

Thoughts? Leave a comment