Compare commits

..

14 Commits

Author SHA1 Message Date
d46305d7ab CMakeLists.txt: omit end{if,foreach} expressions
Their usefulness was almost negative.
2020-10-29 16:09:31 +01:00
7edd9720cd Bump minimum CMake version to 3.0
A nice, round number.
2020-10-26 23:28:11 +01:00
ab5ca0cf8b Elaborate on avoiding XDG_RUNTIME_DIR 2020-10-26 23:09:23 +01:00
f699b89dad Reorder headers 2020-10-02 02:08:39 +02:00
9244d2b657 Write a start marker to the DB event table 2020-10-02 01:55:46 +02:00
4302fc4baf Use an empty string rather than "broken"
If we fail to retrieve the title, then there is no title,
though this doesn't mean the same as "no window",
for which we have NULL.
2020-10-02 01:50:37 +02:00
764dbaa126 Nullify a NULL concern
sqlite3_bind_text() is documented to bind NULL.
2020-10-02 01:37:08 +02:00
7d4695d8bd Ensure the inactivity alarm is launched on startup
We forgot to flush.
2020-10-02 01:32:19 +02:00
3482ee66a3 Watch changes of WM_CLASS
There may be some interesting information in there.
Sometimes it may be hard to identify applications by their title.
2020-10-02 01:31:46 +02:00
86b0579cb7 Write events to the SQLite database 2020-09-25 07:20:49 +02:00
27a63e3414 Collect events in the main thread 2020-09-25 06:45:27 +02:00
3dd4e69235 Update README.adoc
The last commit failed to update documentation.
2020-09-25 05:36:08 +02:00
6e3f3c950d Convert from Xlib xcb
This will make it easier to convert this project to Go/xgb later,
even though the SYNC extension isn't currently supported there.

So far unresolved: error handling.
2020-09-25 05:26:46 +02:00
93c61425b3 Cleanup 2020-09-23 16:47:03 +02:00
9 changed files with 1176 additions and 259 deletions

View File

@@ -1,34 +1,31 @@
project (wdmtg C)
cmake_minimum_required (VERSION 2.8.12)
cmake_minimum_required (VERSION 3.0)
project (wdmtg VERSION 0.1.0 LANGUAGES C)
# Vala really sucks at producing good C code
if ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
set (CMAKE_C_FLAGS_RELEASE
"${CMAKE_C_FLAGS_RELEASE} -Wno-ignored-qualifiers -Wno-incompatible-pointer-types")
endif ("${CMAKE_C_COMPILER_ID}" MATCHES "GNU" OR CMAKE_COMPILER_IS_GNUCC)
endif ()
# Options
option (OPTION_NOINSTALL "Only for developers; work without installing" OFF)
# Version
set (project_VERSION "0.1.0")
# Set some variables
if (OPTION_NOINSTALL)
set (project_SHARE_DIR ${PROJECT_SOURCE_DIR}/share)
elseif (WIN32)
set (project_SHARE_DIR ../share)
set (project_INSTALL_SHARE_DIR share)
else (OPTION_NOINSTALL)
else ()
set (project_SHARE_DIR ${CMAKE_INSTALL_PREFIX}/share/${PROJECT_NAME})
set (project_INSTALL_SHARE_DIR share/${PROJECT_NAME})
endif (OPTION_NOINSTALL)
endif ()
# Gather package information
set (CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
find_package (Vala 0.12 REQUIRED)
find_package (PkgConfig REQUIRED)
pkg_check_modules (dependencies REQUIRED gtk+-3.0 sqlite3 x11 xext xextproto)
pkg_check_modules (dependencies REQUIRED gtk+-3.0 sqlite3 xcb xcb-sync)
# Precompile Vala sources
include (ValaPrecompile)
@@ -57,7 +54,8 @@ set (project_SOURCES ${project_VALA_SOURCES} ${project_VALA_C} ${symbols_path})
# Build the executable and install it
include_directories (${dependencies_INCLUDE_DIRS})
link_directories (${dependencies_LIBRARY_DIRS})
add_executable (${PROJECT_NAME} ${PROJECT_NAME}.c ${project_SOURCES})
add_executable (${PROJECT_NAME}
${PROJECT_NAME}.c compound-text.c ${project_SOURCES})
target_link_libraries (${PROJECT_NAME} ${dependencies_LIBRARIES})
install (TARGETS ${PROJECT_NAME} DESTINATION bin)
@@ -67,14 +65,12 @@ set (CPACK_PACKAGE_DESCRIPTION_SUMMARY "Activity tracker")
set (CPACK_PACKAGE_VENDOR "Premysl Eric Janouch")
set (CPACK_PACKAGE_CONTACT "Přemysl Eric Janouch <p@janouch.name>")
set (CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
set (CPACK_PACKAGE_VERSION ${project_VERSION})
set (CPACK_GENERATOR "TGZ;ZIP")
set (CPACK_PACKAGE_FILE_NAME
"${PROJECT_NAME}-${CPACK_PACKAGE_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${CPACK_PACKAGE_VERSION}")
"${PROJECT_NAME}-${PROJECT_VERSION}-${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}")
set (CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}-${PROJECT_VERSION}")
set (CPACK_SOURCE_GENERATOR "TGZ;ZIP")
set (CPACK_SOURCE_IGNORE_FILES "/build;/\\\\.git")
set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${CPACK_PACKAGE_VERSION}")
set (CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}")
include (CPack)

View File

@@ -16,7 +16,7 @@ a package with the latest development version from Archlinux's AUR.
Building and Running
--------------------
Build dependencies: CMake, pkg-config, Vala >= 0.12 +
Runtime dependencies: gtk+-3.0, sqlite3, x11, xextproto, xext
Runtime dependencies: gtk+-3.0, sqlite3, xcb, xcb-sync
$ git clone --recursive https://git.janouch.name/p/wdmtg.git
$ mkdir wdmtg/build

633
compound-text.c Normal file
View File

@@ -0,0 +1,633 @@
//
// compound-text.c: partial X11 COMPOUND_TEXT to UCS-4 transcoder
//
// Copyright (c) 2020, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
#include <stdbool.h>
#include <stdlib.h> // malloc, free, NULL, size_t
#include <glib.h> // g_utf8_*, g_ucs4_to_utf8
// None of the full Chinese, Japanese, Korean character sets are supported,
// and will be replaced by a lot of funny question marks
enum compound_text_encoding {
COMPOUND_TEXT_ASCII,
COMPOUND_TEXT_ISO8859_1_GR,
COMPOUND_TEXT_ISO8859_2_GR,
COMPOUND_TEXT_ISO8859_3_GR,
COMPOUND_TEXT_ISO8859_4_GR,
COMPOUND_TEXT_ISO8859_5_GR,
COMPOUND_TEXT_ISO8859_6_GR,
COMPOUND_TEXT_ISO8859_7_GR,
COMPOUND_TEXT_ISO8859_8_GR,
COMPOUND_TEXT_ISO8859_9_GR,
COMPOUND_TEXT_ISO8859_10_GR,
COMPOUND_TEXT_ISO8859_13_GR,
COMPOUND_TEXT_ISO8859_14_GR,
COMPOUND_TEXT_ISO8859_15_GR,
COMPOUND_TEXT_ISO8859_16_GR,
COMPOUND_TEXT_JIS_X0201_GR,
COMPOUND_TEXT_JIS_X0201_GL,
COMPOUND_TEXT_COUNT
};
/*
Generated from glibc charset data, based on the following script:
for enc in ISO-8859-{1,2,3,4,5,6,7,8,9,10,13,14,15,16} JIS_X0201; do
echo "[COMPOUND_TEXT_$enc] ="
zcat /usr/share/i18n/charmaps/$enc.gz | \
perl -nle '$x{hex($2)} = "0x$1" if m|^<U(.*?)> +/x(..) +.*|;
sub tbl { join ", ", map { $x{$_} || "0x0000" } @_ }
END { print tbl(0x20..0x7F); print tbl(0xa0..0xFF); }'
done | fmt -sw70 | sed 's|^0|\t&|; s|^|\t|'
*/
static unsigned short compound_text_tables[COMPOUND_TEXT_COUNT][96] = {
[COMPOUND_TEXT_ASCII] = {
0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
0x0058, 0x0059, 0x005A, 0x005B, 0x005C, 0x005D, 0x005E, 0x005F,
0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
0x0078, 0x0079, 0x007A, 0x007B, 0x007C, 0x007D, 0x007E, 0x007F
},
[COMPOUND_TEXT_ISO8859_1_GR] = {
0x00A0, 0x00A1, 0x00A2, 0x00A3, 0x00A4, 0x00A5, 0x00A6, 0x00A7,
0x00A8, 0x00A9, 0x00AA, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF,
0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5, 0x00B6, 0x00B7,
0x00B8, 0x00B9, 0x00BA, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00BF,
0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x00C7,
0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF,
0x00D0, 0x00D1, 0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x00D7,
0x00D8, 0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x00DD, 0x00DE, 0x00DF,
0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x00E7,
0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x00EC, 0x00ED, 0x00EE, 0x00EF,
0x00F0, 0x00F1, 0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x00F7,
0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x00FD, 0x00FE, 0x00FF
},
[COMPOUND_TEXT_ISO8859_2_GR] = {
0x00A0, 0x0104, 0x02D8, 0x0141, 0x00A4, 0x013D, 0x015A, 0x00A7,
0x00A8, 0x0160, 0x015E, 0x0164, 0x0179, 0x00AD, 0x017D, 0x017B,
0x00B0, 0x0105, 0x02DB, 0x0142, 0x00B4, 0x013E, 0x015B, 0x02C7,
0x00B8, 0x0161, 0x015F, 0x0165, 0x017A, 0x02DD, 0x017E, 0x017C,
0x0154, 0x00C1, 0x00C2, 0x0102, 0x00C4, 0x0139, 0x0106, 0x00C7,
0x010C, 0x00C9, 0x0118, 0x00CB, 0x011A, 0x00CD, 0x00CE, 0x010E,
0x0110, 0x0143, 0x0147, 0x00D3, 0x00D4, 0x0150, 0x00D6, 0x00D7,
0x0158, 0x016E, 0x00DA, 0x0170, 0x00DC, 0x00DD, 0x0162, 0x00DF,
0x0155, 0x00E1, 0x00E2, 0x0103, 0x00E4, 0x013A, 0x0107, 0x00E7,
0x010D, 0x00E9, 0x0119, 0x00EB, 0x011B, 0x00ED, 0x00EE, 0x010F,
0x0111, 0x0144, 0x0148, 0x00F3, 0x00F4, 0x0151, 0x00F6, 0x00F7,
0x0159, 0x016F, 0x00FA, 0x0171, 0x00FC, 0x00FD, 0x0163, 0x02D9
},
[COMPOUND_TEXT_ISO8859_3_GR] = {
0x00A0, 0x0126, 0x02D8, 0x00A3, 0x00A4, 0x0000, 0x0124, 0x00A7,
0x00A8, 0x0130, 0x015E, 0x011E, 0x0134, 0x00AD, 0x0000, 0x017B,
0x00B0, 0x0127, 0x00B2, 0x00B3, 0x00B4, 0x00B5, 0x0125, 0x00B7,
0x00B8, 0x0131, 0x015F, 0x011F, 0x0135, 0x00BD, 0x0000, 0x017C,
0x00C0, 0x00C1, 0x00C2, 0x0000, 0x00C4, 0x010A, 0x0108, 0x00C7,
0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF,
0x0000, 0x00D1, 0x00D2, 0x00D3, 0x00D4, 0x0120, 0x00D6, 0x00D7,
0x011C, 0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x016C, 0x015C, 0x00DF,
0x00E0, 0x00E1, 0x00E2, 0x0000, 0x00E4, 0x010B, 0x0109, 0x00E7,
0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x00EC, 0x00ED, 0x00EE, 0x00EF,
0x0000, 0x00F1, 0x00F2, 0x00F3, 0x00F4, 0x0121, 0x00F6, 0x00F7,
0x011D, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x016D, 0x015D, 0x02D9
},
[COMPOUND_TEXT_ISO8859_4_GR] = {
0x00A0, 0x0104, 0x0138, 0x0156, 0x00A4, 0x0128, 0x013B, 0x00A7,
0x00A8, 0x0160, 0x0112, 0x0122, 0x0166, 0x00AD, 0x017D, 0x00AF,
0x00B0, 0x0105, 0x02DB, 0x0157, 0x00B4, 0x0129, 0x013C, 0x02C7,
0x00B8, 0x0161, 0x0113, 0x0123, 0x0167, 0x014A, 0x017E, 0x014B,
0x0100, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x012E,
0x010C, 0x00C9, 0x0118, 0x00CB, 0x0116, 0x00CD, 0x00CE, 0x012A,
0x0110, 0x0145, 0x014C, 0x0136, 0x00D4, 0x00D5, 0x00D6, 0x00D7,
0x00D8, 0x0172, 0x00DA, 0x00DB, 0x00DC, 0x0168, 0x016A, 0x00DF,
0x0101, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x012F,
0x010D, 0x00E9, 0x0119, 0x00EB, 0x0117, 0x00ED, 0x00EE, 0x012B,
0x0111, 0x0146, 0x014D, 0x0137, 0x00F4, 0x00F5, 0x00F6, 0x00F7,
0x00F8, 0x0173, 0x00FA, 0x00FB, 0x00FC, 0x0169, 0x016B, 0x02D9
},
[COMPOUND_TEXT_ISO8859_5_GR] = {
0x00A0, 0x0401, 0x0402, 0x0403, 0x0404, 0x0405, 0x0406, 0x0407,
0x0408, 0x0409, 0x040A, 0x040B, 0x040C, 0x00AD, 0x040E, 0x040F,
0x0410, 0x0411, 0x0412, 0x0413, 0x0414, 0x0415, 0x0416, 0x0417,
0x0418, 0x0419, 0x041A, 0x041B, 0x041C, 0x041D, 0x041E, 0x041F,
0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427,
0x0428, 0x0429, 0x042A, 0x042B, 0x042C, 0x042D, 0x042E, 0x042F,
0x0430, 0x0431, 0x0432, 0x0433, 0x0434, 0x0435, 0x0436, 0x0437,
0x0438, 0x0439, 0x043A, 0x043B, 0x043C, 0x043D, 0x043E, 0x043F,
0x0440, 0x0441, 0x0442, 0x0443, 0x0444, 0x0445, 0x0446, 0x0447,
0x0448, 0x0449, 0x044A, 0x044B, 0x044C, 0x044D, 0x044E, 0x044F,
0x2116, 0x0451, 0x0452, 0x0453, 0x0454, 0x0455, 0x0456, 0x0457,
0x0458, 0x0459, 0x045A, 0x045B, 0x045C, 0x00A7, 0x045E, 0x045F
},
[COMPOUND_TEXT_ISO8859_6_GR] = {
0x00A0, 0x0000, 0x0000, 0x0000, 0x00A4, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x060C, 0x00AD, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x061B, 0x0000, 0x0000, 0x0000, 0x061F,
0x0000, 0x0621, 0x0622, 0x0623, 0x0624, 0x0625, 0x0626, 0x0627,
0x0628, 0x0629, 0x062A, 0x062B, 0x062C, 0x062D, 0x062E, 0x062F,
0x0630, 0x0631, 0x0632, 0x0633, 0x0634, 0x0635, 0x0636, 0x0637,
0x0638, 0x0639, 0x063A, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0640, 0x0641, 0x0642, 0x0643, 0x0644, 0x0645, 0x0646, 0x0647,
0x0648, 0x0649, 0x064A, 0x064B, 0x064C, 0x064D, 0x064E, 0x064F,
0x0650, 0x0651, 0x0652, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000
},
[COMPOUND_TEXT_ISO8859_7_GR] = {
0x00A0, 0x2018, 0x2019, 0x00A3, 0x20AC, 0x20AF, 0x00A6, 0x00A7,
0x00A8, 0x00A9, 0x037A, 0x00AB, 0x00AC, 0x00AD, 0x0000, 0x2015,
0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x0384, 0x0385, 0x0386, 0x00B7,
0x0388, 0x0389, 0x038A, 0x00BB, 0x038C, 0x00BD, 0x038E, 0x038F,
0x0390, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397,
0x0398, 0x0399, 0x039A, 0x039B, 0x039C, 0x039D, 0x039E, 0x039F,
0x03A0, 0x03A1, 0x0000, 0x03A3, 0x03A4, 0x03A5, 0x03A6, 0x03A7,
0x03A8, 0x03A9, 0x03AA, 0x03AB, 0x03AC, 0x03AD, 0x03AE, 0x03AF,
0x03B0, 0x03B1, 0x03B2, 0x03B3, 0x03B4, 0x03B5, 0x03B6, 0x03B7,
0x03B8, 0x03B9, 0x03BA, 0x03BB, 0x03BC, 0x03BD, 0x03BE, 0x03BF,
0x03C0, 0x03C1, 0x03C2, 0x03C3, 0x03C4, 0x03C5, 0x03C6, 0x03C7,
0x03C8, 0x03C9, 0x03CA, 0x03CB, 0x03CC, 0x03CD, 0x03CE, 0x0000
},
[COMPOUND_TEXT_ISO8859_8_GR] = {
0x00A0, 0x0000, 0x00A2, 0x00A3, 0x00A4, 0x00A5, 0x00A6, 0x00A7,
0x00A8, 0x00A9, 0x00D7, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF,
0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5, 0x00B6, 0x00B7,
0x00B8, 0x00B9, 0x00F7, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x2017,
0x05D0, 0x05D1, 0x05D2, 0x05D3, 0x05D4, 0x05D5, 0x05D6, 0x05D7,
0x05D8, 0x05D9, 0x05DA, 0x05DB, 0x05DC, 0x05DD, 0x05DE, 0x05DF,
0x05E0, 0x05E1, 0x05E2, 0x05E3, 0x05E4, 0x05E5, 0x05E6, 0x05E7,
0x05E8, 0x05E9, 0x05EA, 0x0000, 0x0000, 0x200E, 0x200F, 0x0000
},
[COMPOUND_TEXT_ISO8859_9_GR] = {
0x00A0, 0x00A1, 0x00A2, 0x00A3, 0x00A4, 0x00A5, 0x00A6, 0x00A7,
0x00A8, 0x00A9, 0x00AA, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF,
0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5, 0x00B6, 0x00B7,
0x00B8, 0x00B9, 0x00BA, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00BF,
0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x00C7,
0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF,
0x011E, 0x00D1, 0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x00D7,
0x00D8, 0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x0130, 0x015E, 0x00DF,
0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x00E7,
0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x00EC, 0x00ED, 0x00EE, 0x00EF,
0x011F, 0x00F1, 0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x00F7,
0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x0131, 0x015F, 0x00FF
},
[COMPOUND_TEXT_ISO8859_10_GR] = {
0x00A0, 0x0104, 0x0112, 0x0122, 0x012A, 0x0128, 0x0136, 0x00A7,
0x013B, 0x0110, 0x0160, 0x0166, 0x017D, 0x00AD, 0x016A, 0x014A,
0x00B0, 0x0105, 0x0113, 0x0123, 0x012B, 0x0129, 0x0137, 0x00B7,
0x013C, 0x0111, 0x0161, 0x0167, 0x017E, 0x2015, 0x016B, 0x014B,
0x0100, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x012E,
0x010C, 0x00C9, 0x0118, 0x00CB, 0x0116, 0x00CD, 0x00CE, 0x00CF,
0x00D0, 0x0145, 0x014C, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x0168,
0x00D8, 0x0172, 0x00DA, 0x00DB, 0x00DC, 0x00DD, 0x00DE, 0x00DF,
0x0101, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x012F,
0x010D, 0x00E9, 0x0119, 0x00EB, 0x0117, 0x00ED, 0x00EE, 0x00EF,
0x00F0, 0x0146, 0x014D, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x0169,
0x00F8, 0x0173, 0x00FA, 0x00FB, 0x00FC, 0x00FD, 0x00FE, 0x0138
},
[COMPOUND_TEXT_ISO8859_13_GR] = {
0x00A0, 0x201D, 0x00A2, 0x00A3, 0x00A4, 0x201E, 0x00A6, 0x00A7,
0x00D8, 0x00A9, 0x0156, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00C6,
0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x201C, 0x00B5, 0x00B6, 0x00B7,
0x00F8, 0x00B9, 0x0157, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x00E6,
0x0104, 0x012E, 0x0100, 0x0106, 0x00C4, 0x00C5, 0x0118, 0x0112,
0x010C, 0x00C9, 0x0179, 0x0116, 0x0122, 0x0136, 0x012A, 0x013B,
0x0160, 0x0143, 0x0145, 0x00D3, 0x014C, 0x00D5, 0x00D6, 0x00D7,
0x0172, 0x0141, 0x015A, 0x016A, 0x00DC, 0x017B, 0x017D, 0x00DF,
0x0105, 0x012F, 0x0101, 0x0107, 0x00E4, 0x00E5, 0x0119, 0x0113,
0x010D, 0x00E9, 0x017A, 0x0117, 0x0123, 0x0137, 0x012B, 0x013C,
0x0161, 0x0144, 0x0146, 0x00F3, 0x014D, 0x00F5, 0x00F6, 0x00F7,
0x0173, 0x0142, 0x015B, 0x016B, 0x00FC, 0x017C, 0x017E, 0x2019
},
[COMPOUND_TEXT_ISO8859_14_GR] = {
0x00A0, 0x1E02, 0x1E03, 0x00A3, 0x010A, 0x010B, 0x1E0A, 0x00A7,
0x1E80, 0x00A9, 0x1E82, 0x1E0B, 0x1EF2, 0x00AD, 0x00AE, 0x0178,
0x1E1E, 0x1E1F, 0x0120, 0x0121, 0x1E40, 0x1E41, 0x00B6, 0x1E56,
0x1E81, 0x1E57, 0x1E83, 0x1E60, 0x1EF3, 0x1E84, 0x1E85, 0x1E61,
0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x00C7,
0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF,
0x0174, 0x00D1, 0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x1E6A,
0x00D8, 0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x00DD, 0x0176, 0x00DF,
0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x00E7,
0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x00EC, 0x00ED, 0x00EE, 0x00EF,
0x0175, 0x00F1, 0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x1E6B,
0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x00FD, 0x0177, 0x00FF
},
[COMPOUND_TEXT_ISO8859_15_GR] = {
0x00A0, 0x00A1, 0x00A2, 0x00A3, 0x20AC, 0x00A5, 0x0160, 0x00A7,
0x0161, 0x00A9, 0x00AA, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF,
0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x017D, 0x00B5, 0x00B6, 0x00B7,
0x017E, 0x00B9, 0x00BA, 0x00BB, 0x0152, 0x0153, 0x0178, 0x00BF,
0x00C0, 0x00C1, 0x00C2, 0x00C3, 0x00C4, 0x00C5, 0x00C6, 0x00C7,
0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF,
0x00D0, 0x00D1, 0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D6, 0x00D7,
0x00D8, 0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x00DD, 0x00DE, 0x00DF,
0x00E0, 0x00E1, 0x00E2, 0x00E3, 0x00E4, 0x00E5, 0x00E6, 0x00E7,
0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x00EC, 0x00ED, 0x00EE, 0x00EF,
0x00F0, 0x00F1, 0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F6, 0x00F7,
0x00F8, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x00FD, 0x00FE, 0x00FF
},
[COMPOUND_TEXT_ISO8859_16_GR] = {
0x00A0, 0x0104, 0x0105, 0x0141, 0x20AC, 0x201E, 0x0160, 0x00A7,
0x0161, 0x00A9, 0x0218, 0x00AB, 0x0179, 0x00AD, 0x017A, 0x017B,
0x00B0, 0x00B1, 0x010C, 0x0142, 0x017D, 0x201D, 0x00B6, 0x00B7,
0x017E, 0x010D, 0x0219, 0x00BB, 0x0152, 0x0153, 0x0178, 0x017C,
0x00C0, 0x00C1, 0x00C2, 0x0102, 0x00C4, 0x0106, 0x00C6, 0x00C7,
0x00C8, 0x00C9, 0x00CA, 0x00CB, 0x00CC, 0x00CD, 0x00CE, 0x00CF,
0x0110, 0x0143, 0x00D2, 0x00D3, 0x00D4, 0x0150, 0x00D6, 0x015A,
0x0170, 0x00D9, 0x00DA, 0x00DB, 0x00DC, 0x0118, 0x021A, 0x00DF,
0x00E0, 0x00E1, 0x00E2, 0x0103, 0x00E4, 0x0107, 0x00E6, 0x00E7,
0x00E8, 0x00E9, 0x00EA, 0x00EB, 0x00EC, 0x00ED, 0x00EE, 0x00EF,
0x0111, 0x0144, 0x00F2, 0x00F3, 0x00F4, 0x0151, 0x00F6, 0x015B,
0x0171, 0x00F9, 0x00FA, 0x00FB, 0x00FC, 0x0119, 0x021B, 0x00FF
},
[COMPOUND_TEXT_JIS_X0201_GR] = {
0x0000, 0x3002, 0x300C, 0x300D, 0x3001, 0x30FB, 0x30F2, 0x30A1,
0x30A3, 0x30A5, 0x30A7, 0x30A9, 0x30E3, 0x30E5, 0x30E7, 0x30C3,
0x30FC, 0x30A2, 0x30A4, 0x30A6, 0x30A8, 0x30AA, 0x30AB, 0x30AD,
0x30AF, 0x30B1, 0x30B3, 0x30B5, 0x30B7, 0x30B9, 0x30BB, 0x30BD,
0x30BF, 0x30C1, 0x30C4, 0x30C6, 0x30C8, 0x30CA, 0x30CB, 0x30CC,
0x30CD, 0x30CE, 0x30CF, 0x30D2, 0x30D5, 0x30D8, 0x30DB, 0x30DE,
0x30DF, 0x30E0, 0x30E1, 0x30E2, 0x30E4, 0x30E6, 0x30E8, 0x30E9,
0x30EA, 0x30EB, 0x30EC, 0x30ED, 0x30EF, 0x30F3, 0x309B, 0x309C,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000
},
[COMPOUND_TEXT_JIS_X0201_GL] = {
0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
0x0058, 0x0059, 0x005A, 0x005B, 0x00A5, 0x005D, 0x005E, 0x005F,
0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
0x0078, 0x0079, 0x007A, 0x007B, 0x007C, 0x007D, 0x203E, 0x007F
},
};
struct compound_text_state {
const char *in; // Current input iterator
const char *end; // End of input
int *out; // Current result iterator
int gl_encoding; // Current GL encoding or -N for unknown N-octet
int gr_encoding; // Current GR encoding or -N for unknown N-octet
};
static bool
compound_text_peek(struct compound_text_state *s, unsigned char *c)
{
if (s->in >= s->end)
return false;
*c = *s->in;
return true;
}
static bool
compound_text_read(struct compound_text_state *s, unsigned char *c)
{
if (!compound_text_peek(s, c))
return false;
s->in++;
return true;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static bool
compound_text_skip_I(struct compound_text_state *s, unsigned char *c)
{
if (*c >= 0x21 && *c <= 0x23) {
while (compound_text_peek(s, c) && *c >= 0x20 && *c <= 0x2f)
compound_text_read(s, c);
if (!compound_text_read(s, c))
return false;
}
return true;
}
static bool
compound_text_unknown_1(struct compound_text_state *s, unsigned char c,
int *encoding)
{
if (!compound_text_skip_I(s, &c))
return false;
else if (c >= 0x40 && c <= 0x7e)
*encoding = -1;
else
return false;
return true;
}
static bool
compound_text_unknown_N(struct compound_text_state *s, unsigned char c,
int *encoding)
{
if (!compound_text_skip_I(s, &c))
return false;
else if (c >= 0x40 && c <= 0x5f)
*encoding = -2;
else if (c >= 0x60 && c <= 0x6f)
*encoding = -3;
else if (c >= 0x70 && c <= 0x7e)
return false; // "4 or more"
else
return false;
return true;
}
static bool
compound_text_utf8(struct compound_text_state *s)
{
// The specification isn't entirely clear about termination,
// let's be naïve and careful about what we accept
while (s->in + 3 <= s->end) {
if (s->in[0] == 0x1b && s->in[1] == 0x25 && s->in[2] == 0x40) {
s->in += 3;
return true;
}
gunichar r = g_utf8_get_char_validated(s->in, s->end - s->in);
if (r == (gunichar) -1 || r == (gunichar) -2)
return false;
// Don't allow circumventing the rules with this stupid mode
if (r < 0x20 && r != '\t' && r != '\n')
r = 0xFFFD;
s->in = g_utf8_next_char(s->in);
*s->out++ = r;
}
return false;
}
static bool
compound_text_ESC(struct compound_text_state *s)
{
unsigned char c;
if (!compound_text_read(s, &c)) {
return false;
} else if (c == 0x28 /* GL 94 */) {
if (!compound_text_read(s, &c))
return false;
else if (c == 0x42)
s->gl_encoding = COMPOUND_TEXT_ASCII;
else if (c == 0x4a)
s->gl_encoding = COMPOUND_TEXT_JIS_X0201_GL;
else if (!compound_text_unknown_1(s, c, &s->gl_encoding))
return false;
} else if (c == 0x29 /* GR 94 */) {
if (!compound_text_read(s, &c))
return false;
else if (c == 0x49)
s->gr_encoding = COMPOUND_TEXT_JIS_X0201_GR;
else if (!compound_text_unknown_1(s, c, &s->gr_encoding))
return false;
} else if (c == 0x2d /* GR 96 */) {
if (!compound_text_read(s, &c))
return false;
else if (c == 0x41)
s->gr_encoding = COMPOUND_TEXT_ISO8859_1_GR;
else if (c == 0x42)
s->gr_encoding = COMPOUND_TEXT_ISO8859_2_GR;
else if (c == 0x43)
s->gr_encoding = COMPOUND_TEXT_ISO8859_3_GR;
else if (c == 0x44)
s->gr_encoding = COMPOUND_TEXT_ISO8859_4_GR;
else if (c == 0x46)
s->gr_encoding = COMPOUND_TEXT_ISO8859_7_GR;
else if (c == 0x47)
s->gr_encoding = COMPOUND_TEXT_ISO8859_6_GR;
else if (c == 0x48)
s->gr_encoding = COMPOUND_TEXT_ISO8859_8_GR;
else if (c == 0x4c)
s->gr_encoding = COMPOUND_TEXT_ISO8859_5_GR;
else if (c == 0x4d)
s->gr_encoding = COMPOUND_TEXT_ISO8859_9_GR;
else if (c == 0x56)
s->gr_encoding = COMPOUND_TEXT_ISO8859_10_GR;
else if (c == 0x59)
s->gr_encoding = COMPOUND_TEXT_ISO8859_13_GR;
else if (c == 0x5f)
s->gr_encoding = COMPOUND_TEXT_ISO8859_14_GR;
else if (c == 0x62)
s->gr_encoding = COMPOUND_TEXT_ISO8859_15_GR;
else if (c == 0x66)
s->gr_encoding = COMPOUND_TEXT_ISO8859_16_GR;
else if (!compound_text_unknown_1(s, c, &s->gr_encoding))
return false;
} else if (c == 0x24 /* ^N */) {
if (!compound_text_read(s, &c)) {
return false;
} else if (c == 0x28 /* GL 94^N */) {
if (!compound_text_read(s, &c) ||
!compound_text_unknown_N(s, c, &s->gl_encoding))
return false;
} else if (c == 0x29 /* GR 94^N */) {
if (!compound_text_read(s, &c) ||
!compound_text_unknown_N(s, c, &s->gr_encoding))
return false;
} else {
return false;
}
} else if (c == 0x25 /* Non-Standard Character Set Encodings */) {
if (!compound_text_read(s, &c))
return false;
if (c == 0x47 /* from version 1.1.xf86.1 */)
return compound_text_utf8(s);
if (c != 0x2f || !compound_text_read(s, &c) || c < 0x30 || c > 0x34)
return false;
if (!compound_text_read(s, &c) || !(c & 0x80))
return false;
int skip_h = c - 128;
if (!compound_text_read(s, &c) || !(c & 0x80))
return false;
int skip_l = c - 128;
for (int skip = skip_h << 8 | skip_l; skip--; ) {
if (!compound_text_read(s, &c))
return false;
}
// TODO: this would deserve more obvious handling,
// we're replacing an entire sequence with just one character.
// For that, we'd need to parse the sequence, though.
*s->out++ = 0xFFFD;
} else if (c == 0x23 /* Extensions, starting control sequences */) {
// NOTE: major version = c - 0x20 + 1
if (!compound_text_read(s, &c) || c < 0x20 || c > 0x2f)
return false;
if (!compound_text_read(s, &c))
return false;
else if (c == 0x30)
return false; // not supported: ignoring extensions is OK
else if (c == 0x31)
return false; // not supported: ignoring extensions is not OK
else
return false;
} else if (c >= 0x20 && c <= 0x2f /* extension, Intermediate */) {
return false; // not supported
} else if (c >= 0x30 && c <= 0x7e /* extension, Final */) {
return false; // not supported
}
return true;
}
static bool
compound_text_CSI(struct compound_text_state *s)
{
unsigned char c;
if (!compound_text_read(s, &c)) {
return false;
} else if (c == 0x31) {
if (!compound_text_read(s, &c) || c != 0x5d)
return false;
*s->out++ = 0x202A; // LRE
} else if (c == 0x32) {
if (!compound_text_read(s, &c) || c != 0x5d)
return false;
*s->out++ = 0x202B; // RLE
} else if (c == 0x5d) {
*s->out++ = 0x202C; // PDF
} else if (c >= 0x30 && c <= 0x3f /* extension, P */) {
return false; // not supported
} else if (c >= 0x20 && c <= 0x2f /* extension, Intermediate */) {
return false; // not supported
} else if (c >= 0x40 && c <= 0x7e /* extension, Final */) {
return false; // not supported
} else {
return false;
}
return true;
}
static bool
compound_text_GL(struct compound_text_state *s, unsigned char c)
{
if (s->gl_encoding < 0 ||
!(*s->out = compound_text_tables[s->gl_encoding][c - 0x20]))
*s->out = 0xFFFD;
for (int i = 0; --i > s->gl_encoding; ) {
if (!compound_text_read(s, &c) || c < 0x20 || c >= 0x80)
return false;
}
s->out++;
return true;
}
static bool
compound_text_GR(struct compound_text_state *s, unsigned char c)
{
if (s->gr_encoding < 0 ||
!(*s->out = compound_text_tables[s->gr_encoding][c - 0xa0]))
*s->out = 0xFFFD;
for (int i = 0; --i > s->gr_encoding; ) {
if (!compound_text_read(s, &c) || c < 0xa0)
return false;
}
s->out++;
return true;
}
int *
compound_text_to_ucs4(const char *compound_text, size_t length)
{
// This is a good approximation, as well as the upper bound
int *result = calloc(sizeof *result, length + 1);
struct compound_text_state s = {
.in = compound_text, .end = compound_text + length, .out = result,
.gl_encoding = COMPOUND_TEXT_ASCII,
.gr_encoding = COMPOUND_TEXT_ISO8859_1_GR,
};
unsigned char c;
while (compound_text_read(&s, &c) && c != 0) {
bool ok = true;
if (c == '\t' || c == '\n')
*s.out++ = c;
else if (c == 0x1b)
ok = compound_text_ESC(&s);
else if (c == 0x9b)
ok = compound_text_CSI(&s);
else if ((c & ~0x80) < 0x20 /* C0, C1 */)
*s.out++ = 0xFFFD;
else if (c < 0x80)
ok = compound_text_GL(&s, c);
else
ok = compound_text_GR(&s, c);
if (!ok) {
// TODO: consider returning partial results
free(result);
return NULL;
}
}
*s.out++ = 0;
return result;
}
#if COMPOUND_TEXT_SELFTEST
// Build with -DCOMPOUND_TEXT_SELFTEST `pkg-config --cflags --libs x11 glib-2.0`
#include <X11/Xlib.h>
#include <X11/Xutil.h>
int
main(void)
{
Display *dpy = XOpenDisplay(NULL); GString *s = g_string_new("");
while (1) {
g_string_truncate(s, 0);
for (gsize i = 0; i < 10; i++) {
int c = rand() & 0x10FFFF;
if ((c < 0xD800 || c > 0xDFFF) && // GLib rejects surrogates
(c != 0x9b) && // Xlib inserts a lone CSI (!)
(c >= 0x20)) // not allowed or disruptive
g_string_append_unichar(s, c);
}
XTextProperty prop;
Xutf8TextListToTextProperty(dpy, (char **) &s->str, 1,
XCompoundTextStyle, &prop);
int *ucs4 = NULL; char *x = NULL;
if (!(ucs4 = compound_text_to_ucs4((char *) prop.value, prop.nitems)))
g_printerr("parse error '%s' -> '%s'\n", s->str, prop.value);
else if (!(x = g_ucs4_to_utf8((gunichar *) ucs4, -1, NULL, NULL, NULL)))
g_printerr("total failure: %s\n", prop.value);
free(ucs4); free(x); XFree(prop.value);
}
}
#endif

21
compound-text.h Normal file
View File

@@ -0,0 +1,21 @@
//
// compound-text.h: partial X11 COMPOUND_TEXT to UCS-4 transcoder
//
// Copyright (c) 2020, Přemysl Eric Janouch <p@janouch.name>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
#include <stdlib.h>
// Convert an X11 COMPOUND_TEXT string to a NUL-terminated UCS4 sequence
int *compound_text_to_ucs4(const char *compound_text, size_t length);

View File

@@ -1,3 +1,3 @@
#define PROJECT_NAME "@CMAKE_PROJECT_NAME@"
#define PROJECT_VERSION "@project_VERSION@"
#define PROJECT_NAME "@PROJECT_NAME@"
#define PROJECT_VERSION "@PROJECT_VERSION@"
#define SHARE_DIR "@project_SHARE_DIR@"

View File

@@ -4,8 +4,8 @@ add_project_arguments(['--vapidir', meson.current_source_dir()],
language: 'vala')
conf = configuration_data()
conf.set('CMAKE_PROJECT_NAME', meson.project_name())
conf.set('project_VERSION', meson.project_version())
conf.set('PROJECT_NAME', meson.project_name())
conf.set('PROJECT_VERSION', meson.project_version())
configure_file(
input : 'config.h.in',
output : 'config.h',
@@ -19,14 +19,13 @@ dependencies = [
dependency('gio-unix-2.0'),
dependency('gee-0.8'),
dependency('sqlite3'),
dependency('x11'),
dependency('xext'),
dependency('xextproto'),
dependency('xcb'),
dependency('xcb-sync'),
]
gui = static_library('gui', 'gui.vala', 'config.vapi',
install : false,
dependencies : dependencies)
executable('wdmtg', 'wdmtg.c',
executable('wdmtg', 'wdmtg.c', 'compound-text.c',
install : true,
link_with : [gui],
dependencies : [dependencies])

721
wdmtg.c
View File

@@ -15,10 +15,6 @@
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
#include <gtk/gtk.h>
#include <glib.h>
#include <sqlite3.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdarg.h>
@@ -28,14 +24,16 @@
#include <sys/socket.h>
#include <sys/un.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include <X11/extensions/sync.h>
#include <gtk/gtk.h>
#include <glib.h>
#include <sqlite3.h>
#include <xcb/xcb.h>
#include <xcb/sync.h>
#include "config.h"
#include "gui.h"
#include "compound-text.h"
// --- Utilities ---------------------------------------------------------------
@@ -67,11 +65,28 @@ get_xdg_config_dirs(void)
return (GStrv) g_ptr_array_free(paths, false);
}
static GString *
latin1_to_utf8(const gchar *latin1, gsize len)
{
GString *s = g_string_new(NULL);
while (len--) {
guchar c = *latin1++;
if (c < 0x80) {
g_string_append_c(s, c);
} else {
g_string_append_c(s, 0xC0 | (c >> 6));
g_string_append_c(s, 0x80 | (c & 0x3F));
}
}
return s;
}
// --- Globals -----------------------------------------------------------------
struct event {
gint64 timestamp; // When the event happened
gchar *title; // Current title at the time
gchar *class; // Current class at the time
gboolean idle; // Whether the user is idle
};
@@ -79,6 +94,7 @@ static void
event_free(struct event *self)
{
g_free(self->title);
g_free(self->class);
g_slice_free(struct event, self);
}
@@ -86,100 +102,209 @@ event_free(struct event *self)
struct {
GAsyncQueue *queue; // Async queue of `struct event`
sqlite3 *db; // Event database
sqlite3_stmt *add_event; // Prepared statement: add event
} g;
struct {
Display *dpy; // X display handle
xcb_connection_t *X; // X display handle
xcb_screen_t *screen; // X screen information
GThread *thread; // Worker thread
Atom net_active_window; // _NET_ACTIVE_WINDOW
Atom net_wm_name; // _NET_WM_NAME
xcb_atom_t atom_net_active_window; // _NET_ACTIVE_WINDOW
xcb_atom_t atom_net_wm_name; // _NET_WM_NAME
xcb_atom_t atom_utf8_string; // UTF8_STRING
xcb_atom_t atom_compound_text; // COMPOUND_TEXT
// Window title tracking
gchar *current_title; // Current window title or NULL
Window current_window; // Current window
gchar *current_class; // Current window class or NULL
xcb_window_t current_window; // Current window
gboolean current_idle; // Current idle status
// XSync activity tracking
int xsync_base_event_code; // XSync base event code
XSyncCounter idle_counter; // XSync IDLETIME counter
XSyncValue idle_timeout; // Idle timeout
const xcb_query_extension_reply_t *sync; // Sync extension
xcb_sync_counter_t idle_counter; // Sync IDLETIME counter
xcb_sync_int64_t idle_timeout; // Idle timeout
XSyncAlarm idle_alarm_inactive; // User is inactive
XSyncAlarm idle_alarm_active; // User is active
xcb_sync_alarm_t idle_alarm_inactive; // User is inactive
xcb_sync_alarm_t idle_alarm_active; // User is active
} gen;
// --- X helpers ---------------------------------------------------------------
// --- XCB helpers -------------------------------------------------------------
static XSyncCounter
static xcb_atom_t
intern_atom(const char *atom)
{
xcb_intern_atom_reply_t *iar = xcb_intern_atom_reply(gen.X,
xcb_intern_atom(gen.X, false, strlen(atom), atom), NULL);
xcb_atom_t result = iar ? iar->atom : XCB_NONE;
free(iar);
return result;
}
static xcb_sync_counter_t
find_counter(xcb_sync_list_system_counters_reply_t *slsr, const char *name)
{
// FIXME: https://gitlab.freedesktop.org/xorg/lib/libxcb/-/issues/36
const size_t xcb_sync_systemcounter_t_len = 14;
xcb_sync_systemcounter_iterator_t slsi =
xcb_sync_list_system_counters_counters_iterator(slsr);
while (slsi.rem--) {
xcb_sync_systemcounter_t *counter = slsi.data;
char *counter_name = (char *) counter + xcb_sync_systemcounter_t_len;
if (!strncmp(counter_name, name, counter->name_len) &&
!name[counter->name_len])
return counter->counter;
slsi.data = (void *) counter +
((xcb_sync_systemcounter_t_len + counter->name_len + 3) & ~3);
}
return XCB_NONE;
}
static xcb_sync_counter_t
get_counter(const char *name)
{
int n;
XSyncSystemCounter *counters = XSyncListSystemCounters(gen.dpy, &n);
XSyncCounter counter = None;
while (n--) {
if (!strcmp(counters[n].name, name))
counter = counters[n].counter;
xcb_sync_list_system_counters_reply_t *slsr =
xcb_sync_list_system_counters_reply(gen.X,
xcb_sync_list_system_counters(gen.X), NULL);
xcb_sync_counter_t counter = XCB_NONE;
if (slsr) {
counter = find_counter(slsr, name);
free(slsr);
}
XSyncFreeSystemCounterList(counters);
return counter;
}
static char *
x_text_property_to_utf8(XTextProperty *prop)
{
Atom utf8_string = XInternAtom(gen.dpy, "UTF8_STRING", true);
if (prop->encoding == utf8_string)
return g_strdup((char *) prop->value);
// --- X helpers ---------------------------------------------------------------
int n = 0;
char **list = NULL;
if (XmbTextPropertyToTextList(gen.dpy, prop, &list, &n) >= Success
&& n > 0 && *list) {
char *result = g_locale_to_utf8(*list, -1, NULL, NULL, NULL);
XFreeStringList(list);
return result;
static GString *
x_text_property_to_utf8(GString *value, xcb_atom_t encoding)
{
if (encoding == gen.atom_utf8_string)
return value;
if (encoding == XCB_ATOM_STRING) {
// Could use g_convert() but this will certainly never fail
GString *utf8 = latin1_to_utf8(value->str, value->len);
g_string_free(value, true);
return utf8;
}
// COMPOUND_TEXT doesn't deserve support for multiple NUL-separated items
int *ucs4 = NULL;
if (encoding == gen.atom_compound_text &&
(ucs4 = compound_text_to_ucs4((char *) value->str, value->len))) {
g_string_free(value, true);
glong len = 0;
gchar *utf8 = g_ucs4_to_utf8((gunichar *) ucs4, -1, NULL, &len, NULL);
free(ucs4);
// malloc failure or, rather theoretically, an out of range codepoint
if (utf8) {
value = g_string_new_len(utf8, len);
free(utf8);
return value;
}
}
g_string_free(value, true);
return NULL;
}
static char *
x_text_property(Window window, Atom atom)
static GString *
x_text_property(xcb_window_t window, xcb_atom_t property)
{
XTextProperty name;
XGetTextProperty(gen.dpy, window, &name, atom);
if (!name.value)
GString *buffer = g_string_new(NULL);
xcb_atom_t type = XCB_NONE;
uint32_t offset = 0;
xcb_get_property_reply_t *gpr = NULL;
while ((gpr = xcb_get_property_reply(gen.X,
xcb_get_property(gen.X, false /* delete */, window,
property, XCB_GET_PROPERTY_TYPE_ANY, offset, 0x8000), NULL))) {
if (gpr->format != 8 || (type && gpr->type != type)) {
free(gpr);
break;
}
int len = xcb_get_property_value_length(gpr);
g_string_append_len(buffer, xcb_get_property_value(gpr), len);
offset += len >> 2;
type = gpr->type;
bool last = !gpr->bytes_after;
free(gpr);
if (last)
return x_text_property_to_utf8(buffer, type);
}
g_string_free(buffer, true);
return NULL;
char *result = x_text_property_to_utf8(&name);
XFree(name.value);
return result;
}
// --- X error handling --------------------------------------------------------
// --- Async Queue Source ------------------------------------------------------
static XErrorHandler g_default_x_error_handler;
static int
on_x_error(Display *dpy, XErrorEvent *ee)
static gboolean
async_queue_source_prepare(G_GNUC_UNUSED GSource *source,
G_GNUC_UNUSED gint *timeout_)
{
// This just is going to happen since those windows aren't ours
if (ee->error_code == BadWindow)
return 0;
return g_default_x_error_handler(dpy, ee);
return g_async_queue_length(g.queue) > 0;
}
// --- Application -------------------------------------------------------------
static gboolean
async_queue_source_dispatch(G_GNUC_UNUSED GSource *source,
GSourceFunc callback, gpointer user_data)
{
// I don't want to call it once per message, prefer batch processing
if (callback)
return callback(user_data);
return G_SOURCE_CONTINUE;
}
static GSource *
async_queue_source_new(void)
{
static GSourceFuncs funcs = {
.prepare = async_queue_source_prepare,
.check = NULL,
.dispatch = async_queue_source_dispatch,
.finalize = NULL,
};
GSource *source = g_source_new(&funcs, sizeof *source);
g_source_set_name(source, "AsyncQueueSource");
return source;
}
// --- Generator ---------------------------------------------------------------
static void
push_event(void) {
struct event *event = g_slice_new0(struct event);
event->timestamp = g_get_real_time();
event->title = g_strdup(gen.current_title);
event->class = g_strdup(gen.current_class);
event->idle = gen.current_idle;
g_async_queue_push(g.queue, event);
// This is the best thing GLib exposes (GWakeUp is internal)
g_main_context_wakeup(g_main_context_default());
}
static char *
x_window_title(Window window)
x_window_title(xcb_window_t window)
{
char *title;
if (!(title = x_text_property(window, gen.net_wm_name))
&& !(title = x_text_property(window, XA_WM_NAME)))
title = g_strdup("broken");
return title;
GString *title;
if (!(title = x_text_property(window, gen.atom_net_wm_name))
&& !(title = x_text_property(window, XCB_ATOM_WM_NAME)))
return g_strdup("");
return g_string_free(title, false);
}
static bool
@@ -192,123 +317,158 @@ update_window_title(char *new_title)
return changed;
}
static void
push_event(void) {
struct event *event = g_slice_new0(struct event);
event->timestamp = g_get_real_time();
event->title = g_strdup(gen.current_title);
event->idle = gen.current_idle;
g_async_queue_push(g.queue, event);
static char *
x_window_class(xcb_window_t window)
{
GString *title;
if (!(title = x_text_property(window, XCB_ATOM_WM_CLASS)))
return NULL;
// First is an "instance name", followed by a NUL and a "class name".
// Strongly prefer Firefox/Thunderbird over Navigator/Mail.
size_t skip = strlen(title->str);
if (++skip >= title->len)
return g_string_free(title, true);
g_string_erase(title, 0, skip);
return g_string_free(title, false);
}
static bool
update_window_class(char *new_class)
{
bool changed = !gen.current_class != !new_class
|| (new_class && strcmp(gen.current_class, new_class));
free(gen.current_class);
gen.current_class = new_class;
return changed;
}
static void
update_current_window(void)
{
Window root = DefaultRootWindow(gen.dpy);
Atom dummy_type; int dummy_format; unsigned long nitems, dummy_bytes;
unsigned char *p = NULL;
if (XGetWindowProperty(gen.dpy, root, gen.net_active_window,
0L, 1L, false, XA_WINDOW, &dummy_type, &dummy_format,
&nitems, &dummy_bytes, &p) != Success)
xcb_get_property_reply_t *gpr = xcb_get_property_reply(gen.X,
xcb_get_property(gen.X, false /* delete */, gen.screen->root,
gen.atom_net_active_window, XCB_ATOM_WINDOW, 0, 1), NULL);
if (!gpr)
return;
char *new_title = NULL;
if (nitems) {
Window active_window = *(Window *) p;
XFree(p);
char *new_class = NULL;
if (xcb_get_property_value_length(gpr)) {
xcb_window_t active_window =
*(xcb_window_t *) xcb_get_property_value(gpr);
const uint32_t disable[] = { 0 };
if (gen.current_window != active_window && gen.current_window)
XSelectInput(gen.dpy, gen.current_window, 0);
(void) xcb_change_window_attributes(gen.X,
gen.current_window, XCB_CW_EVENT_MASK, disable);
const uint32_t enable[] = { XCB_EVENT_MASK_PROPERTY_CHANGE };
(void) xcb_change_window_attributes(gen.X,
active_window, XCB_CW_EVENT_MASK, enable);
XSelectInput(gen.dpy, active_window, PropertyChangeMask);
new_title = x_window_title(active_window);
new_class = x_window_class(active_window);
gen.current_window = active_window;
}
if (update_window_title(new_title)) {
printf("Window changed: %s\n",
gen.current_title ? gen.current_title : "(none)");
free(gpr);
// We need to absorb both pointers, so we need a bitwise OR
if (update_window_title(new_title) |
update_window_class(new_class))
push_event();
}
}
static void
on_x_property_notify(XPropertyEvent *ev)
on_x_property_notify(const xcb_property_notify_event_t *ev)
{
// This is from the EWMH specification, set by the window manager
if (ev->atom == gen.net_active_window) {
if (ev->atom == gen.atom_net_active_window) {
update_current_window();
} else if (ev->window == gen.current_window &&
ev->atom == gen.net_wm_name) {
if (update_window_title(x_window_title(ev->window))) {
printf("Title changed: %s\n", gen.current_title);
ev->atom == gen.atom_net_wm_name) {
if (update_window_title(x_window_title(ev->window)))
push_event();
}
}
}
static void
set_idle_alarm(XSyncAlarm *alarm, XSyncTestType test, XSyncValue value)
set_idle_alarm(xcb_sync_alarm_t *alarm, xcb_sync_testtype_t test,
xcb_sync_int64_t value)
{
XSyncAlarmAttributes attr;
attr.trigger.counter = gen.idle_counter;
attr.trigger.test_type = test;
attr.trigger.wait_value = value;
XSyncIntToValue(&attr.delta, 0);
// TODO: consider xcb_sync_{change,create}_alarm_aux()
uint32_t values[] = {
gen.idle_counter,
value.hi, value.lo,
test,
0, 0,
};
long flags = XSyncCACounter | XSyncCATestType | XSyncCAValue | XSyncCADelta;
if (*alarm)
XSyncChangeAlarm(gen.dpy, *alarm, flags, &attr);
else
*alarm = XSyncCreateAlarm(gen.dpy, flags, &attr);
xcb_sync_ca_t flags = XCB_SYNC_CA_COUNTER | XCB_SYNC_CA_VALUE |
XCB_SYNC_CA_TEST_TYPE | XCB_SYNC_CA_DELTA;
if (*alarm) {
xcb_sync_change_alarm(gen.X, *alarm, flags, values);
} else {
*alarm = xcb_generate_id(gen.X);
xcb_sync_create_alarm(gen.X, *alarm, flags, values);
}
}
static void
on_x_alarm_notify(XSyncAlarmNotifyEvent *ev)
on_x_alarm_notify(const xcb_sync_alarm_notify_event_t *ev)
{
if (ev->alarm == gen.idle_alarm_inactive) {
printf("User is inactive\n");
gen.current_idle = true;
push_event();
XSyncValue one, minus_one;
XSyncIntToValue(&one, 1);
Bool overflow;
XSyncValueSubtract(&minus_one, ev->counter_value, one, &overflow);
xcb_sync_int64_t minus_one = ev->counter_value;
if (!~(--minus_one.lo))
minus_one.hi--;
// Set an alarm for IDLETIME <= current_idletime - 1
set_idle_alarm(&gen.idle_alarm_active,
XSyncNegativeComparison, minus_one);
XCB_SYNC_TESTTYPE_NEGATIVE_COMPARISON, minus_one);
} else if (ev->alarm == gen.idle_alarm_active) {
printf("User is active\n");
gen.current_idle = false;
push_event();
set_idle_alarm(&gen.idle_alarm_inactive,
XSyncPositiveComparison, gen.idle_timeout);
XCB_SYNC_TESTTYPE_POSITIVE_COMPARISON, gen.idle_timeout);
}
}
static void
process_x11_event(xcb_generic_event_t *ev)
{
int event_code = ev->response_type & 0x7f;
if (!event_code) {
xcb_generic_error_t *err = (xcb_generic_error_t *) ev;
// TODO: report the error
} else if (event_code == XCB_PROPERTY_NOTIFY) {
on_x_property_notify((const xcb_property_notify_event_t *) ev);
} else if (event_code == gen.sync->first_event + XCB_SYNC_ALARM_NOTIFY) {
on_x_alarm_notify((const xcb_sync_alarm_notify_event_t *) ev);
}
}
static gboolean
on_x_ready(G_GNUC_UNUSED gpointer user_data)
{
XEvent ev;
while (XPending(gen.dpy)) {
if (XNextEvent(gen.dpy, &ev))
exit_fatal("XNextEvent returned non-zero");
else if (ev.type == PropertyNotify)
on_x_property_notify(&ev.xproperty);
else if (ev.type == gen.xsync_base_event_code + XSyncAlarmNotify)
on_x_alarm_notify((XSyncAlarmNotifyEvent *) &ev);
xcb_generic_event_t *event;
while ((event = xcb_poll_for_event(gen.X))) {
process_x11_event(event);
free(event);
}
return G_SOURCE_CONTINUE;
(void) xcb_flush(gen.X);
// TODO: some form of error handling, this just silently stops working
return xcb_connection_has_error(gen.X) == 0;
}
static void
generate_events(void)
generator_thread(void)
{
GIOChannel *channel = g_io_channel_unix_new(ConnectionNumber(gen.dpy));
GIOChannel *channel = g_io_channel_unix_new(xcb_get_file_descriptor(gen.X));
GSource *watch = g_io_create_watch(channel, G_IO_IN);
g_source_set_callback(watch, on_x_ready, NULL, NULL);
@@ -318,59 +478,51 @@ generate_events(void)
g_main_loop_run(loop);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
int
main(int argc, char *argv[])
static void
generator_init(void)
{
gboolean show_version = false;
const GOptionEntry options[] = {
{"version", 'V', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE,
&show_version, "output version information and exit", NULL},
{},
};
int which_screen = -1, xcb_error;
gen.X = xcb_connect(NULL, &which_screen);
if ((xcb_error = xcb_connection_has_error(gen.X)))
exit_fatal("cannot open display (code %d)", xcb_error);
GError *error = NULL;
if (!gtk_init_with_args(&argc, &argv, " - activity tracker",
options, NULL, &error))
exit_fatal("%s", error->message);
if (show_version) {
printf(PROJECT_NAME " " PROJECT_VERSION "\n");
return 0;
}
g.queue = g_async_queue_new_full((GDestroyNotify) event_free);
if (!setlocale(LC_CTYPE, ""))
exit_fatal("cannot set locale");
if (!XSupportsLocale())
exit_fatal("locale not supported by Xlib");
XInitThreads();
if (!(gen.dpy = XOpenDisplay(NULL)))
exit_fatal("cannot open display");
gen.net_active_window = XInternAtom(gen.dpy, "_NET_ACTIVE_WINDOW", true);
gen.net_wm_name = XInternAtom(gen.dpy, "_NET_WM_NAME", true);
if (!(gen.atom_net_active_window = intern_atom("_NET_ACTIVE_WINDOW")) ||
!(gen.atom_net_wm_name = intern_atom("_NET_WM_NAME")) ||
!(gen.atom_utf8_string = intern_atom("UTF8_STRING")) ||
!(gen.atom_compound_text = intern_atom("COMPOUND_TEXT")))
exit_fatal("unable to resolve atoms");
// TODO: it is possible to employ a fallback mechanism via XScreenSaver
// by polling the XScreenSaverInfo::idle field, see
// https://www.x.org/releases/X11R7.5/doc/man/man3/Xss.3.html
int dummy;
if (!XSyncQueryExtension(gen.dpy, &gen.xsync_base_event_code, &dummy)
|| !XSyncInitialize(gen.dpy, &dummy, &dummy))
exit_fatal("cannot initialize XSync");
gen.sync = xcb_get_extension_data(gen.X, &xcb_sync_id);
if (!gen.sync->present)
exit_fatal("missing Sync extension");
xcb_generic_error_t *err = NULL;
xcb_sync_initialize_cookie_t sic = xcb_sync_initialize(gen.X,
XCB_SYNC_MAJOR_VERSION, XCB_SYNC_MINOR_VERSION);
xcb_sync_initialize_reply_t *sir =
xcb_sync_initialize_reply(gen.X, sic, &err);
if (!sir)
exit_fatal("failed to initialise Sync extension");
free(sir);
// The idle counter is not guaranteed to exist, only SERVERTIME is
if (!(gen.idle_counter = get_counter("IDLETIME")))
exit_fatal("idle counter is missing");
Window root = DefaultRootWindow(gen.dpy);
XSelectInput(gen.dpy, root, PropertyChangeMask);
XSync(gen.dpy, False);
// TODO: what is the interaction with GTK+ here?
g_default_x_error_handler = XSetErrorHandler(on_x_error);
const xcb_setup_t *setup = xcb_get_setup(gen.X);
xcb_screen_iterator_t setup_iter = xcb_setup_roots_iterator(setup);
while (which_screen--)
xcb_screen_next(&setup_iter);
gen.screen = setup_iter.data;
xcb_window_t root = gen.screen->root;
const uint32_t values[] = { XCB_EVENT_MASK_PROPERTY_CHANGE };
(void) xcb_change_window_attributes(gen.X, root, XCB_CW_EVENT_MASK, values);
GKeyFile *kf = g_key_file_new();
gchar *subpath = g_build_filename(PROJECT_NAME, PROJECT_NAME ".conf", NULL);
@@ -388,60 +540,84 @@ main(int argc, char *argv[])
g_free(subpath);
g_key_file_free(kf);
XSyncIntToValue(&gen.idle_timeout, timeout * 1000);
// Write a start marker so that we can reliably detect interruptions
struct event *event = g_slice_new0(struct event);
event->timestamp = -1;
g_async_queue_push(g.queue, event);
update_current_window();
gint64 timeout_ms = timeout * 1000;
gen.idle_timeout.hi = timeout_ms >> 32;
gen.idle_timeout.lo = timeout_ms;
set_idle_alarm(&gen.idle_alarm_inactive,
XSyncPositiveComparison, gen.idle_timeout);
XCB_SYNC_TESTTYPE_POSITIVE_COMPARISON, gen.idle_timeout);
gchar *data_path =
g_build_filename(g_get_user_data_dir(), PROJECT_NAME, NULL);
g_mkdir_with_parents(data_path, 0755);
(void) xcb_flush(gen.X);
// TODO: how are XCB errors handled? What if the last xcb_flush() fails?
}
// TODO: try exclusivity/invocation either via DBus directly,
// or via GApplication or GtkApplication:
// - GtkApplication calls Gtk.init automatically during "startup" signal,
// Gtk.init doesn't get command line args
// - "inhibiting" makes no sense, it can't be used for mere delays
// - actually, the "query-end" signal
// - should check whether it tries to exit cleanly
// - what is the session manager, do I have it?
// - "register-session" looks useful
// - GTK+ keeps the application running as long as it has windows,
// though I want to keep it running forever
// - g_application_hold(), perhaps
// - so maybe just use GApplication, that will provide more control
// Bind to a control socket, also ensuring only one instance is running
gchar *socket_path = g_build_filename(data_path, "socket", NULL);
struct flock lock =
static void
generator_launch(void)
{
.l_type = F_WRLCK,
.l_start = 0,
.l_whence = SEEK_SET,
.l_len = 0,
};
gen.thread =
g_thread_new("generator", (GThreadFunc) generator_thread, NULL);
}
gchar *lock_path = g_strdup_printf("%s.lock", socket_path);
int lock_fd = open(lock_path, O_RDWR | O_CREAT, 0644);
if (fcntl(lock_fd, F_SETLK, &lock))
exit_fatal("failed to acquire lock: %s", strerror(errno));
unlink(socket_path);
static void
generator_cleanup(void)
{
g_thread_join(gen.thread);
free(gen.current_title);
xcb_disconnect(gen.X);
}
int socket_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (socket_fd < 0)
exit_fatal("%s: %s", socket_path, strerror(errno));
// --- Main --------------------------------------------------------------------
struct sockaddr_un sun;
sun.sun_family = AF_UNIX;
strncpy(sun.sun_path, socket_path, sizeof sun.sun_path);
if (bind(socket_fd, (struct sockaddr *) &sun, sizeof sun))
exit_fatal("%s: %s", socket_path, strerror(errno));
if (listen(socket_fd, 10))
exit_fatal("%s: %s", socket_path, strerror(errno));
static gboolean
on_queue_incoming(G_GNUC_UNUSED gpointer user_data)
{
int rc = 0;
char *errmsg = NULL;
if ((rc = sqlite3_exec(g.db, "BEGIN", NULL, NULL, &errmsg))) {
g_printerr("DB BEGIN error: %s\n", errmsg);
free(errmsg);
return G_SOURCE_CONTINUE;
}
// TODO: there should ideally be a limit to how many things can end up
// in a transaction at once (the amount of dequeues here)
struct event *event = NULL;
while ((event = g_async_queue_try_pop(g.queue))) {
printf("Event: ts: %ld, title: %s, class: %s, idle: %d\n",
event->timestamp, event->title ?: "(none)",
event->class ?: "(none)", event->idle);
if ((rc = sqlite3_bind_int64(g.add_event, 1, event->timestamp)) ||
(rc = sqlite3_bind_text(g.add_event, 2, event->title, -1,
SQLITE_STATIC)) ||
(rc = sqlite3_bind_text(g.add_event, 3, event->class, -1,
SQLITE_STATIC)) ||
(rc = sqlite3_bind_int(g.add_event, 4, event->idle)))
g_printerr("DB bind error: %s\n", sqlite3_errmsg(g.db));
if (((rc = sqlite3_step(g.add_event)) && rc != SQLITE_DONE) ||
(rc = sqlite3_reset(g.add_event)))
g_printerr("DB write error: %s\n", sqlite3_errmsg(g.db));
event_free(event);
}
if ((rc = sqlite3_exec(g.db, "COMMIT", NULL, NULL, &errmsg))) {
g_printerr("DB commit error: %s\n", errmsg);
free(errmsg);
}
return G_SOURCE_CONTINUE;
}
static sqlite3 *
database_init(const gchar *db_path)
{
sqlite3 *db = NULL;
gchar *db_path = g_build_filename(data_path, "db.sqlite", NULL);
int rc = sqlite3_open(db_path, &db);
if (rc != SQLITE_OK)
exit_fatal("%s: %s", db_path, sqlite3_errmsg(db));
@@ -464,11 +640,15 @@ main(int argc, char *argv[])
exit_fatal("%s: %s", db_path, "cannot retrieve user version");
int user_version = sqlite3_column_int(stmt, 0);
if ((rc = sqlite3_finalize(stmt)))
exit_fatal("%s: %s", db_path, "cannot retrieve user version");
if (user_version == 0) {
if ((rc = sqlite3_exec(db, "CREATE TABLE events ("
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"timestamp INTEGER, "
"title TEXT, "
"class TEXT, "
"idle BOOLEAN)", NULL, NULL, &errmsg)))
exit_fatal("%s: %s", db_path, errmsg);
if ((rc = sqlite3_exec(db, "PRAGMA user_version = 1", NULL, NULL,
@@ -480,27 +660,116 @@ main(int argc, char *argv[])
if ((rc = sqlite3_exec(db, "COMMIT", NULL, NULL, &errmsg)))
exit_fatal("%s: %s", db_path, errmsg);
g_free(db_path);
if ((rc = sqlite3_prepare_v2(db,
"INSERT INTO events (timestamp, title, class, idle) "
"VALUES (?, ?, ?, ?)", -1, &g.add_event, NULL)))
exit_fatal("%s: %s", db_path, sqlite3_errmsg(db));
return db;
}
static int
socket_init(const gchar *socket_path)
{
// TODO: try exclusivity/invocation either via DBus directly,
// or via GApplication or GtkApplication:
// - GtkApplication calls Gtk.init automatically during "startup" signal,
// Gtk.init doesn't get command line args
// - "inhibiting" makes no sense, it can't be used for mere delays
// - actually, the "query-end" signal
// - should check whether it tries to exit cleanly
// - what is the session manager, do I have it?
// - "register-session" looks useful
// - GTK+ keeps the application running as long as it has windows,
// though I want to keep it running forever
// - g_application_hold(), perhaps
// - so maybe just use GApplication, that will provide more control
struct flock lock =
{
.l_type = F_WRLCK,
.l_start = 0,
.l_whence = SEEK_SET,
.l_len = 0,
};
gchar *lock_path = g_strdup_printf("%s.lock", socket_path);
int lock_fd = open(lock_path, O_RDWR | O_CREAT, 0644);
if (fcntl(lock_fd, F_SETLK, &lock))
exit_fatal("failed to acquire lock: %s", strerror(errno));
unlink(socket_path);
g_free(lock_path);
g_free(socket_path);
g_free(data_path);
GThread *generator =
g_thread_new("generator", (GThreadFunc) generate_events, NULL);
int socket_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (socket_fd < 0)
exit_fatal("%s: %s", socket_path, strerror(errno));
// TODO: somehow read events from the async queue
// TODO: how in the name of fuck would our custom source wake up a sleeping
// main loop? There is g_main_context_wakeup() but...
// - GWakeUp is internal, apparently
struct sockaddr_un sun;
sun.sun_family = AF_UNIX;
strncpy(sun.sun_path, socket_path, sizeof sun.sun_path);
if (bind(socket_fd, (struct sockaddr *) &sun, sizeof sun))
exit_fatal("%s: %s", socket_path, strerror(errno));
if (listen(socket_fd, 10))
exit_fatal("%s: %s", socket_path, strerror(errno));
// TODO: listen for connections on the control socket
// lock_fd stays open as long as the application is running--leak
return socket_fd;
}
WdmtgWindow *window = wdmtg_window_new_with_db(db);
int
main(int argc, char *argv[])
{
if (!setlocale(LC_CTYPE, ""))
exit_fatal("cannot set locale");
gtk_main();
g_thread_join(generator);
gboolean show_version = false;
const GOptionEntry options[] = {
{"version", 'V', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE,
&show_version, "output version information and exit", NULL},
{},
};
free(gen.current_title);
XCloseDisplay(gen.dpy);
GError *error = NULL;
if (!gtk_init_with_args(&argc, &argv, " - activity tracker",
options, NULL, &error))
exit_fatal("%s", error->message);
if (show_version) {
printf(PROJECT_NAME " " PROJECT_VERSION "\n");
return 0;
}
g.queue = g_async_queue_new_full((GDestroyNotify) event_free);
generator_init();
gchar *data_path =
g_build_filename(g_get_user_data_dir(), PROJECT_NAME, NULL);
g_mkdir_with_parents(data_path, 0755);
// Bind to a control socket, also ensuring only one instance is running.
// We're intentionally not using XDG_RUNTIME_DIR so that what is effectively
// the database lock is right next to the database.
gchar *socket_path = g_build_filename(data_path, "socket", NULL);
int socket_fd = socket_init(socket_path);
g_free(socket_path);
gchar *db_path = g_build_filename(data_path, "db.sqlite", NULL);
g_free(data_path);
g.db = database_init(db_path);
g_free(db_path);
GSource *queue_source = async_queue_source_new();
g_source_set_callback(queue_source, on_queue_incoming, NULL, NULL);
g_source_attach(queue_source, g_main_context_default());
// TODO: listen for connections on the control socket
// - just show/map the window when anything connects at all
WdmtgWindow *window = wdmtg_window_new_with_db(g.db);
generator_launch();
gtk_main();
generator_cleanup();
sqlite3_close(g.db);
close(socket_fd);
return 0;
}

View File

@@ -1 +0,0 @@
// https://github.com/mesonbuild/meson/issues/1195