slock: pantalla con reloj, fecha, batería y barra de estado

El fondo es fijo y el color del estado (inicio, escribiendo, error) pasa a
una barra inferior. Se muestran la hora, la fecha, un punto por carácter
tecleado, el usuario y la batería, refrescando reloj y batería cada
segundo. Usa Xft y los iconos de Symbols Nerd Font.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016UdJsHrbkmzhoCb6TthP2m
This commit is contained in:
Alejandro Guerrero 2026-09-27 14:29:49 +02:00
parent c736ea8b01
commit ce3a0db5bf
4 changed files with 382 additions and 34 deletions

View file

@ -8,5 +8,21 @@ static const char *colorname[NUMCOLS] = {
[FAILED] = "#CC3333", /* wrong password */ [FAILED] = "#CC3333", /* wrong password */
}; };
/* lock screen */
static const char *bgcolor = "black";
static const char *fgcolor = "#eeeeee";
static const char *dimcolor = "#888888";
static const char *clockfont = "monospace:bold:pixelsize=160";
static const char *datefont = "monospace:pixelsize=34";
static const char *textfont = "monospace:pixelsize=22";
static const char *iconfont = "Symbols Nerd Font Mono:pixelsize=24";
static const int barheight = 10; /* height of the bottom bar */
static const unsigned int maxdots = 24; /* max dots shown while typing */
static const char *battery = "BAT0"; /* /sys/class/power_supply/... */
static const int batterylow = 20; /* percent shown as low */
/* treat a cleared input like a wrong password (color) */ /* treat a cleared input like a wrong password (color) */
static const int failonclear = 1; static const int failonclear = 1;

View file

@ -2,11 +2,28 @@
static const char *user = "nobody"; static const char *user = "nobody";
static const char *group = "nogroup"; static const char *group = "nogroup";
/* colores de la barra inferior según el estado */
static const char *colorname[NUMCOLS] = { static const char *colorname[NUMCOLS] = {
[INIT] = "#2d353b", /* after initialization (fondo Everforest) */ [INIT] = "#475258", /* after initialization (gris Everforest) */
[INPUT] = "#a7c080", /* during input (verde acento) */ [INPUT] = "#a7c080", /* during input (verde acento) */
[FAILED] = "#e67e80", /* wrong password (rojo Everforest) */ [FAILED] = "#e67e80", /* wrong password (rojo Everforest) */
}; };
/* pantalla de bloqueo (paleta Everforest) */
static const char *bgcolor = "#2d353b"; /* fondo */
static const char *fgcolor = "#d3c6aa"; /* reloj y texto */
static const char *dimcolor = "#859289"; /* fecha y avisos */
static const char *clockfont = "Fira Code:style=Bold:pixelsize=160:antialias=true";
static const char *datefont = "Fira Code:pixelsize=34:antialias=true";
static const char *textfont = "Fira Code:pixelsize=22:antialias=true";
static const char *iconfont = "Symbols Nerd Font Mono:pixelsize=24:antialias=true";
static const int barheight = 10; /* alto de la barra inferior */
static const unsigned int maxdots = 24; /* puntos máximos al teclear */
static const char *battery = "BAT1"; /* /sys/class/power_supply/... */
static const int batterylow = 20; /* % para mostrarla en rojo */
/* treat a cleared input like a wrong password (color) */ /* treat a cleared input like a wrong password (color) */
static const int failonclear = 1; static const int failonclear = 1;

View file

@ -11,8 +11,8 @@ X11INC = /usr/X11R6/include
X11LIB = /usr/X11R6/lib X11LIB = /usr/X11R6/lib
# includes and libs # includes and libs
INCS = -I. -I/usr/include -I${X11INC} INCS = -I. -I/usr/include -I${X11INC} -I/usr/include/freetype2
LIBS = -L/usr/lib -lc -lcrypt -L${X11LIB} -lX11 -lXext -lXrandr LIBS = -L/usr/lib -lc -lcrypt -L${X11LIB} -lX11 -lXext -lXrandr -lXft -lfontconfig
# flags # flags
CPPFLAGS = -DVERSION=\"${VERSION}\" -D_DEFAULT_SOURCE -DHAVE_SHADOW_H CPPFLAGS = -DVERSION=\"${VERSION}\" -D_DEFAULT_SOURCE -DHAVE_SHADOW_H
@ -21,7 +21,7 @@ LDFLAGS = -s ${LIBS}
COMPATSRC = explicit_bzero.c COMPATSRC = explicit_bzero.c
# On OpenBSD and Darwin remove -lcrypt from LIBS # On OpenBSD and Darwin remove -lcrypt from LIBS
#LIBS = -L/usr/lib -lc -L${X11LIB} -lX11 -lXext -lXrandr #LIBS = -L/usr/lib -lc -L${X11LIB} -lX11 -lXext -lXrandr -lXft -lfontconfig
# On *BSD remove -DHAVE_SHADOW_H from CPPFLAGS # On *BSD remove -DHAVE_SHADOW_H from CPPFLAGS
# On NetBSD add -D_NETBSD_SOURCE to CPPFLAGS # On NetBSD add -D_NETBSD_SOURCE to CPPFLAGS
#CPPFLAGS = -DVERSION=\"${VERSION}\" -D_BSD_SOURCE -D_NETBSD_SOURCE #CPPFLAGS = -DVERSION=\"${VERSION}\" -D_BSD_SOURCE -D_NETBSD_SOURCE

View file

@ -12,12 +12,15 @@
#include <stdlib.h> #include <stdlib.h>
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <time.h>
#include <unistd.h> #include <unistd.h>
#include <sys/select.h>
#include <sys/types.h> #include <sys/types.h>
#include <X11/extensions/Xrandr.h> #include <X11/extensions/Xrandr.h>
#include <X11/keysym.h> #include <X11/keysym.h>
#include <X11/Xlib.h> #include <X11/Xlib.h>
#include <X11/Xutil.h> #include <X11/Xutil.h>
#include <X11/Xft/Xft.h>
#include "util.h" #include "util.h"
@ -30,13 +33,40 @@ enum {
NUMCOLS NUMCOLS
}; };
enum {
COLBG,
COLFG,
COLDIM,
COLCHARGE,
COLLOW,
NUMUICOLS
};
struct lock { struct lock {
int screen; int screen;
Window root, win; Window root, win;
Pixmap pmap; Pixmap pmap;
unsigned long colors[NUMCOLS]; unsigned long colors[NUMCOLS];
/* dibujado: doble búfer del tamaño de la pantalla */
int w, h;
Pixmap buf;
XftDraw *draw;
XftColor bar[NUMCOLS];
XftColor ui[NUMUICOLS];
XftFont *fclock, *fdate, *ftext, *ficon;
}; };
/* estado que se muestra en la pantalla de bloqueo */
struct state {
unsigned int len; /* caracteres tecleados */
int color; /* INIT, INPUT o FAILED */
int failure; /* ha habido una contraseña incorrecta */
char clock[16], date[64], bat[16];
int batlevel, charging;
};
static char username[64];
struct xrandr { struct xrandr {
int active; int active;
int evbase; int evbase;
@ -123,23 +153,296 @@ gethash(void)
return hash; return hash;
} }
static XftFont *
loadfont(Display *dpy, int screen, const char *name)
{
XftFont *f;
if (!(f = XftFontOpenName(dpy, screen, name)) &&
!(f = XftFontOpenName(dpy, screen, "monospace")))
die("slock: cannot load font %s\n", name);
return f;
}
static void
setupdraw(Display *dpy, struct lock *lock)
{
Visual *vis = DefaultVisual(dpy, lock->screen);
Colormap cmap = DefaultColormap(dpy, lock->screen);
const char *uinames[NUMUICOLS] = {
[COLBG] = bgcolor, [COLFG] = fgcolor, [COLDIM] = dimcolor,
[COLCHARGE] = colorname[INPUT], [COLLOW] = colorname[FAILED],
};
int i;
for (i = 0; i < NUMCOLS; i++)
if (!XftColorAllocName(dpy, vis, cmap, colorname[i], &lock->bar[i]))
die("slock: cannot allocate color %s\n", colorname[i]);
for (i = 0; i < NUMUICOLS; i++)
if (!XftColorAllocName(dpy, vis, cmap, uinames[i], &lock->ui[i]))
die("slock: cannot allocate color %s\n", uinames[i]);
lock->fclock = loadfont(dpy, lock->screen, clockfont);
lock->fdate = loadfont(dpy, lock->screen, datefont);
lock->ftext = loadfont(dpy, lock->screen, textfont);
lock->ficon = loadfont(dpy, lock->screen, iconfont);
lock->w = DisplayWidth(dpy, lock->screen);
lock->h = DisplayHeight(dpy, lock->screen);
lock->buf = XCreatePixmap(dpy, lock->root, lock->w, lock->h,
DefaultDepth(dpy, lock->screen));
lock->draw = XftDrawCreate(dpy, lock->buf, vis, cmap);
}
static void
resizedraw(Display *dpy, struct lock *lock, int w, int h)
{
if (w == lock->w && h == lock->h)
return;
lock->w = w;
lock->h = h;
XFreePixmap(dpy, lock->buf);
lock->buf = XCreatePixmap(dpy, lock->root, w, h,
DefaultDepth(dpy, lock->screen));
XftDrawChange(lock->draw, lock->buf);
}
static int
textw(Display *dpy, XftFont *f, const char *s)
{
XGlyphInfo ext;
XftTextExtentsUtf8(dpy, f, (const FcChar8 *)s, strlen(s), &ext);
return ext.xOff;
}
static int
iconw(Display *dpy, XftFont *f, FcChar32 icon)
{
XGlyphInfo ext;
if (!icon || !XftCharExists(dpy, f, icon))
return 0;
XftTextExtents32(dpy, f, &icon, 1, &ext);
return ext.xOff;
}
/* icono (opcional) + separación + texto, con la línea base en y */
static int
drawlabel(Display *dpy, struct lock *lock, XftFont *f, XftColor *c,
int x, int y, FcChar32 icon, const char *s)
{
int iw = iconw(dpy, lock->ficon, icon), gap = iw ? f->height / 2 : 0;
if (iw)
XftDrawString32(lock->draw, c, lock->ficon, x, y, &icon, 1);
XftDrawStringUtf8(lock->draw, c, f, x + iw + gap, y,
(const FcChar8 *)s, strlen(s));
return iw + gap + textw(dpy, f, s);
}
static int
labelw(Display *dpy, struct lock *lock, XftFont *f, FcChar32 icon,
const char *s)
{
int iw = iconw(dpy, lock->ficon, icon);
return iw + (iw ? f->height / 2 : 0) + textw(dpy, f, s);
}
/* Actualiza hora, fecha y batería. Devuelve 1 si algo ha cambiado. */
static int
updatestate(struct state *st)
{
static const char *wdays[] = { "domingo", "lunes", "martes",
"miércoles", "jueves", "viernes", "sábado" };
static const char *months[] = { "enero", "febrero", "marzo", "abril",
"mayo", "junio", "julio", "agosto", "septiembre", "octubre",
"noviembre", "diciembre" };
char clk[sizeof(st->clock)], date[sizeof(st->date)], bat[sizeof(st->bat)];
char path[128], status[32] = "";
int level = -1, charging = 0, changed;
time_t t = time(NULL);
struct tm *tm = localtime(&t);
FILE *f;
strftime(clk, sizeof(clk), "%H:%M", tm);
snprintf(date, sizeof(date), "%s, %d de %s", wdays[tm->tm_wday],
tm->tm_mday, months[tm->tm_mon]);
date[0] = toupper((unsigned char)date[0]);
snprintf(path, sizeof(path), "/sys/class/power_supply/%s/capacity",
battery);
if ((f = fopen(path, "r"))) {
if (fscanf(f, "%d", &level) != 1)
level = -1;
fclose(f);
}
snprintf(path, sizeof(path), "/sys/class/power_supply/%s/status",
battery);
if ((f = fopen(path, "r"))) {
if (fscanf(f, "%31s", status) != 1)
status[0] = '\0';
fclose(f);
}
charging = !strcmp(status, "Charging") || !strcmp(status, "Full");
if (level >= 0)
snprintf(bat, sizeof(bat), "%d%%", level);
else
bat[0] = '\0';
changed = strcmp(clk, st->clock) || strcmp(date, st->date) ||
strcmp(bat, st->bat) || charging != st->charging;
memcpy(st->clock, clk, sizeof(clk));
memcpy(st->date, date, sizeof(date));
memcpy(st->bat, bat, sizeof(bat));
st->batlevel = level;
st->charging = charging;
return changed;
}
/* Símbolos de Nerd Font (Material Design) */
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define ICON_LOCK 0xF033E
#define ICON_ALERT 0xF0026
#define ICON_USER 0xF0004
#define ICON_CHARGING 0xF0084
#define ICON_BAT10 0xF007A /* 10 % .. 90 % seguidos; 100 % es 0xF0079 */
#define ICON_BAT100 0xF0079
static FcChar32
baticon(const struct state *st)
{
if (st->charging)
return ICON_CHARGING;
if (st->batlevel >= 95)
return ICON_BAT100;
if (st->batlevel < 10)
return ICON_ALERT;
return ICON_BAT10 + st->batlevel / 10 - 1;
}
/* Dibuja la pantalla de bloqueo en el rectángulo de un monitor */
static void
drawmon(Display *dpy, struct lock *lock, const struct state *st,
int mx, int my, int mw, int mh)
{
XftColor *fg = &lock->ui[COLFG], *dim = &lock->ui[COLDIM], *msgc;
char msg[128];
FcChar32 icon = 0;
int x, y, w, pad = mh / 24, low;
unsigned int i, n;
/* reloj y fecha, algo por encima del centro */
y = my + mh * 2 / 5;
x = mx + (mw - textw(dpy, lock->fclock, st->clock)) / 2;
XftDrawStringUtf8(lock->draw, fg, lock->fclock, x, y,
(const FcChar8 *)st->clock, strlen(st->clock));
y += lock->fdate->height * 3 / 2;
x = mx + (mw - textw(dpy, lock->fdate, st->date)) / 2;
XftDrawStringUtf8(lock->draw, dim, lock->fdate, x, y,
(const FcChar8 *)st->date, strlen(st->date));
/* mensaje de estado: indicación, puntos o error */
msgc = dim;
if (st->color == INPUT) {
n = MIN(st->len, maxdots);
for (i = 0, msg[0] = '\0'; i < n; i++)
strcat(msg, i ? " ●" : "●");
msgc = &lock->bar[INPUT];
} else if (st->color == FAILED && st->failure) {
icon = ICON_ALERT;
snprintf(msg, sizeof(msg), "Contraseña incorrecta");
msgc = &lock->bar[FAILED];
} else {
icon = ICON_LOCK;
snprintf(msg, sizeof(msg), "Escribe la contraseña");
}
y = my + mh * 3 / 5 + lock->ftext->ascent;
x = mx + (mw - labelw(dpy, lock, lock->ftext, icon, msg)) / 2;
drawlabel(dpy, lock, lock->ftext, msgc, x, y, icon, msg);
/* abajo: usuario a la izquierda, batería a la derecha */
y = my + mh - barheight - pad;
drawlabel(dpy, lock, lock->ftext, dim, mx + pad, y, ICON_USER, username);
if (st->bat[0]) {
low = !st->charging && st->batlevel <= batterylow;
w = labelw(dpy, lock, lock->ftext, baticon(st), st->bat);
drawlabel(dpy, lock, lock->ftext,
low ? &lock->ui[COLLOW] :
st->charging ? &lock->ui[COLCHARGE] : fg,
mx + mw - pad - w, y, baticon(st), st->bat);
}
/* barra con el color del estado */
XftDrawRect(lock->draw, &lock->bar[st->color], mx, my + mh - barheight,
mw, barheight);
}
static void
drawlock(Display *dpy, struct lock *lock, const struct state *st)
{
XRRMonitorInfo *mons;
int i, n = 0;
XftDrawRect(lock->draw, &lock->ui[COLBG], 0, 0, lock->w, lock->h);
mons = XRRGetMonitors(dpy, lock->root, True, &n);
for (i = 0; i < n; i++)
drawmon(dpy, lock, st, mons[i].x, mons[i].y,
mons[i].width, mons[i].height);
if (mons)
XRRFreeMonitors(mons);
if (n <= 0)
drawmon(dpy, lock, st, 0, 0, lock->w, lock->h);
XCopyArea(dpy, lock->buf, lock->win, DefaultGC(dpy, lock->screen),
0, 0, lock->w, lock->h, 0, 0);
}
static void
drawall(Display *dpy, struct lock **locks, int nscreens,
const struct state *st)
{
int screen;
for (screen = 0; screen < nscreens; screen++)
drawlock(dpy, locks[screen], st);
XFlush(dpy);
}
static void static void
readpw(Display *dpy, struct xrandr *rr, struct lock **locks, int nscreens, readpw(Display *dpy, struct xrandr *rr, struct lock **locks, int nscreens,
const char *hash) const char *hash)
{ {
XRRScreenChangeNotifyEvent *rre; XRRScreenChangeNotifyEvent *rre;
char buf[32], passwd[256], *inputhash; char buf[32], passwd[256], *inputhash;
int num, screen, running, failure, oldc; int num, screen, running, xfd;
unsigned int len, color; struct state st = { 0 };
struct timeval tv;
KeySym ksym; KeySym ksym;
XEvent ev; XEvent ev;
fd_set fds;
len = 0;
running = 1; running = 1;
failure = 0; st.color = INIT;
oldc = INIT; xfd = ConnectionNumber(dpy);
updatestate(&st);
drawall(dpy, locks, nscreens, &st);
while (running && !XNextEvent(dpy, &ev)) { while (running) {
/* sin eventos pendientes: esperar como mucho 1 s y refrescar
* reloj y batería si han cambiado (también tras suspender) */
if (!XPending(dpy)) {
FD_ZERO(&fds);
FD_SET(xfd, &fds);
tv.tv_sec = 1;
tv.tv_usec = 0;
if (select(xfd + 1, &fds, NULL, NULL, &tv) <= 0 &&
updatestate(&st))
drawall(dpy, locks, nscreens, &st);
continue;
}
XNextEvent(dpy, &ev);
if (ev.type == KeyPress) { if (ev.type == KeyPress) {
explicit_bzero(&buf, sizeof(buf)); explicit_bzero(&buf, sizeof(buf));
num = XLookupString(&ev.xkey, buf, sizeof(buf), &ksym, 0); num = XLookupString(&ev.xkey, buf, sizeof(buf), &ksym, 0);
@ -157,7 +460,7 @@ readpw(Display *dpy, struct xrandr *rr, struct lock **locks, int nscreens,
continue; continue;
switch (ksym) { switch (ksym) {
case XK_Return: case XK_Return:
passwd[len] = '\0'; passwd[st.len] = '\0';
errno = 0; errno = 0;
if (!(inputhash = crypt(passwd, hash))) if (!(inputhash = crypt(passwd, hash)))
fprintf(stderr, "slock: crypt: %s\n", strerror(errno)); fprintf(stderr, "slock: crypt: %s\n", strerror(errno));
@ -165,52 +468,56 @@ readpw(Display *dpy, struct xrandr *rr, struct lock **locks, int nscreens,
running = !!strcmp(inputhash, hash); running = !!strcmp(inputhash, hash);
if (running) { if (running) {
XBell(dpy, 100); XBell(dpy, 100);
failure = 1; st.failure = 1;
} }
explicit_bzero(&passwd, sizeof(passwd)); explicit_bzero(&passwd, sizeof(passwd));
len = 0; st.len = 0;
break; break;
case XK_Escape: case XK_Escape:
explicit_bzero(&passwd, sizeof(passwd)); explicit_bzero(&passwd, sizeof(passwd));
len = 0; st.len = 0;
break; break;
case XK_BackSpace: case XK_BackSpace:
if (len) if (st.len)
passwd[--len] = '\0'; passwd[--st.len] = '\0';
break; break;
default: default:
if (num && !iscntrl((unsigned char)buf[0]) && if (num && !iscntrl((unsigned char)buf[0]) &&
(len + num < sizeof(passwd))) { (st.len + num < sizeof(passwd))) {
memcpy(passwd + len, buf, num); memcpy(passwd + st.len, buf, num);
len += num; st.len += num;
} else if (buf[0] == '\025') { /* ctrl-u clears input */ } else if (buf[0] == '\025') { /* ctrl-u clears input */
explicit_bzero(&passwd, sizeof(passwd)); explicit_bzero(&passwd, sizeof(passwd));
len = 0; st.len = 0;
} }
break; break;
} }
color = len ? INPUT : ((failure || failonclear) ? FAILED : INIT); st.color = st.len ? INPUT :
if (running && oldc != color) { ((st.failure || failonclear) ? FAILED : INIT);
for (screen = 0; screen < nscreens; screen++) { if (running) {
XSetWindowBackground(dpy, updatestate(&st);
locks[screen]->win, drawall(dpy, locks, nscreens, &st);
locks[screen]->colors[color]);
XClearWindow(dpy, locks[screen]->win);
}
oldc = color;
} }
} else if (ev.type == Expose) {
if (ev.xexpose.count == 0)
drawall(dpy, locks, nscreens, &st);
} else if (rr->active && ev.type == rr->evbase + RRScreenChangeNotify) { } else if (rr->active && ev.type == rr->evbase + RRScreenChangeNotify) {
rre = (XRRScreenChangeNotifyEvent*)&ev; rre = (XRRScreenChangeNotifyEvent*)&ev;
for (screen = 0; screen < nscreens; screen++) { for (screen = 0; screen < nscreens; screen++) {
if (locks[screen]->win == rre->window) { if (locks[screen]->win == rre->window) {
if (rre->rotation == RR_Rotate_90 || if (rre->rotation == RR_Rotate_90 ||
rre->rotation == RR_Rotate_270) rre->rotation == RR_Rotate_270) {
XResizeWindow(dpy, locks[screen]->win, XResizeWindow(dpy, locks[screen]->win,
rre->height, rre->width); rre->height, rre->width);
else resizedraw(dpy, locks[screen],
rre->height, rre->width);
} else {
XResizeWindow(dpy, locks[screen]->win, XResizeWindow(dpy, locks[screen]->win,
rre->width, rre->height); rre->width, rre->height);
XClearWindow(dpy, locks[screen]->win); resizedraw(dpy, locks[screen],
rre->width, rre->height);
}
drawlock(dpy, locks[screen], &st);
break; break;
} }
} }
@ -243,16 +550,20 @@ lockscreen(Display *dpy, struct xrandr *rr, int screen)
lock->colors[i] = color.pixel; lock->colors[i] = color.pixel;
} }
setupdraw(dpy, lock);
/* init */ /* init */
wa.override_redirect = 1; wa.override_redirect = 1;
wa.background_pixel = lock->colors[INIT]; wa.background_pixel = lock->ui[COLBG].pixel;
wa.event_mask = ExposureMask;
lock->win = XCreateWindow(dpy, lock->root, 0, 0, lock->win = XCreateWindow(dpy, lock->root, 0, 0,
DisplayWidth(dpy, lock->screen), DisplayWidth(dpy, lock->screen),
DisplayHeight(dpy, lock->screen), DisplayHeight(dpy, lock->screen),
0, DefaultDepth(dpy, lock->screen), 0, DefaultDepth(dpy, lock->screen),
CopyFromParent, CopyFromParent,
DefaultVisual(dpy, lock->screen), DefaultVisual(dpy, lock->screen),
CWOverrideRedirect | CWBackPixel, &wa); CWOverrideRedirect | CWBackPixel | CWEventMask,
&wa);
lock->pmap = XCreateBitmapFromData(dpy, lock->win, curs, 8, 8); lock->pmap = XCreateBitmapFromData(dpy, lock->win, curs, 8, 8);
invisible = XCreatePixmapCursor(dpy, lock->pmap, lock->pmap, invisible = XCreatePixmapCursor(dpy, lock->pmap, lock->pmap,
&color, &color, 0, 0); &color, &color, 0, 0);
@ -340,6 +651,10 @@ main(int argc, char **argv) {
dontkillme(); dontkillme();
#endif #endif
/* nombre del usuario que bloquea, para mostrarlo */
if ((pwd = getpwuid(getuid())))
snprintf(username, sizeof(username), "%s", pwd->pw_name);
hash = gethash(); hash = gethash();
errno = 0; errno = 0;
if (!crypt("", hash)) if (!crypt("", hash))