Initial commit.

This commit is contained in:
Storm Dragon
2026-06-02 17:29:55 -04:00
commit db522d7d66
33 changed files with 4017 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
AllowShortBlocksOnASingleLine: false
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AlwaysBreakBeforeMultilineStrings: false
BasedOnStyle: google
ColumnLimit: 0
IndentWidth: 4
InsertBraces: true
PointerBindsToType: false
SortIncludes: false
SpaceBeforeParens: ControlStatements
+10
View File
@@ -0,0 +1,10 @@
/i38lock
*.o
tags
*.swp
# We recommend building in a subdirectory called build.
# If you chose a different directory name,
# it is up to you to arrange for it to be ignored by git,
# e.g. by listing your directory in .git/info/exclude.
/build
+40
View File
@@ -0,0 +1,40 @@
# Repository Guidelines
## Project Structure & Module Organization
I38Lock is an accessible C screen-locking utility based on i3lock. Core sources live at the repository root: `i3lock.c` handles the main program, while `xcb.c`, `randr.c`, `dpi.c`, `unlock_indicator.c`, and `accessibility_feedback.c` separate display, UI, and screen-reader concerns. Public project headers are under `include/`. The PAM service file is `pam/i38lock`, the manual page is `i38lock.1`, and packaging or CI support lives in `meson/`, `ci/`, and `.github/`.
## Build, Test, and Development Commands
Install the dependencies listed in `README.md`, then build out of tree:
```sh
meson setup build -Dprefix=/usr
ninja -C build
```
To reproduce CI's stricter compile check, configure a fresh build directory:
```sh
CFLAGS="-Wformat -Wformat-security -Wextra -Wno-unused-parameter -Werror" meson setup build-ci
ninja -C build-ci
```
Run `nix develop` first if using the provided Nix development shell. Avoid installing from a development build unless installation behavior is the focus of the change.
## Coding Style & Naming Conventions
The project uses C11 and the checked-in `.clang-format`. Use four-space indentation, braces for control statements, and existing snake_case naming patterns. Format C sources and headers with:
```sh
clang-format-15 -i $(git ls-files '*.c' 'include/*.h')
clang-format-15 --dry-run --Werror $(git ls-files '*.c' 'include/*.h')
```
## Testing Guidelines
There is no automated runtime test suite. Every change must compile with the strict warning flags above. For behavior changes, run `timeout --foreground 30s build/i38lock -n` from an X11 session, verify the affected workflow, then unlock by entering your password and pressing Enter. Install `pam/i38lock` first and keep the watchdog enabled to avoid locking yourself out of an active development session.
## Commit & Pull Request Guidelines
Recent commits use short imperative subjects, often scoped when useful, such as `meson: use explicit_bzero if it is available`. Keep each commit focused. Pull requests should explain the behavior change, include reproduction and verification steps, and link relevant issues. Include `i38lock --version` and environment details for bug fixes. Do not propose image-manipulation features; preprocess background images with external tools.
+234
View File
@@ -0,0 +1,234 @@
2025-10-31 i3lock 2.16
• fix crash when the user changes the XKB configuration
• when started on Wayland, display an error and usage
• switch to clang-format 15 (with InsertBraces)
• fix -Werror=calloc-transposed-args by swapping calloc args
• reword: remove "dynamic" TWM
• update meson setup command in README
• do not increase failed_attempts beyond 999
• i3lock.1 man page: fix acute accent
• declare a development shell in flake.nix
• fix in_dpi variable checking
• meson: use explicit_bzero if it is available
2024-03-16 i3lock 2.15
• unlock indicator: display current keyboard layout when
user types password (with --show-keyboard-layout flag).
2022-06-21 i3lock 2.14.1
• unlock indicator: display only caps lock and num lock,
not all modifiers like shift (which can leak information
about your password to bystanders)
2022-05-28 i3lock 2.14
• Change default background color to #a3a3a3
See https://github.com/i3/i3lock/pull/300 for extensive
discussion and rationale for this change.
• Recommend using xss-lock to start i3lock in the README
and i3lock.1 man page. xss-lock is the best way to ensure
your screen truly is locked before your computer suspends.
• Display modifier key warning before unlocking, too,
not just on failed attempts like before.
• Switch build system from autotools to meson.
2020-10-27 i3lock 2.13
• Throw error when trying to start on Wayland
• Use explicit_bzero() where available, not just on OpenBSD
• avoid pixmap allocations in the redraw path
• make --debug output go to stderr
• unlock_indicator.c: fix build failure against gcc-10
• fix: call pam_end in cleanup in main, not in event loop
• set _NET_WM_BYPASS_COMPOSITOR hint to avoid flickering
2019-07-21 i3lock 2.12
• remove stray \n from error messages
• capitalize unlock indicator contents
• set WM_CLASS property
• reference modifier as “Super”, not “Win”
• add --raw option to read image as raw bytes
2018-10-18 i3lock 2.11.1
• Fix dist tarball by including I3LOCK_VERSION
2018-10-10 i3lock 2.11
• Switch to autotools
• Display an error when backspace is pressed without any input
• Print an error when a non-PNG file is opened
(i3lock only supports PNG files) (Thanks eplanet)
• Dont unnecessarily check the xcb_connect return value,
it is known never to be NULL (Thanks SegFault42)
• Fix memory leak when grabbing fails (Thanks karulont)
• Respect Xft.dpi for determining the unlock indicators scale factor
• Discard pending password verification attempts
when a new password is entered (Thanks layus)
2017-11-25 i3lock 2.10
• Only use -lpam when not on OpenBSD (Thanks Kaashif)
• locale: treat empty string same as unset (Thanks Ran)
• Fix overwrite of getopt optind (Thanks jakob)
• Immediately hide the unlock indicator after ESC / C-u (Thanks Orestis)
• Measure wall-clock time instead of CPU time for “locking” indicator.
• SetInputFocus to the i3lock window to force-close context menus
• Use RandR for learning about attached monitors
2017-06-21 i3lock 2.9.1
• Fix version number mechanism (for --version)
• Revert the fix for composited notifications, as it causes more issues than
it solves:
https://github.com/i3/i3lock/issues/130
https://github.com/i3/i3lock/issues/128
2017-05-26 i3lock 2.9
• i3lock.1: use signal names without SIG prefix
• Removed obsolete inactivity timeout
• Added version files for release tarball.
• Set font face
• Automatically unlock (without having to press <Enter>) one attempt which was
entered while authenticating
• Stop leaking the image_path dup
• Displaying locking message when grabbing the pointer/keyboard
• Display error message when locking failed
• Add Enter on C-m
• Change input slices to be exactly pi/3 in size instead of slightly more
• Fix covering of composited notifications using the XComposite extension
• Remove last traces of DPMS
• Use bsd_auth(3) instead of PAM on OpenBSD
• Restore intended behaviour and don't use mlock(2) on OpenBSD.
2016-06-04 i3lock 2.8
• Remove DPMS support in favor of a wrapper script and xset(1).
• Indicate that the --inactivity-timeout option takes an argument. (Thanks
Kenneth Lyons)
• fix pam_securetty: set PAM_TTY to getenv("DISPLAY")
• Eat XKB_KEY_Delete and XKB_KEY_KP_Delete (Thanks bebehei)
• Show unlock indicator if password was entered during PAM verification
• Allow CTRL+J as enter and CTRL+H as backspace (Thanks Karl Tarbe)
• Flush xcb connection after opening fullscreen window (Thanks martin)
• Add support for `xss-lock --transfer-sleep-lock'
2015-05-20 i3lock 2.7
• Die when the X11 connection breaks during runtime (Thanks Eduan)
• Implement logging the number of failed attempts (Thanks koebi)
• Ignore password validation is pam is in wrong state (Thanks Neodyblue)
• Get current user with getpwuid() instead of using $ENV{USER} (Thanks Martin)
• Add support for Compose and dead-keys with libxkbcommon.
Requires libxkbcommon ≥ 0.5.0 (Thanks Daniel)
• Format the source using clang-format.
• Refresh pam credentials on successful authentication (for Kerberos and the
like) (Thanks James)
• List pressed modifiers on failed authentication (Thanks Deiz, Alexandre)
• Only redraw the screen if the unlock indicator is actually used
(Thanks Ingo)
• Make pkg-config configurable for cross-compilation (Thanks Nikolay)
2014-07-18 i3lock 2.6
• NEW DEPENDENCY: use libxkbcommon-x11 instead of libX11
This helps us get rid of all code that directly uses libX11
• Use cairo_status_to_string for descriptive errors.
• Add `-e` option to not validate empty password.
• Bugfix: update the initial keyboard modifier state (Thanks lee, Ran)
• Re-raise i3lock when obscured in a separate process
• Turn on the screen on successful authentication
• Delay to turn off the screen after wrong passwd
• Discard half-entered passwd after some inactivity
• Ignore empty passwd after repeated Enter keypress
• Scale the unlock indicator (for retina displays)
2013-06-09 i3lock 2.5
• NEW DEPENDENCY: Use libxkbcommon for input handling
This makes input handling much better for many edge cases.
• Bugfix: fix argument parsing on ARM (s/char/int/)
• Bugfix: free(reply) to avoid memory leak
• Bugfix: Use ev_loop_fork after fork, fixes forking on kqueue based OSes
• Bugfix: Fix centering the indicator in the no-xinerama case
• Only use mlock() on Linux, FreeBSD (for example) requires root
• promote the "could not load image" message from debug to normal
• s/pam_message/pam_response/ (Thanks Tucos)
• remove support for NOLIBCAIRO, cairo-xcb is widespread by now
• Allow XKB_KEY_XF86ScreenSaver as synonym for enter
This keysym is generated on convertible tablets by pressing a hardware
lock/unlock button.
• Allow passwordless PAM conversations (e.g. fingerprint)
• Add ctrl+u password reset
• Set window name to i3lock
2012-06-02 i3lock 2.4.1
• Bugfix: Correctly center unlock indicator after reconfiguring screens
(Thanks xeen)
• Bugfix: Revert shift lock handling (broke uppercase letters)
• Bugfix: Skip shift when getting the modifier mask (Thanks SardemFF7)
2012-04-01 i3lock 2.4
• Bugfix: Fix background color when using cairo (Thanks Pascal)
• Only output text when in debug mode (fixes problems with xautolock)
• fallback when the image cannot be loaded
• Use (void) instead of () for functions without args (Thanks fernandotcl)
2012-03-15 i3lock 2.3.1
• Fix compilation on some systems
2012-03-15 i3lock 2.3
• Implement a visual unlock indicator
• Support ISO_Level5_Shift and Caps Lock
• Lock the password buffer in memory, clear it in RAM after verifying
• Fork after the window is visible, not before
• Bugfix: Copy the color depth from parent (root) window instead of
hardcoding a depth of 24
2011-11-06 i3lock 2.2
• Dont re-grab pointer/keyboard on MappingNotify. In some rare situations,
this lead to some keypresses "slipping through" to the last focused window.
• Correctly handle Mode_switch/ISO_Level3_Shift
• Render to a pixmap which is used as background for the window instead of
copying contents on every expose event
• Handle screen resolution changes while screen is locked
• Manpage: document arguments for every option
2011-05-13 i3lock 2.1
• Accept return/backspace when the buffer of 512 bytes is full
• Handle numpad keys correctly
• Handle MappingNotify events
• Correctly check for errors when connecting to X11
• Add i3lock.pam to not rely on debians /etc/pam.d/other anymore
• dont display debug output
• add NOLIBCAIRO flag to permit compilation without cairo
2010-09-05 i3lock 2.0
• Complete rewrite of i3lock. Now using xcb instead of Xlib.
• When a window obscures i3lock, it pushes itself back to the top again.
• Display version when starting with -v
2009-08-02 i3lock 1.1
• Implement background pictures (-i) and colors (-c)
2009-05-10 i3lock 1.0
• Implement PAM support
• Implement options for forking, beeping, DPMS
2009-05-01 i3lock 0.9
• First release, forked from slock 0.9
+26
View File
@@ -0,0 +1,26 @@
Copyright © 2010, Michael Stapelberg and contributors
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+91
View File
@@ -0,0 +1,91 @@
I38Lock - accessible screen locker for I38
==========================================
I38Lock is an accessible screen locker for I38, based on
[i3lock](https://i3wm.org/i3lock/).
After starting it, you will see a white screen (you can configure the
color/an image). You can return to your screen by entering your password.
It retains i3lock's behavior while adding blind-accessible password feedback:
- I38Lock forks, so you can combine it with an alias to suspend to RAM
(run "i38lock && echo mem > /sys/power/state" to get a locked screen
after waking up your computer from suspend to RAM)
- You can specify either a background color or a PNG image which will be
displayed while your screen is locked. Note that I38Lock is not an image
manipulation software. If you need to resize the image to fill the screen
or similar, use existing tooling to do this before passing it to I38Lock.
- You can specify whether I38Lock should bell upon a wrong password.
- When Cthulhu or Orca is running, I38Lock sends password-entry feedback to the
screen reader over its D-Bus remote-controller interface. Empty passwords are
rejected locally without invoking PAM.
- I38Lock uses PAM and therefore is compatible with LDAP etc.
On OpenBSD I38Lock uses the bsd_auth(3) framework.
Install
-------
See [the upstream i3lock home page](https://i3wm.org/i3lock/).
Requirements
------------
- pkg-config
- libxcb
- libxcb-util
- libpam-dev
- libcairo-dev
- libglib2.0-dev
- libxcb-xinerama
- libxcb-randr
- libev
- libx11-dev
- libx11-xcb-dev
- libxkbcommon >= 0.5.0
- libxkbcommon-x11 >= 0.5.0
- libxcb-image
- libxcb-xrm
Running I38Lock
---------------
To test I38Lock, you can directly run the `i38lock` command. To get out of it,
enter your password and press enter.
For a more permanent setup, we strongly recommend using `xss-lock` so that the
screen is locked *before* your laptop suspends:
```
xss-lock --transfer-sleep-lock -- i38lock --nofork
```
On OpenBSD the `i38lock` binary needs to be setgid `auth` to call the
authentication helpers, e.g. `/usr/libexec/auth/login_passwd`.
Building I38Lock
----------------
We recommend you use the provided package from your distribution. Do not build
I38Lock unless you have a reason to do so.
First install the dependencies listed in requirements section, then run these
commands (might need to be adapted to your OS):
```
rm -rf build/
mkdir -p build && cd build/
meson setup -Dprefix=/usr
ninja
```
Before testing a locally built binary on Linux, install the PAM policy:
```
sudo install -Dm644 ../pam/i38lock /etc/pam.d/i38lock
```
Without this file, I38Lock refuses to lock rather than risk creating a session
that cannot be unlocked.
Upstream
--------
I38Lock is based on https://github.com/i3/i3lock.
+251
View File
@@ -0,0 +1,251 @@
/*
* vim:ts=4:sw=4:expandtab
*
* See LICENSE for licensing information
*
*/
#include <errno.h>
#include <fcntl.h>
#include <gio/gio.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "accessibility_feedback.h"
static int feedback_fd = -1;
struct screen_reader {
const char *service;
const char *path;
const char *interface;
};
static const struct screen_reader screen_readers[] = {
{
"org.stormux.Cthulhu1.Service",
"/org/stormux/Cthulhu1/Service",
"org.stormux.Cthulhu1.Service",
},
{
"org.gnome.Orca1.Service",
"/org/gnome/Orca1/Service",
"org.gnome.Orca1.Service",
},
};
static const char *feedback_message(accessibility_feedback_t feedback) {
switch (feedback) {
case ACCESSIBILITY_SCREEN_LOCKED:
return "Screen locked. Enter password.";
case ACCESSIBILITY_ENTER_PASSWORD:
return "Enter password.";
case ACCESSIBILITY_CHARACTER_TYPED:
return "star";
case ACCESSIBILITY_CHARACTER_ERASED:
return "backspace star";
case ACCESSIBILITY_NOTHING_TO_ERASE:
return "backspace blank";
case ACCESSIBILITY_PASSWORD_CLEARED:
return "Password cleared.";
case ACCESSIBILITY_VERIFYING:
return "Verifying.";
case ACCESSIBILITY_AUTHENTICATED:
return "Authenticated.";
case ACCESSIBILITY_INCORRECT_PASSWORD:
return "Incorrect password. Enter password.";
case ACCESSIBILITY_CAPS_LOCK_ON:
return "Caps lock on.";
case ACCESSIBILITY_CAPS_LOCK_OFF:
return "Caps lock off.";
case ACCESSIBILITY_NUM_LOCK_ON:
return "Num lock on.";
case ACCESSIBILITY_NUM_LOCK_OFF:
return "Num lock off.";
}
return NULL;
}
static bool service_has_owner(GDBusConnection *connection, const char *service) {
GError *error = NULL;
GVariant *result = g_dbus_connection_call_sync(
connection,
"org.freedesktop.DBus",
"/org/freedesktop/DBus",
"org.freedesktop.DBus",
"NameHasOwner",
g_variant_new("(s)", service),
G_VARIANT_TYPE("(b)"),
G_DBUS_CALL_FLAGS_NONE,
250,
NULL,
&error);
if (result == NULL) {
g_clear_error(&error);
return false;
}
gboolean has_owner = false;
g_variant_get(result, "(b)", &has_owner);
g_variant_unref(result);
return has_owner;
}
static void present_message(GDBusConnection *connection, const char *message) {
for (size_t i = 0; i < sizeof(screen_readers) / sizeof(screen_readers[0]); i++) {
const struct screen_reader *reader = &screen_readers[i];
if (!service_has_owner(connection, reader->service)) {
continue;
}
GError *error = NULL;
GVariant *result = g_dbus_connection_call_sync(
connection,
reader->service,
reader->path,
reader->interface,
"PresentMessage",
g_variant_new("(s)", message),
G_VARIANT_TYPE("(b)"),
G_DBUS_CALL_FLAGS_NONE,
500,
NULL,
&error);
if (result != NULL) {
g_variant_unref(result);
}
g_clear_error(&error);
return;
}
}
static void flush_stars(GDBusConnection *connection, size_t count) {
char message[640];
size_t position = 0;
for (size_t i = 0; i < count; i++) {
const char *separator = (i == 0 ? "" : " ");
position += snprintf(message + position, sizeof(message) - position, "%sstar", separator);
}
present_message(connection, message);
}
static void close_sleep_lock_fd(void) {
const char *sleep_lock_fd = getenv("XSS_SLEEP_LOCK_FD");
if (sleep_lock_fd == NULL || *sleep_lock_fd == '\0') {
return;
}
char *endptr;
long int fd = strtol(sleep_lock_fd, &endptr, 10);
if (*endptr == '\0') {
close(fd);
}
}
static void feedback_worker(int read_fd, int inherited_xcb_fd) {
if (inherited_xcb_fd >= 0) {
close(inherited_xcb_fd);
}
close_sleep_lock_fd();
GError *error = NULL;
GDBusConnection *connection = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error);
if (connection == NULL) {
g_clear_error(&error);
}
accessibility_feedback_t feedback[128];
ssize_t bytes_read;
while ((bytes_read = read(read_fd, feedback, sizeof(feedback))) != 0) {
if (bytes_read < 0) {
if (errno == EINTR) {
continue;
}
break;
}
size_t count = (size_t)bytes_read / sizeof(feedback[0]);
size_t stars = 0;
for (size_t i = 0; i < count; i++) {
if (feedback[i] == ACCESSIBILITY_CHARACTER_TYPED) {
stars++;
continue;
}
if (connection != NULL && stars > 0) {
flush_stars(connection, stars);
}
stars = 0;
const char *message = feedback_message(feedback[i]);
if (connection != NULL && message != NULL) {
present_message(connection, message);
}
}
if (connection != NULL && stars > 0) {
flush_stars(connection, stars);
}
}
if (connection != NULL) {
g_object_unref(connection);
}
close(read_fd);
}
void accessibility_feedback_start(int inherited_xcb_fd) {
int pipe_fds[2];
if (pipe(pipe_fds) != 0) {
return;
}
int flags = fcntl(pipe_fds[1], F_GETFL);
if (flags == -1 || fcntl(pipe_fds[1], F_SETFL, flags | O_NONBLOCK) == -1) {
close(pipe_fds[0]);
close(pipe_fds[1]);
return;
}
signal(SIGPIPE, SIG_IGN);
pid_t pid = fork();
if (pid == -1) {
close(pipe_fds[0]);
close(pipe_fds[1]);
return;
}
if (pid == 0) {
close(pipe_fds[1]);
feedback_worker(pipe_fds[0], inherited_xcb_fd);
_exit(EXIT_SUCCESS);
}
close(pipe_fds[0]);
feedback_fd = pipe_fds[1];
}
void accessibility_feedback_notify(accessibility_feedback_t feedback) {
if (feedback_fd == -1) {
return;
}
ssize_t result;
do {
result = write(feedback_fd, &feedback, sizeof(feedback));
} while (result == -1 && errno == EINTR);
if (result == -1 && errno == EPIPE) {
close(feedback_fd);
feedback_fd = -1;
}
}
+25
View File
@@ -0,0 +1,25 @@
# vim:ft=Dockerfile
FROM debian:sid
RUN echo force-unsafe-io > /etc/dpkg/dpkg.cfg.d/docker-apt-speedup
# Paper over occasional network flakiness of some mirrors.
RUN echo 'APT::Acquire::Retries "5";' > /etc/apt/apt.conf.d/80retry
# NOTE: I tried exclusively using gce_debian_mirror.storage.googleapis.com
# instead of httpredir.debian.org, but the results (Fetched 123 MB in 36s (3357
# kB/s)) are not any better than httpredir.debian.org (Fetched 123 MB in 34s
# (3608 kB/s)). Hence, lets stick with httpredir.debian.org (default) for now.
# Install mk-build-deps (for installing the i3 build dependencies),
# clang (for building),
# lintian (for checking spelling errors),
# test suite dependencies (for running tests)
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
build-essential clang git meson libxcb-randr0-dev pkg-config libpam0g-dev \
libcairo2-dev libxcb1-dev libxcb-dpms0-dev libxcb-image0-dev libxcb-util0-dev \
libxcb-xrm-dev libev-dev libxcb-xinerama0-dev libxcb-xkb-dev libxkbcommon-dev \
libxkbcommon-x11-dev libglib2.0-dev && \
rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src
@@ -0,0 +1,5 @@
/i38lock/
/pkg/
/src/
/*.pkg.tar.*
/*.src.tar.*
@@ -0,0 +1,4 @@
[i38lock]
source = "git"
git = "https://git.stormux.org/storm/i38lock.git"
use_max_tag = true
@@ -0,0 +1,12 @@
Copyright Arch Linux Contributors
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.
+1
View File
@@ -0,0 +1 @@
../LICENSE
@@ -0,0 +1,46 @@
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
pkgname=i38lock
pkgver=2026.06.02
pkgrel=1
pkgdesc='X11 screen locker for I38'
url='https://git.stormux.org/storm/i38lock'
arch=('aarch64' 'x86_64')
license=('BSD-3-Clause')
depends=(
'cairo' 'libcairo.so'
'glib2'
'glibc'
'libev' 'libev.so'
'libxcb'
'libxkbcommon' 'libxkbcommon.so'
'libxkbcommon-x11' 'libxkbcommon-x11.so'
'pam' 'libpam.so'
'xcb-util'
'xcb-util-image'
'xcb-util-xrm'
)
makedepends=(
'git'
'meson'
)
options=('docs')
backup=('etc/pam.d/i38lock')
source=("$pkgname::git+$url.git#tag=$pkgver")
sha512sums=('SKIP')
build() {
arch-meson "$pkgname" build
meson compile -C build
}
package() {
# Fix ticket FS#31544, sed line taken from gentoo
sed -i -e 's:login:system-auth:g' "$pkgname/pam/i38lock"
DESTDIR="$pkgdir" meson install -C build --no-rebuild
install -Dm644 "$pkgname/LICENSE" -t "$pkgdir/usr/share/licenses/$pkgname"
}
# vim: ts=2 sw=2 et:
@@ -0,0 +1,22 @@
version = 1
[[annotations]]
path = [
"PKGBUILD",
"README.md",
"keys/**",
".SRCINFO",
".nvchecker.toml",
"*.install",
"*.sysusers",
"*.tmpfiles",
"*.logrotate",
"*.pam",
"*.service",
"*.socket",
"*.timer",
"*.desktop",
"*.hook",
]
SPDX-FileCopyrightText = "Arch Linux contributors"
SPDX-License-Identifier = "0BSD"
+110
View File
@@ -0,0 +1,110 @@
/*
* vim:ts=4:sw=4:expandtab
*
* i3 - an improved tiling window manager
* © 2009 Michael Stapelberg and contributors (see also: LICENSE)
*
*/
#include "dpi.h"
#include <math.h>
#include <stdlib.h>
#include <stdio.h>
#include <xcb/xcb_xrm.h>
#include "xcb.h"
#include "i3lock.h"
extern bool debug_mode;
static long dpi;
extern xcb_screen_t *screen;
static long init_dpi_fallback(void) {
return (double)screen->height_in_pixels * 25.4 / (double)screen->height_in_millimeters;
}
/*
* Initialize the DPI setting.
* This will use the 'Xft.dpi' X resource if available and fall back to
* guessing the correct value otherwise.
*/
void init_dpi(void) {
xcb_xrm_database_t *database = NULL;
char *resource = NULL;
if (conn == NULL) {
goto init_dpi_end;
}
database = xcb_xrm_database_from_default(conn);
if (database == NULL) {
DEBUG("Failed to open the resource database.\n");
goto init_dpi_end;
}
xcb_xrm_resource_get_string(database, "Xft.dpi", NULL, &resource);
if (resource == NULL) {
DEBUG("Resource Xft.dpi not specified, skipping.\n");
goto init_dpi_end;
}
char *endptr;
double in_dpi = strtod(resource, &endptr);
if (in_dpi == HUGE_VAL || in_dpi < 0 || *endptr != '\0' || endptr == resource) {
DEBUG("Xft.dpi = %s is an invalid number and couldn't be parsed.\n", resource);
dpi = 0;
goto init_dpi_end;
}
dpi = (long)round(in_dpi);
DEBUG("Found Xft.dpi = %ld.\n", dpi);
init_dpi_end:
if (resource != NULL) {
free(resource);
}
if (database != NULL) {
xcb_xrm_database_free(database);
}
if (dpi == 0) {
DEBUG("Using fallback for calculating DPI.\n");
dpi = init_dpi_fallback();
DEBUG("Using dpi = %ld\n", dpi);
}
}
/*
* This function returns the value of the DPI setting.
*
*/
long get_dpi_value(void) {
return dpi;
}
/*
* Convert a logical amount of pixels (e.g. 2 pixels on a “standard” 96 DPI
* screen) to a corresponding amount of physical pixels on a standard or retina
* screen, e.g. 5 pixels on a 227 DPI MacBook Pro 13" Retina screen.
*
*/
int logical_px(const int logical) {
if (screen == NULL) {
/* Dpi info may not be available when parsing a config without an X
* server, such as for config file validation. */
return logical;
}
/* There are many misconfigurations out there, i.e. systems with screens
* whose dpi is in fact higher than 96 dpi, but not significantly higher,
* so software was never adapted. We could tell people to reconfigure their
* systems to 96 dpi in order to get the behavior they expect/are used to,
* but since we can easily detect this case in code, lets do it for them.
*/
if ((dpi / 96.0) < 1.25) {
return logical;
}
return ceil((dpi / 96.0) * logical);
}
Generated
+27
View File
@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1758791193,
"narHash": "sha256-F8WmEwFoHsnix7rt290R0rFXNJiMbClMZyIC/e+HYf0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "25e53aa156d47bad5082ff7618f5feb1f5e02d01",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
+29
View File
@@ -0,0 +1,29 @@
{
description = "development environment for I38Lock";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
};
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
in {
devShells.${system}.default = pkgs.mkShell {
buildInputs = with pkgs; [
cairo
glib
libev
libxkbcommon
pam
pkg-config
xcbutilxrm
xorg.libX11
xorg.libxcb
xorg.xcbutil
xorg.xcbutilimage
];
};
};
}
+193
View File
@@ -0,0 +1,193 @@
.de Vb \" Begin verbatim text
.ft CW
.nf
.ne \\$1
..
.de Ve \" End verbatim text
.ft R
.fi
..
.TH i38lock 1 "JUNE 2026" Linux "User Manuals"
.SH NAME
I38Lock \- accessible screen locker for I38
.SH SYNOPSIS
.B i38lock
.RB [\|\-v\|]
.RB [\|\-n\|]
.RB [\|\-b\|]
.RB [\|\-i
.IR image.png \|]
.RB [\|\-c
.IR color \|]
.RB [\|\-t\|]
.RB [\|\-p
.IR pointer\|]
.RB [\|\-u\|]
.RB [\|\-f\|]
.SH RECOMMENDED USAGE
.RB xss-lock
.RB --transfer-sleep-lock
.RB --
.RB i38lock
.RB --nofork
Using
.B xss-lock
ensures that your screen is locked before your laptop suspends.
Notably, using a systemd service file is not adequate, as it will not delay
suspend until your screen is locked.
.SH DESCRIPTION
.B I38Lock
is an accessible screen locker based on i3lock. After starting it, you will see a white
screen (you can configure the color/an image). You can return to your screen by
entering your password.
.SH IMPROVEMENTS
.IP \[bu] 2
I38Lock forks, so you can combine it with an alias to suspend to RAM (run "i38lock && echo mem > /sys/power/state" to get a locked screen after waking up your computer from suspend to RAM)
.IP \[bu]
You can specify either a background color or a PNG image which will be displayed while your screen is locked.
.IP \[bu]
You can specify whether I38Lock should bell upon a wrong password.
.IP \[bu]
I38Lock uses PAM and therefore is compatible with LDAP, etc.
.SH OPTIONS
.TP
.B \-v, \-\-version
Display the version of your
.B i38lock
.TP
.B \-n, \-\-nofork
Don't fork after starting.
.TP
.B \-b, \-\-beep
Enable beeping. Be sure to not do this when you are about to annoy other people,
like when opening your laptop in a boring lecture.
.TP
.B \-u, \-\-no-unlock-indicator
Disable the unlock indicator. I38Lock will by default show an unlock indicator
after pressing keys. This will give feedback for every keypress and it will
show you the current PAM state (whether your password is currently being
verified or whether it is wrong).
.TP
.BI \-i\ path \fR,\ \fB\-\-image= path
Display the given PNG image instead of a blank screen.
.TP
.BI \fB\-\-raw= format
Read the image given by \-\-image as a raw image instead of PNG. The argument is the image's format
as <width>x<height>:<pixfmt>. The supported pixel formats are:
\(aqnative', \(aqrgb', \(aqxrgb', \(aqrgbx', \(aqbgr', \(aqxbgr', and \(aqbgrx'.
The "native" pixel format expects a pixel as a 32-bit (4-byte) integer in
the machine's native endianness, with the upper 8 bits unused. Red, green and blue are stored in
the remaining bits, in that order.
.BR Example:
.Vb 6
\& --raw=1920x1080:rgb
.Ve
.BR
You can use ImageMagicks
.IR convert(1)
program to feed raw images into I38Lock:
.BR
.Vb 6
\& convert wallpaper.jpg RGB:- | i38lock --raw 3840x2160:rgb --image /dev/stdin
.Ve
This allows you to load a variety of image formats without I38Lock having to
support each one explicitly.
.TP
.BI \-c\ rrggbb \fR,\ \fB\-\-color= rrggbb
Turn the screen into the given color instead of white. Color must be given in 3-byte
format: rrggbb (i.e. ff0000 is red).
.TP
.B \-t, \-\-tiling
If an image is specified (via \-i) it will display the image tiled all over the screen
(if it is a multi-monitor setup, the image is visible on all screens).
.TP
.BI \-p\ win|default \fR,\ \fB\-\-pointer= win|default
If you specify "default",
.B I38Lock
does not hide your mouse pointer. If you specify "win",
.B I38Lock
displays a hardcoded Windows-Pointer (thus enabling you to mess with your
friends by using a screenshot of a Windows desktop as a locking-screen).
.TP
.B \-f, \-\-show-failed-attempts
Show the number of failed attempts, if any.
.TP
.B \-k, \-\-show-keyboard-layout
Show the current keyboard layout.
.TP
.B \-\-debug
Enables debug logging.
Note, that this will log the password used for authentication to stdout.
.SH ACCESSIBILITY
When Cthulhu or Orca is running, I38Lock automatically sends password-entry
feedback to the screen reader over its D-Bus remote-controller interface.
Feedback includes lock state, accepted characters as the word "star",
backspace results, modifier-lock changes, verification, and authentication
failure. Password characters are never sent. Empty passwords are rejected
locally without invoking PAM.
.SH PAM CONFIGURATION
On Linux,
.B /etc/pam.d/i38lock
must exist and be readable before I38Lock will lock the screen. This prevents
an absent service policy from creating a session that cannot be unlocked.
.SH DPMS
The \-d (\-\-dpms) option was removed from i3lock in version 2.8. There were
plenty of use-cases that were not properly addressed, and plenty of bugs
surrounding that feature. While features are not normally removed from i3 and
its tools, we felt the need to make an exception in this case.
Users who wish to explicitly enable DPMS only when their screen is locked can
use a wrapper script around I38Lock like the following:
.Vb 6
\& #!/bin/sh
\& revert() {
\& xset dpms 0 0 0
\& }
\& trap revert HUP INT TERM
\& xset +dpms dpms 5 5 5
\& i38lock -n
\& revert
.Ve
The \-I (-\-inactivity-timeout=seconds) was removed because it only makes sense with DPMS.
.SH SEE ALSO
.IR xss-lock(1)
\- hooks up I38Lock to the systemd login manager
.IR convert(1)
\- feed a wide variety of image formats into I38Lock
.SH AUTHOR
Michael Stapelberg <michael+i3lock at stapelberg dot de>
+1389
View File
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
#ifndef _ACCESSIBILITY_FEEDBACK_H
#define _ACCESSIBILITY_FEEDBACK_H
typedef enum {
ACCESSIBILITY_SCREEN_LOCKED = 0,
ACCESSIBILITY_ENTER_PASSWORD,
ACCESSIBILITY_CHARACTER_TYPED,
ACCESSIBILITY_CHARACTER_ERASED,
ACCESSIBILITY_NOTHING_TO_ERASE,
ACCESSIBILITY_PASSWORD_CLEARED,
ACCESSIBILITY_VERIFYING,
ACCESSIBILITY_AUTHENTICATED,
ACCESSIBILITY_INCORRECT_PASSWORD,
ACCESSIBILITY_CAPS_LOCK_ON,
ACCESSIBILITY_CAPS_LOCK_OFF,
ACCESSIBILITY_NUM_LOCK_ON,
ACCESSIBILITY_NUM_LOCK_OFF,
} accessibility_feedback_t;
void accessibility_feedback_start(int inherited_xcb_fd);
void accessibility_feedback_notify(accessibility_feedback_t feedback);
#endif
+8
View File
@@ -0,0 +1,8 @@
#ifndef _CURSORS_H
#define _CURSORS_H
#define CURS_NONE 0
#define CURS_WIN 1
#define CURS_DEFAULT 2
#endif
+22
View File
@@ -0,0 +1,22 @@
#pragma once
/**
* Initialize the DPI setting.
* This will use the 'Xft.dpi' X resource if available and fall back to
* guessing the correct value otherwise.
*/
void init_dpi(void);
/**
* This function returns the value of the DPI setting.
*
*/
long get_dpi_value(void);
/**
* Convert a logical amount of pixels (e.g. 2 pixels on a “standard” 96 DPI
* screen) to a corresponding amount of physical pixels on a standard or retina
* screen, e.g. 5 pixels on a 227 DPI MacBook Pro 13" Retina screen.
*
*/
int logical_px(const int logical);
+15
View File
@@ -0,0 +1,15 @@
#ifndef _I3LOCK_H
#define _I3LOCK_H
/* This macro will only print debug output when started with --debug.
* This is important because xautolock (for example) closes stdout/stderr by
* default, so just printing something to stdout will lead to the data ending
* up on the X11 socket (!). */
#define DEBUG(fmt, ...) \
do { \
if (debug_mode) { \
fprintf(stderr, "[i38lock-debug] " fmt, ##__VA_ARGS__); \
} \
} while (0)
#endif
+17
View File
@@ -0,0 +1,17 @@
#ifndef _XINERAMA_H
#define _XINERAMA_H
typedef struct Rect {
int16_t x;
int16_t y;
uint16_t width;
uint16_t height;
} Rect;
extern int xr_screens;
extern Rect *xr_resolutions;
void randr_init(int *event_base, xcb_window_t root);
void randr_query(xcb_window_t root);
#endif
+29
View File
@@ -0,0 +1,29 @@
#ifndef _UNLOCK_INDICATOR_H
#define _UNLOCK_INDICATOR_H
#include <xcb/xcb.h>
typedef enum {
STATE_STARTED = 0, /* default state */
STATE_KEY_PRESSED = 1, /* key was pressed, show unlock indicator */
STATE_KEY_ACTIVE = 2, /* a key was pressed recently, highlight part
of the unlock indicator. */
STATE_BACKSPACE_ACTIVE = 3, /* backspace was pressed recently, highlight
part of the unlock indicator in red. */
STATE_NOTHING_TO_DELETE = 4, /* backspace was pressed, but there is nothing to delete. */
} unlock_state_t;
typedef enum {
STATE_AUTH_IDLE = 0, /* no authenticator interaction at the moment */
STATE_AUTH_VERIFY = 1, /* currently verifying the password via authenticator */
STATE_AUTH_LOCK = 2, /* currently locking the screen */
STATE_AUTH_WRONG = 3, /* the password was wrong */
STATE_I38LOCK_LOCK_FAILED = 4, /* I38Lock failed to load */
} auth_state_t;
void free_bg_pixmap(void);
void draw_image(xcb_pixmap_t bg_pixmap, uint32_t *resolution);
void redraw_screen(void);
void clear_indicator(void);
#endif
+17
View File
@@ -0,0 +1,17 @@
#ifndef _XCB_H
#define _XCB_H
#include <xcb/xcb.h>
extern xcb_connection_t *conn;
extern xcb_screen_t *screen;
xcb_visualtype_t *get_root_visual_type(xcb_screen_t *s);
xcb_pixmap_t create_bg_pixmap(xcb_connection_t *conn, xcb_screen_t *scr, u_int32_t *resolution, char *color);
xcb_window_t open_fullscreen_window(xcb_connection_t *conn, xcb_screen_t *scr, char *color, xcb_pixmap_t pixmap);
bool grab_pointer_and_keyboard(xcb_connection_t *conn, xcb_screen_t *screen, xcb_cursor_t cursor, int tries);
xcb_cursor_t create_cursor(xcb_connection_t *conn, xcb_screen_t *screen, xcb_window_t win, int choice);
xcb_window_t find_focused_window(xcb_connection_t *conn, const xcb_window_t root);
void set_focused_window(xcb_connection_t *conn, const xcb_window_t root, const xcb_window_t window);
#endif
+143
View File
@@ -0,0 +1,143 @@
# -*- mode: meson -*-
# Style objective: be consistent with what mesonbuild.com documents/uses, and/or
# the meson book: https://meson-manual.com/
project(
'i38lock',
'c',
version: '2026.06.02',
default_options: [
'c_std=c11',
'sysconfdir=/etc',
'warning_level=1', # enable all warnings (-Wall)
# TODO(https://github.com/i3/i3/issues/4087): switch to
# 'buildtype=debugoptimized',
],
# Ubuntu 18.04 (supported until 2023) has meson 0.45.
# We can revisit our minimum supported meson version
# if it turns out to be too hard to maintain.
meson_version: '>=0.45.0',
)
cc = meson.get_compiler('c')
add_project_arguments(cc.get_supported_arguments(['-Wunused-value']), language: 'c')
if meson.version().version_compare('>=0.48.0')
# https://github.com/mesonbuild/meson/issues/2166#issuecomment-629696911
meson.add_dist_script('meson/meson-dist-script')
else
message('meson <0.48.0 detected, dist tarballs will not be filtered')
endif
################################################################################
# Version handling
################################################################################
cdata = configuration_data()
version_array = meson.project_version().split('.')
cdata.set_quoted('I38LOCK_VERSION', '@VCS_TAG@')
cdata.set_quoted('SYSCONFDIR', join_paths(get_option('prefix'), get_option('sysconfdir')))
if get_option('b_sanitize').split(',').contains('address')
cdata.set('I38LOCK_ASAN_ENABLED', 1)
endif
cdata.set('HAVE_STRNDUP', cc.has_function('strndup'))
cdata.set('HAVE_MKDIRP', cc.has_function('mkdirp'))
cdata.set('HAVE_EXPLICIT_BZERO', cc.has_function('explicit_bzero'))
# Instead of generating config.h directly, make vcs_tag generate it so that
# @VCS_TAG@ is replaced.
config_h_in = configure_file(
output: 'config.h.in',
configuration: cdata,
)
config_h = declare_dependency(
sources: vcs_tag(
input: config_h_in,
output: 'config.h',
command: ['git', 'describe', '--tags', '--match', '20*', '--always', '--dirty=+'],
fallback: meson.project_version() + '-non-git',
)
)
################################################################################
# manpages
################################################################################
install_man('i38lock.1')
# Required for e.g. struct ucred to be defined as per unix(7).
add_project_arguments('-D_GNU_SOURCE', language: 'c')
# https://mesonbuild.com/howtox.html#add-math-library-lm-portably
m_dep = cc.find_library('m', required: false)
rt_dep = cc.find_library('rt', required: false)
xcb_dep = dependency('xcb', method: 'pkg-config')
xcb_xkb_dep = dependency('xcb-xkb', method: 'pkg-config')
xcb_xinerama_dep = dependency('xcb-xinerama', method: 'pkg-config')
xcb_randr_dep = dependency('xcb-randr', method: 'pkg-config')
xcb_image_dep = dependency('xcb-image', method: 'pkg-config')
xcb_util_dep = dependency('xcb-util', method: 'pkg-config')
xcb_util_xrm_dep = dependency('xcb-xrm', method: 'pkg-config')
xkbcommon_dep = dependency('xkbcommon', method: 'pkg-config')
xkbcommon_x11_dep = dependency('xkbcommon-x11', method: 'pkg-config')
cairo_dep = dependency('cairo', version: '>=1.14.4', method: 'pkg-config')
gio_dep = dependency('gio-2.0', method: 'pkg-config')
i38lock_srcs = [
'accessibility_feedback.c',
'dpi.c',
'i3lock.c',
'randr.c',
'unlock_indicator.c',
'xcb.c',
]
ev_dep = cc.find_library('ev')
thread_dep = dependency('threads')
i38lock_deps = [
thread_dep,
m_dep,
rt_dep,
ev_dep,
config_h,
cairo_dep,
gio_dep,
xcb_dep,
xcb_xkb_dep,
xcb_xinerama_dep,
xcb_randr_dep,
xcb_image_dep,
xcb_util_dep,
xcb_util_xrm_dep,
xkbcommon_dep,
xkbcommon_x11_dep,
]
host_os = host_machine.system()
if host_os != 'openbsd'
pam_dep = cc.find_library('pam', required: true)
i38lock_deps += [pam_dep]
endif
inc = include_directories('include')
executable(
'i38lock',
i38lock_srcs,
install: true,
include_directories: inc,
dependencies: i38lock_deps,
)
install_subdir(
'pam',
strip_directory: true,
install_dir: join_paths(get_option('sysconfdir'), 'pam.d'),
)
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
set -eu
cd "${MESON_DIST_ROOT}"
# Delete everything we do not want to have in the release tarballs:
rm -rf \
.clang-format \
.editorconfig \
ci \
.github
+1
View File
@@ -0,0 +1 @@
# -*- mode: meson -*-
+6
View File
@@ -0,0 +1,6 @@
#
# PAM configuration file for the I38Lock screen locker. By default, it includes
# the 'login' configuration file (see /etc/pam.d/login)
#
auth include login
+299
View File
@@ -0,0 +1,299 @@
/*
* vim:ts=4:sw=4:expandtab
*
* © 2010 Michael Stapelberg
*
* See LICENSE for licensing information
*
*/
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <xcb/xcb.h>
#include <xcb/xinerama.h>
#include <xcb/randr.h>
#include "i3lock.h"
#include "xcb.h"
#include "randr.h"
/* Number of Xinerama screens which are currently present. */
int xr_screens = 0;
/* The resolutions of the currently present Xinerama screens. */
Rect *xr_resolutions = NULL;
static bool xinerama_active;
static bool has_randr = false;
static bool has_randr_1_5 = false;
extern bool debug_mode;
void _xinerama_init(void);
void randr_init(int *event_base, xcb_window_t root) {
const xcb_query_extension_reply_t *extreply;
extreply = xcb_get_extension_data(conn, &xcb_randr_id);
if (!extreply->present) {
DEBUG("RandR is not present, falling back to Xinerama.\n");
_xinerama_init();
return;
}
xcb_generic_error_t *err;
xcb_randr_query_version_reply_t *randr_version =
xcb_randr_query_version_reply(
conn, xcb_randr_query_version(conn, XCB_RANDR_MAJOR_VERSION, XCB_RANDR_MINOR_VERSION), &err);
if (err != NULL) {
DEBUG("Could not query RandR version: X11 error code %d\n", err->error_code);
_xinerama_init();
return;
}
has_randr = true;
has_randr_1_5 = (randr_version->major_version >= 1) &&
(randr_version->minor_version >= 5);
free(randr_version);
if (event_base != NULL) {
*event_base = extreply->first_event;
}
xcb_randr_select_input(conn, root,
XCB_RANDR_NOTIFY_MASK_SCREEN_CHANGE |
XCB_RANDR_NOTIFY_MASK_OUTPUT_CHANGE |
XCB_RANDR_NOTIFY_MASK_CRTC_CHANGE |
XCB_RANDR_NOTIFY_MASK_OUTPUT_PROPERTY);
xcb_flush(conn);
}
void _xinerama_init(void) {
if (!xcb_get_extension_data(conn, &xcb_xinerama_id)->present) {
DEBUG("Xinerama extension not found, disabling.\n");
return;
}
xcb_xinerama_is_active_cookie_t cookie;
xcb_xinerama_is_active_reply_t *reply;
cookie = xcb_xinerama_is_active(conn);
reply = xcb_xinerama_is_active_reply(conn, cookie, NULL);
if (!reply) {
return;
}
if (!reply->state) {
free(reply);
return;
}
xinerama_active = true;
free(reply);
}
/*
* randr_query_outputs_15 uses RandR ≥ 1.5 to update outputs.
*
*/
static bool _randr_query_monitors_15(xcb_window_t root) {
#if XCB_RANDR_MINOR_VERSION < 5
return false;
#else
/* RandR 1.5 available at compile-time, i.e. libxcb is new enough */
if (!has_randr_1_5) {
return false;
}
/* RandR 1.5 available at run-time (supported by the server) */
DEBUG("Querying monitors using RandR 1.5\n");
xcb_generic_error_t *err;
xcb_randr_get_monitors_reply_t *monitors =
xcb_randr_get_monitors_reply(
conn, xcb_randr_get_monitors(conn, root, true), &err);
if (err != NULL) {
DEBUG("Could not get RandR monitors: X11 error code %d\n", err->error_code);
free(err);
/* Fall back to RandR ≤ 1.4 */
return false;
}
int screens = xcb_randr_get_monitors_monitors_length(monitors);
DEBUG("%d RandR monitors found (timestamp %d)\n",
screens, monitors->timestamp);
Rect *resolutions = malloc(screens * sizeof(Rect));
/* No memory? Just keep on using the old information. */
if (!resolutions) {
free(monitors);
return true;
}
xcb_randr_monitor_info_iterator_t iter;
int screen;
for (iter = xcb_randr_get_monitors_monitors_iterator(monitors), screen = 0;
iter.rem;
xcb_randr_monitor_info_next(&iter), screen++) {
const xcb_randr_monitor_info_t *monitor_info = iter.data;
resolutions[screen].x = monitor_info->x;
resolutions[screen].y = monitor_info->y;
resolutions[screen].width = monitor_info->width;
resolutions[screen].height = monitor_info->height;
DEBUG("found RandR monitor: %d x %d at %d x %d\n",
monitor_info->width, monitor_info->height,
monitor_info->x, monitor_info->y);
}
free(xr_resolutions);
xr_resolutions = resolutions;
xr_screens = screens;
free(monitors);
return true;
#endif
}
/*
* randr_query_outputs_14 uses RandR ≤ 1.4 to update outputs.
*
*/
static bool _randr_query_outputs_14(xcb_window_t root) {
if (!has_randr) {
return false;
}
DEBUG("Querying outputs using RandR ≤ 1.4\n");
/* Get screen resources (primary output, crtcs, outputs, modes) */
xcb_randr_get_screen_resources_current_cookie_t rcookie;
rcookie = xcb_randr_get_screen_resources_current(conn, root);
xcb_randr_get_screen_resources_current_reply_t *res =
xcb_randr_get_screen_resources_current_reply(conn, rcookie, NULL);
if (res == NULL) {
DEBUG("Could not query screen resources.\n");
return false;
}
/* timestamp of the configuration so that we get consistent replies to all
* requests (if the configuration changes between our different calls) */
const xcb_timestamp_t cts = res->config_timestamp;
const int len = xcb_randr_get_screen_resources_current_outputs_length(res);
/* an output is VGA-1, LVDS-1, etc. (usually physical video outputs) */
xcb_randr_output_t *randr_outputs = xcb_randr_get_screen_resources_current_outputs(res);
/* Request information for each output */
xcb_randr_get_output_info_cookie_t ocookie[len];
for (int i = 0; i < len; i++) {
ocookie[i] = xcb_randr_get_output_info(conn, randr_outputs[i], cts);
}
Rect *resolutions = malloc(len * sizeof(Rect));
/* No memory? Just keep on using the old information. */
if (!resolutions) {
free(res);
return true;
}
/* Loop through all outputs available for this X11 screen */
int screen = 0;
for (int i = 0; i < len; i++) {
xcb_randr_get_output_info_reply_t *output;
if ((output = xcb_randr_get_output_info_reply(conn, ocookie[i], NULL)) == NULL) {
continue;
}
if (output->crtc == XCB_NONE) {
free(output);
continue;
}
xcb_randr_get_crtc_info_cookie_t icookie;
xcb_randr_get_crtc_info_reply_t *crtc;
icookie = xcb_randr_get_crtc_info(conn, output->crtc, cts);
if ((crtc = xcb_randr_get_crtc_info_reply(conn, icookie, NULL)) == NULL) {
DEBUG("Skipping output: could not get CRTC (0x%08x)\n", output->crtc);
free(output);
continue;
}
resolutions[screen].x = crtc->x;
resolutions[screen].y = crtc->y;
resolutions[screen].width = crtc->width;
resolutions[screen].height = crtc->height;
DEBUG("found RandR output: %d x %d at %d x %d\n",
crtc->width, crtc->height,
crtc->x, crtc->y);
screen++;
free(crtc);
free(output);
}
free(xr_resolutions);
xr_resolutions = resolutions;
xr_screens = screen;
free(res);
return true;
}
void _xinerama_query_screens(void) {
if (!xinerama_active) {
return;
}
xcb_xinerama_query_screens_cookie_t cookie;
xcb_xinerama_query_screens_reply_t *reply;
xcb_xinerama_screen_info_t *screen_info;
xcb_generic_error_t *err;
cookie = xcb_xinerama_query_screens_unchecked(conn);
reply = xcb_xinerama_query_screens_reply(conn, cookie, &err);
if (!reply) {
DEBUG("Couldn't get Xinerama screens: X11 error code %d\n", err->error_code);
free(err);
return;
}
screen_info = xcb_xinerama_query_screens_screen_info(reply);
int screens = xcb_xinerama_query_screens_screen_info_length(reply);
Rect *resolutions = malloc(screens * sizeof(Rect));
/* No memory? Just keep on using the old information. */
if (!resolutions) {
free(reply);
return;
}
for (int screen = 0; screen < xr_screens; screen++) {
resolutions[screen].x = screen_info[screen].x_org;
resolutions[screen].y = screen_info[screen].y_org;
resolutions[screen].width = screen_info[screen].width;
resolutions[screen].height = screen_info[screen].height;
DEBUG("found Xinerama screen: %d x %d at %d x %d\n",
screen_info[screen].width, screen_info[screen].height,
screen_info[screen].x_org, screen_info[screen].y_org);
}
free(xr_resolutions);
xr_resolutions = resolutions;
xr_screens = screens;
free(reply);
}
void randr_query(xcb_window_t root) {
if (_randr_query_monitors_15(root)) {
return;
}
if (_randr_query_outputs_14(root)) {
return;
}
_xinerama_query_screens();
}
+463
View File
@@ -0,0 +1,463 @@
/*
* vim:ts=4:sw=4:expandtab
*
* © 2010 Michael Stapelberg
*
* See LICENSE for licensing information
*
*/
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <xcb/xcb.h>
#include <xkbcommon/xkbcommon.h>
#include <ev.h>
#include <cairo.h>
#include <cairo/cairo-xcb.h>
#include "i3lock.h"
#include "xcb.h"
#include "unlock_indicator.h"
#include "randr.h"
#include "dpi.h"
#define BUTTON_RADIUS 90
#define BUTTON_SPACE (BUTTON_RADIUS + 5)
#define BUTTON_CENTER (BUTTON_RADIUS + 5)
#define BUTTON_DIAMETER (2 * BUTTON_SPACE)
/*******************************************************************************
* Variables defined in i3lock.c.
******************************************************************************/
extern bool debug_mode;
/* The current position in the input buffer. Useful to determine if any
* characters of the password have already been entered or not. */
extern int input_position;
/* The lock window. */
extern xcb_window_t win;
/* The current resolution of the X11 root window. */
extern uint32_t last_resolution[2];
/* Whether the unlock indicator is enabled (defaults to true). */
extern bool unlock_indicator;
/* List of pressed modifiers, or NULL if none are pressed. */
extern char *modifier_string;
/* Name of the current keyboard layout or NULL if not initialized. */
char *layout_string = NULL;
/* A Cairo surface containing the specified image (-i), if any. */
extern cairo_surface_t *img;
/* Whether the image should be tiled. */
extern bool tile;
/* The background color to use (in hex). */
extern char color[7];
/* Whether the failed attempts should be displayed. */
extern bool show_failed_attempts;
/* Whether keyboard layout should be displayed. */
extern bool show_keyboard_layout;
/* Number of failed unlock attempts. */
extern int failed_attempts;
extern struct xkb_keymap *xkb_keymap;
extern struct xkb_state *xkb_state;
/*******************************************************************************
* Variables defined in xcb.c.
******************************************************************************/
/* The root screen, to determine the DPI. */
extern xcb_screen_t *screen;
/*******************************************************************************
* Local variables.
******************************************************************************/
/* Cache the screens visual, necessary for creating a Cairo context. */
static xcb_visualtype_t *vistype;
/* Maintain the current unlock/PAM state to draw the appropriate unlock
* indicator. */
unlock_state_t unlock_state;
auth_state_t auth_state;
static void string_append(char **string_ptr, const char *appended) {
char *tmp = NULL;
if (*string_ptr == NULL) {
if (asprintf(&tmp, "%s", appended) != -1) {
*string_ptr = tmp;
}
} else if (asprintf(&tmp, "%s, %s", *string_ptr, appended) != -1) {
free(*string_ptr);
*string_ptr = tmp;
}
}
static void display_button_text(
cairo_t *ctx, const char *text, double y_offset, bool use_dark_text) {
cairo_text_extents_t extents;
double x, y;
cairo_text_extents(ctx, text, &extents);
x = BUTTON_CENTER - ((extents.width / 2) + extents.x_bearing);
y = BUTTON_CENTER - ((extents.height / 2) + extents.y_bearing) + y_offset;
cairo_move_to(ctx, x, y);
if (use_dark_text) {
cairo_set_source_rgb(ctx, 0., 0., 0.);
} else {
cairo_set_source_rgb(ctx, 1., 1., 1.);
}
cairo_show_text(ctx, text);
cairo_close_path(ctx);
}
static void update_layout_string() {
if (layout_string) {
free(layout_string);
layout_string = NULL;
}
xkb_layout_index_t num_layouts = xkb_keymap_num_layouts(xkb_keymap);
for (xkb_layout_index_t i = 0; i < num_layouts; ++i) {
if (xkb_state_layout_index_is_active(xkb_state, i, XKB_STATE_LAYOUT_EFFECTIVE)) {
const char *name = xkb_keymap_layout_get_name(xkb_keymap, i);
if (name) {
string_append(&layout_string, name);
}
}
}
}
/* check_modifier_keys describes the currently active modifiers (Caps Lock, Alt,
Num Lock or Super) in the modifier_string variable. */
static void check_modifier_keys(void) {
xkb_mod_index_t idx, num_mods;
const char *mod_name;
num_mods = xkb_keymap_num_mods(xkb_keymap);
for (idx = 0; idx < num_mods; idx++) {
if (!xkb_state_mod_index_is_active(xkb_state, idx, XKB_STATE_MODS_EFFECTIVE)) {
continue;
}
mod_name = xkb_keymap_mod_get_name(xkb_keymap, idx);
if (mod_name == NULL) {
continue;
}
/* Replace certain xkb names with nicer, human-readable ones. */
if (strcmp(mod_name, XKB_MOD_NAME_CAPS) == 0) {
mod_name = "Caps Lock";
} else if (strcmp(mod_name, XKB_MOD_NAME_NUM) == 0) {
mod_name = "Num Lock";
} else {
/* Show only Caps Lock and Num Lock, other modifiers (e.g. Shift)
* leak state about the password. */
continue;
}
string_append(&modifier_string, mod_name);
}
}
/*
* Draws global image with fill color onto a pixmap with the given
* resolution and returns it.
*
*/
void draw_image(xcb_pixmap_t bg_pixmap, uint32_t *resolution) {
const double scaling_factor = get_dpi_value() / 96.0;
int button_diameter_physical = ceil(scaling_factor * BUTTON_DIAMETER);
DEBUG("scaling_factor is %.f, physical diameter is %d px\n",
scaling_factor, button_diameter_physical);
if (!vistype) {
vistype = get_root_visual_type(screen);
}
/* Initialize cairo: Create one in-memory surface to render the unlock
* indicator on, create one XCB surface to actually draw (one or more,
* depending on the amount of screens) unlock indicators on. */
cairo_surface_t *output = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, button_diameter_physical, button_diameter_physical);
cairo_t *ctx = cairo_create(output);
cairo_surface_t *xcb_output = cairo_xcb_surface_create(conn, bg_pixmap, vistype, resolution[0], resolution[1]);
cairo_t *xcb_ctx = cairo_create(xcb_output);
/* After the first iteration, the pixmap will still contain the previous
* contents. Explicitly clear the entire pixmap with the background color
* first to get back into a defined state: */
char strgroups[3][3] = {{color[0], color[1], '\0'},
{color[2], color[3], '\0'},
{color[4], color[5], '\0'}};
uint32_t rgb16[3] = {(strtol(strgroups[0], NULL, 16)),
(strtol(strgroups[1], NULL, 16)),
(strtol(strgroups[2], NULL, 16))};
cairo_set_source_rgb(xcb_ctx, rgb16[0] / 255.0, rgb16[1] / 255.0, rgb16[2] / 255.0);
cairo_rectangle(xcb_ctx, 0, 0, resolution[0], resolution[1]);
cairo_fill(xcb_ctx);
if (img) {
if (!tile) {
cairo_set_source_surface(xcb_ctx, img, 0, 0);
cairo_paint(xcb_ctx);
} else {
/* create a pattern and fill a rectangle as big as the screen */
cairo_pattern_t *pattern;
pattern = cairo_pattern_create_for_surface(img);
cairo_set_source(xcb_ctx, pattern);
cairo_pattern_set_extend(pattern, CAIRO_EXTEND_REPEAT);
cairo_rectangle(xcb_ctx, 0, 0, resolution[0], resolution[1]);
cairo_fill(xcb_ctx);
cairo_pattern_destroy(pattern);
}
}
if (unlock_indicator &&
(unlock_state >= STATE_KEY_PRESSED || auth_state > STATE_AUTH_IDLE)) {
cairo_scale(ctx, scaling_factor, scaling_factor);
/* Draw a (centered) circle with transparent background. */
cairo_set_line_width(ctx, 10.0);
cairo_arc(ctx,
BUTTON_CENTER /* x */,
BUTTON_CENTER /* y */,
BUTTON_RADIUS /* radius */,
0 /* start */,
2 * M_PI /* end */);
/* Use the appropriate color for the different PAM states
* (currently verifying, wrong password, or default) */
switch (auth_state) {
case STATE_AUTH_VERIFY:
case STATE_AUTH_LOCK:
cairo_set_source_rgba(ctx, 0, 114.0 / 255, 255.0 / 255, 0.75);
break;
case STATE_AUTH_WRONG:
case STATE_I38LOCK_LOCK_FAILED:
cairo_set_source_rgba(ctx, 250.0 / 255, 0, 0, 0.75);
break;
default:
if (unlock_state == STATE_NOTHING_TO_DELETE) {
cairo_set_source_rgba(ctx, 250.0 / 255, 0, 0, 0.75);
break;
}
cairo_set_source_rgba(ctx, 0, 0, 0, 0.75);
break;
}
cairo_fill_preserve(ctx);
bool use_dark_text = true;
switch (auth_state) {
case STATE_AUTH_VERIFY:
case STATE_AUTH_LOCK:
cairo_set_source_rgb(ctx, 51.0 / 255, 0, 250.0 / 255);
break;
case STATE_AUTH_WRONG:
case STATE_I38LOCK_LOCK_FAILED:
cairo_set_source_rgb(ctx, 125.0 / 255, 51.0 / 255, 0);
break;
case STATE_AUTH_IDLE:
if (unlock_state == STATE_NOTHING_TO_DELETE) {
cairo_set_source_rgb(ctx, 125.0 / 255, 51.0 / 255, 0);
break;
}
cairo_set_source_rgb(ctx, 51.0 / 255, 125.0 / 255, 0);
use_dark_text = false;
break;
}
cairo_stroke(ctx);
/* Draw an inner seperator line. */
cairo_set_source_rgb(ctx, 0, 0, 0);
cairo_set_line_width(ctx, 2.0);
cairo_arc(ctx,
BUTTON_CENTER /* x */,
BUTTON_CENTER /* y */,
BUTTON_RADIUS - 5 /* radius */,
0,
2 * M_PI);
cairo_stroke(ctx);
cairo_set_line_width(ctx, 10.0);
/* Display a (centered) text of the current PAM state. */
char *text = NULL;
/* We don't want to show more than a 3-digit number. */
char buf[4];
cairo_set_source_rgb(ctx, 0, 0, 0);
cairo_select_font_face(ctx, "sans-serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
cairo_set_font_size(ctx, 28.0);
switch (auth_state) {
case STATE_AUTH_VERIFY:
text = "Verifying…";
break;
case STATE_AUTH_LOCK:
text = "Locking…";
break;
case STATE_AUTH_WRONG:
text = "Wrong!";
break;
case STATE_I38LOCK_LOCK_FAILED:
text = "Lock failed!";
break;
default:
if (unlock_state == STATE_NOTHING_TO_DELETE) {
text = "No input";
}
if (show_failed_attempts && failed_attempts > 0) {
if (failed_attempts > 999) {
text = "> 999";
} else {
snprintf(buf, sizeof(buf), "%d", failed_attempts);
text = buf;
}
cairo_set_source_rgb(ctx, 1, 0, 0);
cairo_set_font_size(ctx, 32.0);
}
break;
}
if (text) {
display_button_text(ctx, text, 0., use_dark_text);
}
if (modifier_string != NULL) {
cairo_set_font_size(ctx, 14.0);
display_button_text(ctx, modifier_string, 28., use_dark_text);
}
if (show_keyboard_layout && layout_string != NULL) {
cairo_set_font_size(ctx, 14.0);
display_button_text(ctx, layout_string, -28., use_dark_text);
}
/* After the user pressed any valid key or the backspace key, we
* highlight a random part of the unlock indicator to confirm this
* keypress. */
if (unlock_state == STATE_KEY_ACTIVE ||
unlock_state == STATE_BACKSPACE_ACTIVE) {
cairo_new_sub_path(ctx);
double highlight_start = (rand() % (int)(2 * M_PI * 100)) / 100.0;
cairo_arc(ctx,
BUTTON_CENTER /* x */,
BUTTON_CENTER /* y */,
BUTTON_RADIUS /* radius */,
highlight_start,
highlight_start + (M_PI / 3.0));
if (unlock_state == STATE_KEY_ACTIVE) {
/* For normal keys, we use a lighter green. */
cairo_set_source_rgb(ctx, 51.0 / 255, 219.0 / 255, 0);
} else {
/* For backspace, we use red. */
cairo_set_source_rgb(ctx, 219.0 / 255, 51.0 / 255, 0);
}
cairo_stroke(ctx);
/* Draw two little separators for the highlighted part of the
* unlock indicator. */
cairo_set_source_rgb(ctx, 0, 0, 0);
cairo_arc(ctx,
BUTTON_CENTER /* x */,
BUTTON_CENTER /* y */,
BUTTON_RADIUS /* radius */,
highlight_start /* start */,
highlight_start + (M_PI / 128.0) /* end */);
cairo_stroke(ctx);
cairo_arc(ctx,
BUTTON_CENTER /* x */,
BUTTON_CENTER /* y */,
BUTTON_RADIUS /* radius */,
(highlight_start + (M_PI / 3.0)) - (M_PI / 128.0) /* start */,
highlight_start + (M_PI / 3.0) /* end */);
cairo_stroke(ctx);
}
}
if (xr_screens > 0) {
/* Composite the unlock indicator in the middle of each screen. */
for (int screen = 0; screen < xr_screens; screen++) {
int x = (xr_resolutions[screen].x + ((xr_resolutions[screen].width / 2) - (button_diameter_physical / 2)));
int y = (xr_resolutions[screen].y + ((xr_resolutions[screen].height / 2) - (button_diameter_physical / 2)));
cairo_set_source_surface(xcb_ctx, output, x, y);
cairo_rectangle(xcb_ctx, x, y, button_diameter_physical, button_diameter_physical);
cairo_fill(xcb_ctx);
}
} else {
/* We have no information about the screen sizes/positions, so we just
* place the unlock indicator in the middle of the X root window and
* hope for the best. */
int x = (last_resolution[0] / 2) - (button_diameter_physical / 2);
int y = (last_resolution[1] / 2) - (button_diameter_physical / 2);
cairo_set_source_surface(xcb_ctx, output, x, y);
cairo_rectangle(xcb_ctx, x, y, button_diameter_physical, button_diameter_physical);
cairo_fill(xcb_ctx);
}
cairo_surface_destroy(xcb_output);
cairo_surface_destroy(output);
cairo_destroy(ctx);
cairo_destroy(xcb_ctx);
}
static xcb_pixmap_t bg_pixmap = XCB_NONE;
/*
* Releases the current background pixmap so that the next redraw_screen() call
* will allocate a new one with the updated resolution.
*
*/
void free_bg_pixmap(void) {
xcb_free_pixmap(conn, bg_pixmap);
bg_pixmap = XCB_NONE;
}
/*
* Calls draw_image on a new pixmap and swaps that with the current pixmap
*
*/
void redraw_screen(void) {
DEBUG("redraw_screen(unlock_state = %d, auth_state = %d)\n", unlock_state, auth_state);
if (modifier_string) {
free(modifier_string);
modifier_string = NULL;
}
check_modifier_keys();
update_layout_string();
if (bg_pixmap == XCB_NONE) {
DEBUG("allocating pixmap for %d x %d px\n", last_resolution[0], last_resolution[1]);
bg_pixmap = create_bg_pixmap(conn, screen, last_resolution, color);
}
draw_image(bg_pixmap, last_resolution);
xcb_change_window_attributes(conn, win, XCB_CW_BACK_PIXMAP, (uint32_t[1]){bg_pixmap});
/* XXX: Possible optimization: Only update the area in the middle of the
* screen instead of the whole screen. */
xcb_clear_area(conn, 0, win, 0, 0, last_resolution[0], last_resolution[1]);
xcb_flush(conn);
}
/*
* Hides the unlock indicator completely when there is no content in the
* password buffer.
*
*/
void clear_indicator(void) {
if (input_position == 0) {
unlock_state = STATE_STARTED;
} else {
unlock_state = STATE_KEY_PRESSED;
}
redraw_screen();
}
+435
View File
@@ -0,0 +1,435 @@
/*
* vim:ts=4:sw=4:expandtab
*
* © 2010 Michael Stapelberg
*
* xcb.c: contains all functions which use XCB to talk to X11. Mostly wrappers
* around the rather complicated/ugly parts of the XCB API.
*
*/
#include <xcb/xcb.h>
#include <xcb/xcb_image.h>
#include <xcb/xcb_atom.h>
#include <xcb/xcb_aux.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <err.h>
#include <time.h>
#include <sys/time.h>
#include "cursors.h"
#include "unlock_indicator.h"
extern auth_state_t auth_state;
xcb_connection_t *conn;
xcb_screen_t *screen;
static xcb_atom_t _NET_WM_BYPASS_COMPOSITOR = XCB_NONE;
void _init_net_wm_bypass_compositor(xcb_connection_t *conn) {
if (_NET_WM_BYPASS_COMPOSITOR != XCB_NONE) {
/* already initialized */
return;
}
xcb_generic_error_t *err;
xcb_intern_atom_reply_t *atom_reply = xcb_intern_atom_reply(
conn,
xcb_intern_atom(conn, 0, strlen("_NET_WM_BYPASS_COMPOSITOR"), "_NET_WM_BYPASS_COMPOSITOR"),
&err);
if (atom_reply == NULL) {
fprintf(stderr, "X11 Error %d\n", err->error_code);
free(err);
return;
}
_NET_WM_BYPASS_COMPOSITOR = atom_reply->atom;
free(atom_reply);
}
#define curs_invisible_width 8
#define curs_invisible_height 8
static unsigned char curs_invisible_bits[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
#define curs_windows_width 11
#define curs_windows_height 19
static unsigned char curs_windows_bits[] = {
0xfe, 0x07, 0xfc, 0x07, 0xfa, 0x07, 0xf6, 0x07, 0xee, 0x07, 0xde, 0x07,
0xbe, 0x07, 0x7e, 0x07, 0xfe, 0x06, 0xfe, 0x05, 0x3e, 0x00, 0xb6, 0x07,
0x6a, 0x07, 0x6c, 0x07, 0xde, 0x06, 0xdf, 0x06, 0xbf, 0x05, 0xbf, 0x05,
0x7f, 0x06};
#define mask_windows_width 11
#define mask_windows_height 19
static unsigned char mask_windows_bits[] = {
0x01, 0x00, 0x03, 0x00, 0x07, 0x00, 0x0f, 0x00, 0x1f, 0x00, 0x3f, 0x00,
0x7f, 0x00, 0xff, 0x00, 0xff, 0x01, 0xff, 0x03, 0xff, 0x07, 0x7f, 0x00,
0xf7, 0x00, 0xf3, 0x00, 0xe1, 0x01, 0xe0, 0x01, 0xc0, 0x03, 0xc0, 0x03,
0x80, 0x01};
static uint32_t get_colorpixel(char *hex) {
char strgroups[3][3] = {{hex[0], hex[1], '\0'},
{hex[2], hex[3], '\0'},
{hex[4], hex[5], '\0'}};
uint32_t rgb16[3] = {(strtol(strgroups[0], NULL, 16)),
(strtol(strgroups[1], NULL, 16)),
(strtol(strgroups[2], NULL, 16))};
return (rgb16[0] << 16) + (rgb16[1] << 8) + rgb16[2];
}
xcb_visualtype_t *get_root_visual_type(xcb_screen_t *screen) {
xcb_visualtype_t *visual_type = NULL;
xcb_depth_iterator_t depth_iter;
xcb_visualtype_iterator_t visual_iter;
for (depth_iter = xcb_screen_allowed_depths_iterator(screen);
depth_iter.rem;
xcb_depth_next(&depth_iter)) {
for (visual_iter = xcb_depth_visuals_iterator(depth_iter.data);
visual_iter.rem;
xcb_visualtype_next(&visual_iter)) {
if (screen->root_visual != visual_iter.data->visual_id) {
continue;
}
visual_type = visual_iter.data;
return visual_type;
}
}
return NULL;
}
xcb_pixmap_t create_bg_pixmap(xcb_connection_t *conn, xcb_screen_t *scr, u_int32_t *resolution, char *color) {
xcb_pixmap_t bg_pixmap = xcb_generate_id(conn);
xcb_create_pixmap(conn, scr->root_depth, bg_pixmap, scr->root,
resolution[0], resolution[1]);
/* Generate a Graphics Context and fill the pixmap with background color
* (for images that are smaller than your screen) */
xcb_gcontext_t gc = xcb_generate_id(conn);
uint32_t values[] = {get_colorpixel(color)};
xcb_create_gc(conn, gc, bg_pixmap, XCB_GC_FOREGROUND, values);
xcb_rectangle_t rect = {0, 0, resolution[0], resolution[1]};
xcb_poly_fill_rectangle(conn, bg_pixmap, gc, 1, &rect);
xcb_free_gc(conn, gc);
return bg_pixmap;
}
xcb_window_t open_fullscreen_window(xcb_connection_t *conn, xcb_screen_t *scr, char *color, xcb_pixmap_t pixmap) {
uint32_t mask = 0;
uint32_t values[3];
xcb_window_t win = xcb_generate_id(conn);
if (pixmap == XCB_NONE) {
mask |= XCB_CW_BACK_PIXEL;
values[0] = get_colorpixel(color);
} else {
mask |= XCB_CW_BACK_PIXMAP;
values[0] = pixmap;
}
mask |= XCB_CW_OVERRIDE_REDIRECT;
values[1] = 1;
mask |= XCB_CW_EVENT_MASK;
values[2] = XCB_EVENT_MASK_EXPOSURE |
XCB_EVENT_MASK_KEY_PRESS |
XCB_EVENT_MASK_KEY_RELEASE |
XCB_EVENT_MASK_VISIBILITY_CHANGE |
XCB_EVENT_MASK_STRUCTURE_NOTIFY;
xcb_create_window(conn,
XCB_COPY_FROM_PARENT,
win, /* the window id */
scr->root, /* parent == root */
0, 0,
scr->width_in_pixels,
scr->height_in_pixels, /* dimensions */
0, /* border = 0, we draw our own */
XCB_WINDOW_CLASS_INPUT_OUTPUT,
XCB_WINDOW_CLASS_COPY_FROM_PARENT, /* copy visual from parent */
mask,
values);
char *name = "i38lock";
xcb_change_property(conn,
XCB_PROP_MODE_REPLACE,
win,
XCB_ATOM_WM_NAME,
XCB_ATOM_STRING,
8,
strlen(name),
name);
xcb_change_property(conn,
XCB_PROP_MODE_REPLACE,
win,
XCB_ATOM_WM_CLASS,
XCB_ATOM_STRING,
8,
2 * (strlen("i38lock") + 1),
"i38lock\0i38lock\0");
const uint32_t bypass_compositor = 1; /* disable compositing */
_init_net_wm_bypass_compositor(conn);
xcb_change_property(conn,
XCB_PROP_MODE_REPLACE,
win,
_NET_WM_BYPASS_COMPOSITOR,
XCB_ATOM_CARDINAL,
32,
1,
&bypass_compositor);
/* Map the window (= make it visible) */
xcb_map_window(conn, win);
/* Raise window (put it on top) */
values[0] = XCB_STACK_MODE_ABOVE;
xcb_configure_window(conn, win, XCB_CONFIG_WINDOW_STACK_MODE, values);
/* Ensure that the window is created and set up before returning */
xcb_aux_sync(conn);
return win;
}
/*
* Repeatedly tries to grab pointer and keyboard (up to the specified number of
* tries).
*
* Returns true if the grab succeeded, false if not.
*
*/
bool grab_pointer_and_keyboard(xcb_connection_t *conn, xcb_screen_t *screen, xcb_cursor_t cursor, int tries) {
xcb_grab_pointer_cookie_t pcookie;
xcb_grab_pointer_reply_t *preply;
xcb_grab_keyboard_cookie_t kcookie;
xcb_grab_keyboard_reply_t *kreply;
const suseconds_t screen_redraw_timeout = 100000; /* 100ms */
/* Using few variables to trigger a redraw_screen() if too many tries */
bool redrawn = false;
struct timeval start;
if (gettimeofday(&start, NULL) == -1) {
err(EXIT_FAILURE, "gettimeofday");
}
while (tries-- > 0) {
pcookie = xcb_grab_pointer(
conn,
false, /* get all pointer events specified by the following mask */
screen->root, /* grab the root window */
XCB_NONE, /* which events to let through */
XCB_GRAB_MODE_ASYNC, /* pointer events should continue as normal */
XCB_GRAB_MODE_ASYNC, /* keyboard mode */
XCB_NONE, /* confine_to = in which window should the cursor stay */
cursor, /* we change the cursor to whatever the user wanted */
XCB_CURRENT_TIME);
if ((preply = xcb_grab_pointer_reply(conn, pcookie, NULL)) &&
preply->status == XCB_GRAB_STATUS_SUCCESS) {
free(preply);
break;
}
/* In case the grab failed, we still need to free the reply */
free(preply);
/* Make this quite a bit slower */
usleep(50);
struct timeval now;
if (gettimeofday(&now, NULL) == -1) {
err(EXIT_FAILURE, "gettimeofday");
}
struct timeval elapsed;
timersub(&now, &start, &elapsed);
if (!redrawn &&
(tries % 100) == 0 &&
elapsed.tv_usec >= screen_redraw_timeout) {
redraw_screen();
redrawn = true;
}
}
while (tries-- > 0) {
kcookie = xcb_grab_keyboard(
conn,
true, /* report events */
screen->root, /* grab the root window */
XCB_CURRENT_TIME,
XCB_GRAB_MODE_ASYNC, /* process events as normal, do not require sync */
XCB_GRAB_MODE_ASYNC);
if ((kreply = xcb_grab_keyboard_reply(conn, kcookie, NULL)) &&
kreply->status == XCB_GRAB_STATUS_SUCCESS) {
free(kreply);
break;
}
/* In case the grab failed, we still need to free the reply */
free(kreply);
/* Make this quite a bit slower */
usleep(50);
struct timeval now;
if (gettimeofday(&now, NULL) == -1) {
err(EXIT_FAILURE, "gettimeofday");
}
struct timeval elapsed;
timersub(&now, &start, &elapsed);
/* Trigger a screen redraw if 100ms elapsed */
if (!redrawn &&
(tries % 100) == 0 &&
elapsed.tv_usec >= screen_redraw_timeout) {
redraw_screen();
redrawn = true;
}
}
return (tries > 0);
}
xcb_cursor_t create_cursor(xcb_connection_t *conn, xcb_screen_t *screen, xcb_window_t win, int choice) {
xcb_pixmap_t bitmap;
xcb_pixmap_t mask;
xcb_cursor_t cursor;
unsigned char *curs_bits;
unsigned char *mask_bits;
int curs_w, curs_h;
switch (choice) {
case CURS_NONE:
curs_bits = curs_invisible_bits;
mask_bits = curs_invisible_bits;
curs_w = curs_invisible_width;
curs_h = curs_invisible_height;
break;
case CURS_WIN:
curs_bits = curs_windows_bits;
mask_bits = mask_windows_bits;
curs_w = curs_windows_width;
curs_h = curs_windows_height;
break;
case CURS_DEFAULT:
default:
return XCB_NONE; /* XCB_NONE is xcb's way of saying "don't change the cursor" */
}
bitmap = xcb_create_pixmap_from_bitmap_data(conn,
win,
curs_bits,
curs_w,
curs_h,
1,
screen->white_pixel,
screen->black_pixel,
NULL);
mask = xcb_create_pixmap_from_bitmap_data(conn,
win,
mask_bits,
curs_w,
curs_h,
1,
screen->white_pixel,
screen->black_pixel,
NULL);
cursor = xcb_generate_id(conn);
xcb_create_cursor(conn,
cursor,
bitmap,
mask,
65535, 65535, 65535,
0, 0, 0,
0, 0);
xcb_free_pixmap(conn, bitmap);
xcb_free_pixmap(conn, mask);
return cursor;
}
static xcb_atom_t _NET_ACTIVE_WINDOW = XCB_NONE;
void _init_net_active_window(xcb_connection_t *conn) {
if (_NET_ACTIVE_WINDOW != XCB_NONE) {
/* already initialized */
return;
}
xcb_generic_error_t *err;
xcb_intern_atom_reply_t *atom_reply = xcb_intern_atom_reply(
conn,
xcb_intern_atom(conn, 0, strlen("_NET_ACTIVE_WINDOW"), "_NET_ACTIVE_WINDOW"),
&err);
if (atom_reply == NULL) {
fprintf(stderr, "X11 Error %d\n", err->error_code);
free(err);
return;
}
_NET_ACTIVE_WINDOW = atom_reply->atom;
free(atom_reply);
}
xcb_window_t find_focused_window(xcb_connection_t *conn, const xcb_window_t root) {
xcb_window_t result = XCB_NONE;
_init_net_active_window(conn);
xcb_get_property_reply_t *prop_reply = xcb_get_property_reply(
conn,
xcb_get_property_unchecked(
conn, false, root, _NET_ACTIVE_WINDOW, XCB_GET_PROPERTY_TYPE_ANY, 0, 1 /* word */),
NULL);
if (prop_reply == NULL) {
goto out;
}
if (xcb_get_property_value_length(prop_reply) == 0) {
goto out_prop;
}
if (prop_reply->type != XCB_ATOM_WINDOW) {
goto out_prop;
}
result = *((xcb_window_t *)xcb_get_property_value(prop_reply));
out_prop:
free(prop_reply);
out:
return result;
}
void set_focused_window(xcb_connection_t *conn, const xcb_window_t root, const xcb_window_t window) {
xcb_client_message_event_t ev;
memset(&ev, '\0', sizeof(xcb_client_message_event_t));
_init_net_active_window(conn);
ev.response_type = XCB_CLIENT_MESSAGE;
ev.window = window;
ev.type = _NET_ACTIVE_WINDOW;
ev.format = 32;
ev.data.data32[0] = 2; /* 2 = pager */
xcb_send_event(conn, false, root, XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT, (char *)&ev);
xcb_flush(conn);
}