Compare commits

..
1 Commits
Author SHA1 Message Date
deepend 6bae96668a add autoaway 2026-10-20 23:47:29 -06:00
29 changed files with 828 additions and 8 deletions
+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
build-essential pkg-config meson ninja-build cmake \
gettext \
libcanberra-dev libglib2.0-dev \
libarchive-dev \
libarchive-dev libxss-dev \
libgtk-3-dev libfontconfig1-dev libpango1.0-dev xvfb xauth dbus-daemon at-spi2-core \
libwayland-client0 libwayland-cursor0 libwayland-egl1 \
libxkbcommon0 \
@@ -57,7 +57,7 @@ jobs:
ninja -C build
- name: Test emoji catalog, font coverage and picker
run: xvfb-run -a dbus-run-session -- meson test -C build --print-errorlogs "Emoji Data Tests" "Emoji Font Tests" "Emoji UI Tests"
run: xvfb-run -a dbus-run-session -- meson test -C build --print-errorlogs "Autoaway Tests" "Idle Backend Tests" "Emoji Data Tests" "Emoji Font Tests" "Emoji UI Tests"
- name: Install to AppDir
run: |
+5 -1
View File
@@ -198,7 +198,7 @@ jobs:
)
shell: cmd
- name: Test emoji catalog, font coverage and picker
- name: Test autoaway, emoji catalog, font coverage and picker
shell: cmd
run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat"
@@ -213,6 +213,10 @@ jobs:
..\zoitechat-build\x64\tests\emoji-%%T-tests.exe --tap
if errorlevel 1 exit /b 1
)
msbuild win32\autoaway-tests.vcxproj /m /verbosity:minimal /p:Configuration=Release /p:Platform=x64
if errorlevel 1 exit /b 1
..\zoitechat-build\x64\tests\autoaway-tests.exe --tap
if errorlevel 1 exit /b 1
- name: Preparing Artifacts
run: |
+5 -1
View File
@@ -209,7 +209,7 @@ jobs:
# Master added native emoji font staging and tests after the release
# branch split. Run these before packaging so MSI/MSIX use verified files.
- name: Test emoji catalog, font coverage and picker
- name: Test autoaway, emoji catalog, font coverage and picker
shell: cmd
run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat"
@@ -224,6 +224,10 @@ jobs:
..\zoitechat-build\x64\tests\emoji-%%T-tests.exe --tap
if errorlevel 1 exit /b 1
)
msbuild win32\autoaway-tests.vcxproj /m /verbosity:minimal /p:Configuration=Release /p:Platform=x64
if errorlevel 1 exit /b 1
..\zoitechat-build\x64\tests\autoaway-tests.exe --tap
if errorlevel 1 exit /b 1
- name: Install WiX Toolset
shell: pwsh
+35
View File
@@ -628,3 +628,38 @@ List of Settings
+------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| **url\_logging** | Toggle logging URLs to *<config>/url.log*. |
+------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
Autoaway
--------
In **Preferences > Chatting > General > Away**, set **Autoaway after** to a
number of minutes. The default is 0 (disabled). The separate **Autoaway
message** defaults to "Idle". No plugin or scripting language is required.
The same options are available immediately through ``/SET``::
/SET away_auto_timeout 10
/SET away_auto_reason Idle
/SET away_auto_system ON
With desktop activity enabled, Windows uses the current login session's last
input time, macOS uses CoreGraphics input time, and native X11 uses the
XScreenSaver extension if its development library was available at build time.
Debian/Ubuntu builders can install ``libxss-dev``; Fedora builders can install
``libXScrnSaver-devel``. Meson detects this optional dependency automatically.
On native Wayland, XWayland, unsupported desktops, and builds without XScreenSaver,
autoaway falls back to keyboard, mouse, scroll and touch activity **inside
ZoiteChat**. Activity in other applications does not reset that fallback timer.
Set ``away_auto_system OFF`` to always use ZoiteChat activity on every platform.
The text frontend uses submitted user input. Network traffic, redraws and window
focus changes do not count as user activity.
Autoaway checks every five seconds, after IRC login finishes, independently on
each connected network. Returning clears only away states set by autoaway.
Manual ``/AWAY`` (including ``/AWAY`` while already automatically away) takes
ownership, so activity does not clear it. ``/BACK`` resets the inactivity timer.
The existing **Automatically unmark away** setting keeps its separate behaviour
when sending messages. Do not run another autoaway script at the same time.
Disabling autoaway clears its own away states. Reconnecting re-evaluates current
inactivity instead of restoring an outdated automatic away state; manual away
restoration is unchanged.
+1 -1
View File
@@ -55,7 +55,7 @@
"run-tests": true,
"test-rule": "",
"test-commands": [
"meson test --print-errorlogs 'Emoji Data Tests' 'Emoji Font Tests'"
"meson test --print-errorlogs 'Autoaway Tests' 'Idle Backend Tests' 'Emoji Data Tests' 'Emoji Font Tests'"
],
"config-opts": [
"-Ddbus-service-use-appid=true",
+1
View File
@@ -25,6 +25,7 @@ BuildRequires: pkgconfig(gmodule-2.0)
BuildRequires: pkgconfig(gtk+-3.0) >= 3.22
BuildRequires: pkgconfig(fontconfig) >= 2.13
BuildRequires: pkgconfig(pangoft2) >= 1.44
BuildRequires: pkgconfig(xscrnsaver)
BuildRequires: pkgconfig(iso-codes)
BuildRequires: pkgconfig(libarchive)
BuildRequires: pkgconfig(libcanberra) >= 0.22
+135
View File
@@ -0,0 +1,135 @@
/* ZoiteChat
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "zoitechat.h"
#include "zoitechatc.h"
#include "server.h"
#include "fe.h"
#include "autoaway.h"
static gint64 last_activity;
static int timer_tag;
static int previous_timeout;
static unsigned int previous_system;
void
autoaway_activity (void)
{
last_activity = g_get_monotonic_time ();
}
/* A manual /AWAY takes ownership even if the server already says away. */
void
autoaway_manual (server *serv)
{
serv->autoaway_owned = FALSE;
autoaway_activity ();
}
void
autoaway_reset (server *serv)
{
if (!serv->autoaway_owned)
return;
serv->autoaway_owned = FALSE;
serv->reconnect_away = FALSE;
g_clear_pointer (&serv->last_away_reason, g_free);
}
static void
autoaway_release (server *serv)
{
if (!serv->autoaway_owned)
return;
/* Cancel even an unacknowledged AWAY. IRC replies may arrive later. */
if (serv->connected && serv->end_of_motd)
serv->p_set_back (serv);
autoaway_reset (serv);
}
void
autoaway_check (gint64 now, gint64 desktop_idle)
{
GSList *list;
gint64 idle = MAX ((gint64)0, now - last_activity);
gint64 limit = (gint64)prefs.hex_away_auto_timeout * 60 * G_USEC_PER_SEC;
if (prefs.hex_away_auto_system && desktop_idle >= 0)
idle = MIN (idle, desktop_idle);
for (list = serv_list; list; list = list->next)
{
server *serv = list->data;
if (!serv->connected)
{
/* Re-evaluate idle after login instead of restoring stale autoaway. */
autoaway_reset (serv);
continue;
}
if (!serv->end_of_motd)
continue;
if (limit <= 0 || idle < limit)
{
autoaway_release (serv);
continue;
}
if (serv->is_away || serv->reconnect_away)
continue;
/* Retry a rejected/lost request slowly, never once per poll. */
if (serv->autoaway_owned &&
now - serv->autoaway_sent_at < 30 * G_USEC_PER_SEC)
continue;
g_free (serv->last_away_reason);
serv->last_away_reason = g_strdup (prefs.hex_away_auto_reason[0] ?
prefs.hex_away_auto_reason : _("Idle"));
serv->autoaway_owned = TRUE;
serv->autoaway_sent_at = now;
serv->p_set_away (serv, serv->last_away_reason);
}
}
static int
autoaway_timer (void)
{
autoaway_check (g_get_monotonic_time (),
prefs.hex_away_auto_system ? fe_get_idle_time () : -1);
return 1;
}
void
autoaway_reinit (void)
{
if (previous_timeout != prefs.hex_away_auto_timeout ||
previous_system != prefs.hex_away_auto_system)
{
autoaway_activity ();
previous_timeout = prefs.hex_away_auto_timeout;
previous_system = prefs.hex_away_auto_system;
}
if (prefs.hex_away_auto_timeout > 0)
{
if (!timer_tag)
{
autoaway_activity ();
timer_tag = fe_timeout_add_seconds (5, autoaway_timer, NULL);
}
}
else
{
if (timer_tag)
{
fe_timeout_remove (timer_tag);
timer_tag = 0;
}
/* Disabling autoaway must also work without another input event. */
autoaway_check (g_get_monotonic_time (), -1);
}
}
+19
View File
@@ -0,0 +1,19 @@
/* ZoiteChat
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#ifndef ZOITECHAT_AUTOAWAY_H
#define ZOITECHAT_AUTOAWAY_H
#include <glib.h>
struct server;
void autoaway_activity (void);
void autoaway_manual (struct server *serv);
void autoaway_reset (struct server *serv);
void autoaway_reinit (void);
/* Both samples are in microseconds; -1 means desktop idle is unavailable. */
void autoaway_check (gint64 now, gint64 desktop_idle);
#endif
+6
View File
@@ -29,6 +29,7 @@
#include "fe.h"
#include "text.h"
#include "zoitechatc.h"
#include "autoaway.h"
#include "typedef.h"
#ifdef WIN32
@@ -359,6 +360,9 @@ default_file (void)
const struct prefs vars[] =
{
{"away_auto_reason", P_OFFSET (hex_away_auto_reason), TYPE_STR},
{"away_auto_system", P_OFFINT (hex_away_auto_system), TYPE_BOOL, autoaway_reinit},
{"away_auto_timeout", P_OFFINT (hex_away_auto_timeout), TYPE_INT, autoaway_reinit},
{"away_auto_unmark", P_OFFINT (hex_away_auto_unmark), TYPE_BOOL},
{"away_omit_alerts", P_OFFINT (hex_away_omit_alerts), TYPE_BOOL},
{"away_reason", P_OFFSET (hex_away_reason), TYPE_STR},
@@ -767,6 +771,7 @@ load_default_config(void)
/* put in default values, anything left out is automatically zero */
/* BOOLEANS */
prefs.hex_away_auto_system = 1;
prefs.hex_away_show_once = 1;
prefs.hex_away_track = 1;
prefs.hex_dcc_auto_resume = 1;
@@ -887,6 +892,7 @@ load_default_config(void)
prefs.hex_url_grabber_limit = 100; /* 0 means unlimited */
/* STRINGS */
strcpy (prefs.hex_away_auto_reason, _("Idle"));
strcpy (prefs.hex_away_reason, _("I'm busy"));
strcpy (prefs.hex_completion_suffix, ",");
#ifdef WIN32
+2
View File
@@ -11,6 +11,7 @@
</ProjectConfiguration>
</ItemGroup>
<ItemGroup>
<ClInclude Include="autoaway.h" />
<ClInclude Include="cfgfiles.h" />
<ClInclude Include="chanopt.h" />
<ClInclude Include="ctcp.h" />
@@ -52,6 +53,7 @@
<ClInclude Include="gtk3-theme-service.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="autoaway.c" />
<ClCompile Include="cfgfiles.c" />
<ClCompile Include="chanopt.c" />
<ClCompile Include="ctcp.c" />
+6
View File
@@ -17,6 +17,9 @@
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="autoaway.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="cfgfiles.h">
<Filter>Header Files</Filter>
</ClInclude>
@@ -136,6 +139,9 @@
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="autoaway.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="cfgfiles.c">
<Filter>Source Files</Filter>
</ClCompile>
+1
View File
@@ -142,6 +142,7 @@ void fe_lastlog (session *sess, session *lastlog_sess, char *sstr, gtk_xtext_sea
void fe_set_lag (server *serv, long lag);
void fe_set_throttle (server *serv);
void fe_set_away (server *serv);
gint64 fe_get_idle_time (void);
void fe_serverlist_open (session *sess);
void fe_get_bool (char *title, char *prompt, void *callback, void *userdata);
void fe_get_str (char *prompt, char *def, void *callback, void *ud);
+8
View File
@@ -1,4 +1,5 @@
common_sources = [
'autoaway.c',
'cfgfiles.c',
'chanopt.c',
'ctcp.c',
@@ -224,3 +225,10 @@ test('Hex Color Tests', hexcolor_tests,
protocol: 'tap',
timeout: 120,
)
autoaway_tests = executable('autoaway_tests',
['tests/test-autoaway.c', 'autoaway.c'],
include_directories: [config_h_include, include_directories('.')],
dependencies: zoitechat_common_header_deps,
)
test('Autoaway Tests', autoaway_tests, protocol: 'tap', timeout: 30)
+12 -1
View File
@@ -54,6 +54,7 @@
#include "server.h"
#include "tree.h"
#include "outbound.h"
#include "autoaway.h"
#include "chanopt.h"
#define TBUFSIZE 4096
@@ -360,6 +361,10 @@ cmd_away (struct session *sess, char *tbuf, char *word[], char *word_eol[])
{
char *reason = word_eol[2];
/* Restoring a manual away after reconnect is not new user input. */
if (!sess->server->reconnect_away)
autoaway_manual (sess->server);
if (!(*reason))
{
if (sess->server->is_away)
@@ -396,7 +401,10 @@ cmd_away (struct session *sess, char *tbuf, char *word[], char *word_eol[])
static int
cmd_back (struct session *sess, char *tbuf, char *word[], char *word_eol[])
{
if (sess->server->is_away)
gboolean was_autoaway = sess->server->autoaway_owned;
autoaway_manual (sess->server);
if (sess->server->is_away || was_autoaway)
{
sess->server->p_set_back (sess->server);
}
@@ -5051,6 +5059,9 @@ handle_user_input (session *sess, char *text, int history, int nocommand)
if (*text == '\0')
return 1;
if (history)
autoaway_activity ();
if (history)
history_add (&sess->history, text);
+7
View File
@@ -30,6 +30,7 @@
#include "zoitechat.h"
#include "proto-irc.h"
#include "autoaway.h"
#include "ctcp.h"
#include "fe.h"
#include "ignore.h"
@@ -438,6 +439,12 @@ irc_raw (server *serv, char *raw)
{
int len;
char tbuf[8704];
/* /QUOTE AWAY and scripts sending raw AWAY also take manual ownership. */
if (!g_ascii_strncasecmp (raw, "AWAY", 4) &&
(raw[4] == 0 || raw[4] == ' '))
autoaway_manual (serv);
if (*raw)
{
len = strlen (raw);
+2
View File
@@ -54,6 +54,7 @@
#include "proto-irc.h"
#include "servlist.h"
#include "server.h"
#include "autoaway.h"
#include "sts.h"
#ifdef USE_OPENSSL
@@ -1873,6 +1874,7 @@ is_server (server *serv)
void
server_set_defaults (server *serv)
{
autoaway_reset (serv);
g_free (serv->chantypes);
g_clear_pointer (&serv->clienttagdeny, g_free);
g_free (serv->chanmodes);
+296
View File
@@ -0,0 +1,296 @@
/* ZoiteChat
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <string.h>
#include "../zoitechat.h"
#include "../autoaway.h"
struct zoitechatprefs prefs;
GSList *serv_list;
static server servers[3];
static int away_count, back_count, timer_adds, timer_removes;
static gint64 base;
static char *sent_reason;
/* Protocol stubs deliberately do not acknowledge requests. */
static void
send_away (server *serv, char *reason)
{
away_count++;
g_free (sent_reason);
sent_reason = g_strdup (reason);
}
static void
send_back (server *serv)
{
back_count++;
}
int
fe_timeout_add_seconds (int interval, void *callback, void *data)
{
g_assert_cmpint (interval, ==, 5);
timer_adds++;
return 42;
}
void
fe_timeout_remove (int tag)
{
g_assert_cmpint (tag, ==, 42);
timer_removes++;
}
gint64
fe_get_idle_time (void)
{
return -1;
}
static void
setup (void)
{
int i;
prefs.hex_away_auto_timeout = 0;
autoaway_reinit ();
for (i = 0; i < 3; i++)
g_free (servers[i].last_away_reason);
g_slist_free (serv_list);
serv_list = NULL;
memset (servers, 0, sizeof servers);
memset (&prefs, 0, sizeof prefs);
away_count = back_count = timer_adds = timer_removes = 0;
g_clear_pointer (&sent_reason, g_free);
prefs.hex_away_auto_timeout = 10;
prefs.hex_away_auto_system = TRUE;
g_strlcpy (prefs.hex_away_auto_reason, "Auto idle", sizeof prefs.hex_away_auto_reason);
for (i = 0; i < 3; i++)
{
servers[i].connected = TRUE;
servers[i].end_of_motd = TRUE;
servers[i].p_set_away = send_away;
servers[i].p_set_back = send_back;
}
serv_list = g_slist_append (NULL, &servers[0]);
autoaway_activity ();
base = g_get_monotonic_time ();
}
static void
test_fallback_boundary (void)
{
setup ();
autoaway_check (base + 599 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 0);
autoaway_check (base + 600 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 1);
g_assert_true (servers[0].autoaway_owned);
g_assert_cmpstr (sent_reason, ==, "Auto idle");
/* An ACK is not required to avoid flooding the connection. */
autoaway_check (base + 605 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 1);
servers[0].is_away = TRUE;
autoaway_check (base + 640 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 1);
autoaway_activity ();
autoaway_check (g_get_monotonic_time (), -1);
g_assert_cmpint (back_count, ==, 1);
g_assert_false (servers[0].autoaway_owned);
g_assert_null (servers[0].last_away_reason);
}
static void
test_desktop_activity (void)
{
setup ();
autoaway_check (base + 900 * G_USEC_PER_SEC, 5 * G_USEC_PER_SEC);
g_assert_cmpint (away_count, ==, 0);
autoaway_check (base + 900 * G_USEC_PER_SEC, 700 * G_USEC_PER_SEC);
g_assert_cmpint (away_count, ==, 1);
servers[0].is_away = TRUE;
autoaway_check (base + 905 * G_USEC_PER_SEC, 0);
g_assert_cmpint (back_count, ==, 1);
/* App input wins even if the native backend still has an old sample. */
autoaway_activity ();
autoaway_check (g_get_monotonic_time (), 900 * G_USEC_PER_SEC);
g_assert_cmpint (away_count, ==, 1);
}
static void
test_app_mode (void)
{
setup ();
prefs.hex_away_auto_system = FALSE;
autoaway_check (base + 600 * G_USEC_PER_SEC, 0);
g_assert_cmpint (away_count, ==, 1);
}
static void
test_manual_away (void)
{
setup ();
servers[0].is_away = TRUE;
servers[0].last_away_reason = g_strdup ("Lunch");
autoaway_check (base + 900 * G_USEC_PER_SEC, -1);
autoaway_check (base + 905 * G_USEC_PER_SEC, 0);
prefs.hex_away_auto_timeout = 0;
autoaway_reinit ();
g_assert_cmpint (away_count, ==, 0);
g_assert_cmpint (back_count, ==, 0);
g_assert_cmpstr (servers[0].last_away_reason, ==, "Lunch");
}
static void
test_manual_takeover (void)
{
setup ();
autoaway_check (base + 600 * G_USEC_PER_SEC, -1);
servers[0].is_away = TRUE;
/* This is also called for /AWAY with no reason, even if already away. */
autoaway_manual (&servers[0]);
autoaway_check (g_get_monotonic_time (), 0);
prefs.hex_away_auto_timeout = 0;
autoaway_reinit ();
g_assert_cmpint (back_count, ==, 0);
g_assert_true (servers[0].is_away);
}
static void
test_pending_away_cancel (void)
{
setup ();
autoaway_check (base + 600 * G_USEC_PER_SEC, -1);
g_assert_false (servers[0].is_away);
autoaway_activity ();
autoaway_check (g_get_monotonic_time (), -1);
g_assert_cmpint (back_count, ==, 1);
/* A late 306 must not make us own the away state again. */
servers[0].is_away = TRUE;
autoaway_check (g_get_monotonic_time (), -1);
g_assert_cmpint (back_count, ==, 1);
}
static void
test_login_and_multiple_servers (void)
{
setup ();
serv_list = g_slist_append (serv_list, &servers[1]);
serv_list = g_slist_append (serv_list, &servers[2]);
servers[1].is_away = TRUE;
servers[1].last_away_reason = g_strdup ("Manual");
servers[2].end_of_motd = FALSE;
autoaway_check (base + 600 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 1);
servers[2].end_of_motd = TRUE;
autoaway_check (base + 605 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 2);
autoaway_check (base + 610 * G_USEC_PER_SEC, 0);
g_assert_cmpint (back_count, ==, 2);
g_assert_cmpstr (servers[1].last_away_reason, ==, "Manual");
}
static void
test_disconnect_and_reconnect (void)
{
setup ();
autoaway_check (base + 600 * G_USEC_PER_SEC, -1);
servers[0].connected = FALSE;
servers[0].is_away = TRUE;
servers[0].reconnect_away = TRUE;
autoaway_check (base + 605 * G_USEC_PER_SEC, -1);
g_assert_false (servers[0].autoaway_owned);
g_assert_false (servers[0].reconnect_away);
g_assert_null (servers[0].last_away_reason);
g_assert_cmpint (back_count, ==, 0);
servers[0].is_away = FALSE;
servers[0].connected = TRUE;
servers[0].end_of_motd = FALSE;
autoaway_check (base + 610 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 1);
servers[0].end_of_motd = TRUE;
autoaway_check (base + 615 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 2);
/* server_set_defaults resets ownership even before the next timer tick. */
servers[0].reconnect_away = TRUE;
autoaway_reset (&servers[0]);
g_assert_false (servers[0].reconnect_away);
/* Existing manual reconnect-away must be left alone. */
servers[0].reconnect_away = TRUE;
servers[0].last_away_reason = g_strdup ("Manual");
autoaway_reset (&servers[0]);
g_assert_true (servers[0].reconnect_away);
g_assert_cmpstr (servers[0].last_away_reason, ==, "Manual");
}
static void
test_retry (void)
{
setup ();
autoaway_check (base + 600 * G_USEC_PER_SEC, -1);
autoaway_check (base + 629 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 1);
autoaway_check (base + 630 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 2);
}
static void
test_disable_and_timers (void)
{
setup ();
autoaway_reinit ();
autoaway_reinit ();
g_assert_cmpint (timer_adds, ==, 1);
base = g_get_monotonic_time ();
autoaway_check (base + 600 * G_USEC_PER_SEC, -1);
prefs.hex_away_auto_timeout = 0;
autoaway_reinit ();
g_assert_cmpint (back_count, ==, 1);
g_assert_cmpint (timer_removes, ==, 1);
g_assert_false (servers[0].autoaway_owned);
autoaway_reinit ();
g_assert_cmpint (back_count, ==, 1);
prefs.hex_away_auto_timeout = -1;
autoaway_reinit ();
g_assert_cmpint (away_count, ==, 1);
}
static void
test_empty_reason_and_large_timeout (void)
{
setup ();
prefs.hex_away_auto_reason[0] = 0;
autoaway_check (base + 600 * G_USEC_PER_SEC, -1);
g_assert_nonnull (sent_reason);
g_assert_cmpstr (sent_reason, !=, "");
setup ();
prefs.hex_away_auto_timeout = G_MAXINT;
autoaway_check (base + 900 * G_USEC_PER_SEC, -1);
g_assert_cmpint (away_count, ==, 0);
}
int
main (int argc, char **argv)
{
int result, i;
g_test_init (&argc, &argv, NULL);
g_test_add_func ("/autoaway/fallback-boundary", test_fallback_boundary);
g_test_add_func ("/autoaway/desktop-activity", test_desktop_activity);
g_test_add_func ("/autoaway/app-mode", test_app_mode);
g_test_add_func ("/autoaway/manual-away", test_manual_away);
g_test_add_func ("/autoaway/manual-takeover", test_manual_takeover);
g_test_add_func ("/autoaway/pending-cancel", test_pending_away_cancel);
g_test_add_func ("/autoaway/login-multiple-servers", test_login_and_multiple_servers);
g_test_add_func ("/autoaway/reconnect", test_disconnect_and_reconnect);
g_test_add_func ("/autoaway/retry", test_retry);
g_test_add_func ("/autoaway/disable-timers", test_disable_and_timers);
g_test_add_func ("/autoaway/empty-reason-large-timeout", test_empty_reason_and_large_timeout);
result = g_test_run ();
for (i = 0; i < 3; i++)
g_free (servers[i].last_away_reason);
g_free (sent_reason);
g_slist_free (serv_list);
return result;
}
+3
View File
@@ -46,6 +46,7 @@
#include "plugin.h"
#include "plugin-identd.h"
#include "plugin-timer.h"
#include "autoaway.h"
#include "notify.h"
#include "server.h"
#include "servlist.h"
@@ -499,6 +500,8 @@ zoitechat_reinit_timers (void)
static int lag_check_tag = 0;
static int away_tag = 0;
autoaway_reinit ();
if (prefs.hex_notify_timeout && notify_tag == 0)
{
notify_tag = fe_timeout_add_seconds (prefs.hex_notify_timeout,
+5
View File
@@ -105,6 +105,7 @@ struct zoitechatprefs
/* these are the rebranded, consistent, sorted zoitechat variables */
/* BOOLEANS */
unsigned int hex_away_auto_system;
unsigned int hex_away_auto_unmark;
unsigned int hex_away_omit_alerts;
unsigned int hex_away_show_once;
@@ -233,6 +234,7 @@ struct zoitechatprefs
unsigned int hex_url_logging;
/* NUMBERS */
int hex_away_auto_timeout;
int hex_away_size_max;
int hex_away_timeout;
int hex_completion_amount;
@@ -311,6 +313,7 @@ struct zoitechatprefs
int hex_url_grabber_limit;
/* STRINGS */
char hex_away_auto_reason[256];
char hex_away_reason[256];
char hex_completion_suffix[4]; /* Only ever holds a one-character string. */
char hex_dcc_completed_dir[PATHLEN + 1];
@@ -597,6 +600,7 @@ typedef struct server
/*time_t connect_time;*/ /* when did it connect? */
unsigned long lag_sent; /* we are still waiting for this ping response*/
time_t ping_recv; /* when we last got a ping reply */
gint64 autoaway_sent_at;
time_t away_time; /* when we were marked away */
char *encoding;
@@ -616,6 +620,7 @@ typedef struct server
unsigned int end_of_motd:1; /* end of motd reached (logged in) */
unsigned int sent_quit:1; /* sent a QUIT already? */
unsigned int use_listargs:1; /* undernet and dalnet need /list >0,<10000 */
unsigned int autoaway_owned:1;
unsigned int is_away:1;
unsigned int reconnect_away:1; /* whether to reconnect in is_away state */
unsigned int dont_use_proxy:1; /* to proxy or not to proxy */
+2
View File
@@ -59,6 +59,7 @@
#include "notifygui.h"
#include "textgui.h"
#include "fkeys.h"
#include "idle.h"
#include "plugin-tray.h"
#include "urlgrab.h"
#include "setup.h"
@@ -638,6 +639,7 @@ fe_init (void)
{
theme_manager_init ();
key_init ();
idle_init ();
pixmaps_init ();
#ifdef HAVE_GTK_MAC
+3 -1
View File
@@ -31,7 +31,7 @@
</ClCompile>
<Link>
<AdditionalLibraryDirectories>$(ArchiveLibDir);$(DepsRoot)\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>$(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;dwmapi.lib;pangoft2-1.0.lib;fontconfig.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalDependencies>$(DepLibs);$(ZoiteChatLib)common.lib;wbemuuid.lib;dwmapi.lib;user32.lib;pangoft2-1.0.lib;fontconfig.lib;%(AdditionalDependencies)</AdditionalDependencies>
<EntryPointSymbol>mainCRTStartup</EntryPointSymbol>
</Link>
</ItemDefinitionGroup>
@@ -49,6 +49,7 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
</PreBuildEvent>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="idle.h" />
<ClInclude Include="ascii.h" />
<ClInclude Include="banlist.h" />
<ClInclude Include="chanlist.h" />
@@ -91,6 +92,7 @@ powershell "Get-Content -Encoding UTF8 '$(ZoiteChatLib)zoitechat.rc.utf8' | Out-
<ClInclude Include="theme\theme-preferences.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="idle.c" />
<ClCompile Include="ascii.c" />
<ClCompile Include="banlist.c" />
<ClCompile Include="chanlist.c" />
+6
View File
@@ -15,6 +15,9 @@
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="idle.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="ascii.h">
<Filter>Header Files</Filter>
</ClInclude>
@@ -137,6 +140,9 @@
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="idle.c">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="ascii.c">
<Filter>Source Files</Filter>
</ClCompile>
+98
View File
@@ -0,0 +1,98 @@
/* ZoiteChat
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "config.h"
#include <gtk/gtk.h>
#include "../common/autoaway.h"
#include "idle.h"
#ifdef WIN32
#include <windows.h>
#elif defined(__APPLE__)
#include <CoreGraphics/CoreGraphics.h>
#include <math.h>
#elif defined(HAVE_XSS) && defined(GDK_WINDOWING_X11)
#include <gdk/gdkx.h>
#include <X11/extensions/scrnsaver.h>
#endif
/* Do not count focus changes, redraws, timers or incoming IRC traffic. */
static void
idle_event (GdkEvent *event, gpointer data)
{
switch (event->type)
{
case GDK_KEY_PRESS:
case GDK_BUTTON_PRESS:
case GDK_SCROLL:
case GDK_MOTION_NOTIFY:
case GDK_TOUCH_BEGIN:
case GDK_TOUCH_UPDATE:
autoaway_activity ();
break;
default:
break;
}
gtk_main_do_event (event);
}
void
idle_init (void)
{
autoaway_activity ();
/* Keep GTK's normal dispatch, including detached windows and dialogs. */
gdk_event_handler_set (idle_event, NULL, NULL);
}
/* Microseconds since desktop input, or -1 for the portable app fallback. */
gint64
fe_get_idle_time (void)
{
#ifdef WIN32
LASTINPUTINFO input = {0};
DWORD elapsed;
input.cbSize = sizeof input;
if (!GetLastInputInfo (&input))
return -1;
/* DWORD subtraction handles the 49-day GetTickCount wrap. Some injected
* input has a slightly newer timestamp; do not treat it as weeks idle. */
elapsed = GetTickCount () - input.dwTime;
if ((gint32)elapsed < 0)
elapsed = 0;
return (gint64)elapsed * 1000;
#elif defined(__APPLE__)
CFTimeInterval seconds = CGEventSourceSecondsSinceLastEventType (
kCGEventSourceStateCombinedSessionState, kCGAnyInputEventType);
if (!isfinite (seconds) || seconds < 0 ||
seconds >= (double)G_MAXINT64 / G_USEC_PER_SEC)
return -1;
return (gint64)(seconds * G_USEC_PER_SEC);
#elif defined(HAVE_XSS) && defined(GDK_WINDOWING_X11)
GdkDisplay *display = gdk_display_get_default ();
XScreenSaverInfo info = {0};
Display *xdisplay;
int event_base, error_base, error;
Status ok;
const char *wayland = g_getenv ("WAYLAND_DISPLAY");
/* XWayland idle is not desktop-wide. Never pass a Wayland display to Xlib. */
if (!display || !GDK_IS_X11_DISPLAY (display) || (wayland && *wayland))
return -1;
xdisplay = gdk_x11_display_get_xdisplay (display);
gdk_x11_display_error_trap_push (display);
ok = XScreenSaverQueryExtension (xdisplay, &event_base, &error_base);
if (ok)
ok = XScreenSaverQueryInfo (xdisplay, DefaultRootWindow (xdisplay), &info);
error = gdk_x11_display_error_trap_pop (display);
if (!ok || error)
return -1;
return (gint64)info.idle * 1000;
#else
return -1;
#endif
}
+10
View File
@@ -0,0 +1,10 @@
/* ZoiteChat
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#ifndef ZOITECHAT_IDLE_H
#define ZOITECHAT_IDLE_H
void idle_init (void);
#endif
+22 -1
View File
@@ -25,6 +25,7 @@ zoitechat_gtk_sources = [
'gtkutil.c',
'icon-resolver.c',
'ignoregui.c',
'idle.c',
'inline-image.c',
'joind.c',
'menu.c',
@@ -45,6 +46,7 @@ zoitechat_gtk_sources = [
]
zoitechat_gtk_cflags = []
idle_deps = []
zoitechat_gtk_deps = [
zoitechat_common_dep,
@@ -93,9 +95,17 @@ if host_machine.system() != 'windows'
zoitechat_gtk_deps += gdk_x11_dep
endif
xss_dep = dependency('xscrnsaver', required: false)
if gdk_x11_dep.found() and xss_dep.found()
zoitechat_gtk_deps += xss_dep
idle_deps += [gdk_x11_dep, xss_dep]
zoitechat_gtk_cflags += '-DHAVE_XSS'
endif
x11_dep = dependency('x11', required: false)
if x11_dep.found()
zoitechat_gtk_deps += x11_dep
idle_deps += x11_dep
endif
endif
@@ -105,13 +115,16 @@ zoitechat_gtk_objcflags = []
if host_machine.system() == 'windows'
zoitechat_gtk_sources += 'notifications/notification-windows.c'
zoitechat_gtk_deps += cc.find_library('dwmapi', required: true)
idle_deps += cc.find_library('user32', required: true)
zoitechat_gtk_deps += idle_deps
zoitechat_theme_deps += cc.find_library('dwmapi', required: true)
elif host_machine.system() == 'darwin'
add_languages('objc', required: true)
zoitechat_gtk_sources += 'macos-shortcuts.c'
zoitechat_gtk_sources += 'notifications/notification-osx.m'
zoitechat_gtk_deps += dependency('appleframeworks', modules: ['Cocoa', 'UserNotifications'])
zoitechat_gtk_deps += dependency('appleframeworks', modules: ['Cocoa', 'UserNotifications', 'CoreGraphics'])
idle_deps += dependency('appleframeworks', modules: ['CoreGraphics'])
zoitechat_gtk_objcflags += '-fobjc-arc'
else
zoitechat_gtk_sources += 'notifications/notification-freedesktop.c'
@@ -302,3 +315,11 @@ xtext_tests = executable('xtext_tests',
dependencies: [gtk_dep],
)
test('XText Tests', xtext_tests, protocol: 'tap', timeout: 120)
idle_tests = executable('idle_tests',
'tests/test-idle.c',
include_directories: [config_h_include],
dependencies: [gtk_dep] + idle_deps,
c_args: zoitechat_gtk_cflags,
)
test('Idle Backend Tests', idle_tests, protocol: 'tap', timeout: 30)
+4
View File
@@ -558,6 +558,10 @@ static const setting general_settings[] =
{ST_ENTRY, N_("Away:"), P_OFFSETNL(hex_away_reason), 0, 0, sizeof prefs.hex_away_reason},
{ST_HEADER, N_("Away"),0,0,0},
{ST_NUMBER, N_("Autoaway after:"), P_OFFINTNL(hex_away_auto_timeout), N_("Set to 0 to disable automatic away."), (const char **)N_("minutes (0 = off)."), 1440},
{ST_ENTRY, N_("Autoaway message:"), P_OFFSETNL(hex_away_auto_reason), 0, 0, sizeof prefs.hex_away_auto_reason},
{ST_TOGGLE, N_("Use desktop activity when available"), P_OFFINTNL(hex_away_auto_system), N_("Use desktop input on Windows, macOS and X11. Otherwise, use keyboard and mouse activity in ZoiteChat."), 0, 0},
{ST_LABEL, N_("On Wayland, autoaway uses activity in ZoiteChat.\nOnly automatically set away states are cleared when activity resumes.")},
{ST_TOGGLE, N_("Show away once"), P_OFFINTNL(hex_away_show_once), N_("Show identical away messages only once."), 0, 0},
{ST_TOGGLE, N_("Automatically unmark away"), P_OFFINTNL(hex_away_auto_unmark), N_("Unmark yourself as away before sending messages."), 0, 0},
+85
View File
@@ -0,0 +1,85 @@
/* ZoiteChat
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <gtk/gtk.h>
#include "../idle.h"
static int activity_count, dispatch_count;
static void test_gtk_dispatch (GdkEvent *event);
void
autoaway_activity (void)
{
activity_count++;
}
/* Exercise the real handler without delivering synthetic input to a widget. */
#define gtk_main_do_event test_gtk_dispatch
#include "../idle.c"
#undef gtk_main_do_event
static void
test_gtk_dispatch (GdkEvent *event)
{
dispatch_count++;
}
static void
test_user_input_and_dispatch (void)
{
GdkEvent event = {0};
GdkEventType input[] = {GDK_KEY_PRESS, GDK_BUTTON_PRESS, GDK_SCROLL,
GDK_MOTION_NOTIFY, GDK_TOUCH_BEGIN, GDK_TOUCH_UPDATE};
GdkEventType other[] = {GDK_FOCUS_CHANGE, GDK_CONFIGURE, GDK_WINDOW_STATE,
GDK_PROPERTY_NOTIFY, GDK_VISIBILITY_NOTIFY};
unsigned int i;
activity_count = dispatch_count = 0;
for (i = 0; i < G_N_ELEMENTS (input); i++)
{
event.type = input[i];
idle_event (&event, NULL);
}
g_assert_cmpint (activity_count, ==, G_N_ELEMENTS (input));
for (i = 0; i < G_N_ELEMENTS (other); i++)
{
event.type = other[i];
idle_event (&event, NULL);
}
g_assert_cmpint (activity_count, ==, G_N_ELEMENTS (input));
g_assert_cmpint (dispatch_count, ==, G_N_ELEMENTS (input) + G_N_ELEMENTS (other));
}
static void
test_native_idle (void)
{
gint64 idle = fe_get_idle_time ();
g_assert_cmpint (idle, >=, -1);
#if defined(HAVE_XSS) && defined(GDK_WINDOWING_X11)
if (gdk_display_get_default () && GDK_IS_X11_DISPLAY (gdk_display_get_default ()))
{
/* XWayland must use the app fallback, even if XSS is compiled in. */
char *saved = g_strdup (g_getenv ("WAYLAND_DISPLAY"));
g_unsetenv ("WAYLAND_DISPLAY");
g_assert_cmpint (fe_get_idle_time (), >=, 0);
g_setenv ("WAYLAND_DISPLAY", "wayland-test", TRUE);
g_assert_cmpint (fe_get_idle_time (), ==, -1);
if (saved)
g_setenv ("WAYLAND_DISPLAY", saved, TRUE);
else
g_unsetenv ("WAYLAND_DISPLAY");
g_free (saved);
}
#endif
}
int
main (int argc, char **argv)
{
g_test_init (&argc, &argv, NULL);
gtk_init_check (&argc, &argv);
g_test_add_func ("/idle/user-input-and-dispatch", test_user_input_and_dispatch);
g_test_add_func ("/idle/native-backend", test_native_idle);
return g_test_run ();
}
+6
View File
@@ -48,6 +48,12 @@
#endif
gint64
fe_get_idle_time (void)
{
return -1;
}
static int done = FALSE;
+41
View File
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{EE388A27-6992-43C0-BA9B-61D8968D42E3}</ProjectGuid>
<SolutionDir>$(MSBuildThisFileDirectory)</SolutionDir>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<PlatformToolset>v142</PlatformToolset>
<ConfigurationType>Application</ConfigurationType>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<Import Project="zoitechat.props" />
<PropertyGroup>
<TargetName>autoaway-tests</TargetName>
<OutDir>$(ZoiteChatBuild)\$(ZoiteChatPlatform)\tests\</OutDir>
<IntDir>$(ZoiteChatObj)autoaway-tests\</IntDir>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<PreprocessorDefinitions>WIN32;_WIN64;_CONSOLE;$(OwnFlags);%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>$(ZoiteChatLib);$(DepsRoot)\include;$(OpenSslInclude);$(Glib);$(Gtk);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<AdditionalLibraryDirectories>$(DepsRoot)\lib;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<AdditionalDependencies>gio-2.0.lib;gobject-2.0.lib;glib-2.0.lib;$(IntlLib);%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="..\src\common\autoaway.c" />
<ClCompile Include="..\src\common\tests\test-autoaway.c" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>