dwl-bar/src/main.c

867 lines
23 KiB
C

#include <wayland-client-core.h>
#include <wayland-client-protocol.h>
#include <wayland-client.h>
#include <wayland-cursor.h>
#include <wayland-util.h>
#include <stdarg.h>
#include <stddef.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <poll.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include "bar.h"
#include "common.h"
#include "config.h"
#include "shm.h"
#include "wlr-layer-shell-unstable-v1-protocol.h"
#include "xdg-output-unstable-v1-protocol.h"
#include "xdg-shell-protocol.h"
/*
* When checking to see if two strings are the same with strcmp,
* 0 means they are the same, otherwise they are different.
*/
#define EQUAL 0
#define POLLFDS 4
typedef struct Monitor {
char *xdg_name;
uint32_t registry_name;
wl_output *output;
Bar *bar;
wl_list link;
} Monitor;
typedef struct {
wl_pointer* pointer;
Monitor* focused_monitor;
int x, y;
uint32_t* buttons;
uint size;
} Pointer;
typedef struct {
uint32_t registry_name;
wl_seat* seat;
wl_list link;
Pointer* pointer;
} Seat;
typedef struct {
wl_output *output;
uint32_t name;
wl_list link;
} uninitOutput;
typedef struct {
wl_registry *registry;
uint32_t name;
const char *interface_str;
} HandleGlobal;
static void cleanup(void);
static void setup(void);
static void run(void);
static void globalChecks(void);
static void checkGlobal(void *global, const char *name);
static void monitorSetup(uint32_t name, wl_output *output);
static void set_cloexec(int fd);
static void sighandler(int _);
static void flush(void);
static void setupFifo(void);
static char* to_delimiter(char* string, ulong *start_end, char delimiter);
static char* get_line(int fd);
static void on_status(void);
static void on_stdin(void);
static void handle_stdin(char* line);
static Monitor* monitor_from_name(char* name);
static Monitor* monitor_from_surface(wl_surface* surface);
static void update_monitor(Monitor* monitor);
/* Register listener related functions */
static void onGlobalAdd(void *data, wl_registry *registry, uint32_t name,
const char *interface, uint32_t version);
static void onGlobalRemove(void *data, wl_registry *registry, uint32_t name);
static int regHandle(void **store, HandleGlobal helper,
const wl_interface *interface, int version);
/* xdg listener members */
static void ping(void *data, xdg_wm_base *xdg_wm_base, uint32_t serial);
static void name(void *data, zxdg_output_v1 *output, const char *name);
/* seat listener member */
static void capabilites(void* data, wl_seat* wl_seat, uint32_t capabilities);
/* pointer listener members */
static void enter(void* data, wl_pointer* pointer, uint32_t serial, wl_surface* surface, wl_fixed_t x, wl_fixed_t y);
static void leave(void* data, wl_pointer* pointer, uint32_t serial, wl_surface* surface);
static void motion(void* data, wl_pointer* pointer, uint32_t time, wl_fixed_t x, wl_fixed_t y);
static void button(void* data, wl_pointer* pointer, uint32_t serial, uint32_t time, uint32_t button, uint32_t state);
static void frame(void* data, wl_pointer* pointer);
/* Also pointer listener members, but we don't do anything in these functions */
static void axis(void *data, struct wl_pointer *wl_pointer, uint32_t time, uint32_t axis, wl_fixed_t value) {}
static void axis_source(void *data, struct wl_pointer *wl_pointer, uint32_t axis_source) {}
static void axis_stop(void *data, struct wl_pointer *wl_pointer, uint32_t time, uint32_t axis) {}
static void axis_discrete(void *data, struct wl_pointer *wl_pointer, uint32_t axis, int32_t discrete) {}
static void seat_name(void* data, wl_seat* seat, const char* name) {}
/* Globals */
wl_display *display;
wl_compositor *compositor;
wl_shm *shm;
zwlr_layer_shell_v1 *shell;
static xdg_wm_base *base;
static zxdg_output_manager_v1 *output_manager;
/* Cursor */
static wl_cursor_image* cursor_image;
static wl_surface* cursor_surface;
/* Lists */
static wl_list seats;
static wl_list monitors;
static wl_list uninitializedOutputs;
/* File Descriptors */
static pollfd *pollfds;
static int wl_display_fd;
static int self_pipe[2];
static int fifo_fd;
static char* fifo_path;
/* Sigactions */
static struct sigaction sighandle;
static struct sigaction child_handle;
/*
* So that the global handler knows that we can initialize an output.
* Rather than just store it for when we have all of our globals.
*
* Since wayland is asynchronous we may get our outputs before we're ready for
* them.
*/
static int ready = 0;
static const struct wl_registry_listener registry_listener = {
.global = onGlobalAdd,
.global_remove = onGlobalRemove,
};
static const struct xdg_wm_base_listener xdg_wm_base_listener = {
.ping = ping,
};
/* So that we can get the monitor names to match with dwl monitor names. */
static const struct zxdg_output_v1_listener xdg_output_listener = {
.name = name,
};
static const struct wl_pointer_listener pointer_listener = {
.enter = enter,
.leave = leave,
.motion = motion,
.button = button,
.frame = frame,
.axis = axis,
.axis_discrete = axis_discrete,
.axis_source = axis_source,
.axis_stop = axis_stop,
};
static const struct wl_seat_listener seat_listener = {
.capabilities = capabilites,
.name = seat_name,
};
void enter(void *data, wl_pointer *pointer, uint32_t serial,
wl_surface *surface, wl_fixed_t x, wl_fixed_t y) {
Seat seat = *(Seat*)data;
seat.pointer->focused_monitor = monitor_from_surface(surface);
if (!cursor_image) {
wl_cursor_theme *theme = wl_cursor_theme_load(NULL, 24, shm);
cursor_image = wl_cursor_theme_get_cursor(theme, "left_ptr")->images[0];
cursor_surface = wl_compositor_create_surface(compositor);
wl_surface_attach(cursor_surface, wl_cursor_image_get_buffer(cursor_image), 0, 0);
wl_surface_commit(cursor_surface);
}
wl_pointer_set_cursor(pointer, serial, cursor_surface,
cursor_image->hotspot_x, cursor_image->hotspot_y);
}
void leave(void* data, wl_pointer* pointer, uint32_t serial, wl_surface* surface) {
Seat seat = *(Seat*)data;
seat.pointer->focused_monitor = NULL;
}
void motion(void* data, wl_pointer* pointer, uint32_t time, wl_fixed_t x, wl_fixed_t y) {
Seat seat = *(Seat*)data;
seat.pointer->x = wl_fixed_to_int(x);
seat.pointer->y = wl_fixed_to_int(y);
}
void button(void *data, wl_pointer *pointer, uint32_t serial, uint32_t time,
uint32_t button, uint32_t state) {
Seat seat = *(Seat*)data;
uint32_t* new_buttons = NULL;
int i, prev = -1; /* The index of this button */
for (i = 0; i < seat.pointer->size; i++) {
if (seat.pointer->buttons[i] == button)
prev = i;
}
/* If this button was newly pressed. */
if (state == WL_POINTER_BUTTON_STATE_PRESSED && prev == -1) {
new_buttons = ecalloc(seat.pointer->size+1, sizeof(uint32_t));
for (i = 0; i < seat.pointer->size+1; i++) {
if (i == seat.pointer->size) {
new_buttons[i] = button;
break;
}
new_buttons[i] = seat.pointer->buttons[i];
}
seat.pointer->size++;
}
/* If this button was released and we have it. */
if(state == WL_KEYBOARD_KEY_STATE_RELEASED && prev != -1) {
new_buttons = ecalloc(seat.pointer->size-1, sizeof(uint32_t));
for (i = 0; i < seat.pointer->size; i++) {
if (i == prev)
continue;
if (i < prev)
new_buttons[i] = seat.pointer->buttons[i];
if (i > prev)
new_buttons[i-1] = seat.pointer->buttons[i];
}
seat.pointer->size--;
}
free(seat.pointer->buttons);
seat.pointer->buttons = new_buttons;
return;
}
void frame(void* data, wl_pointer* pointer) {
Seat seat = *(Seat*)data;
Monitor* monitor = seat.pointer->focused_monitor;
if (!monitor)
return;
for (int i = 0; i < seat.pointer->size; i++)
bar_click(monitor->bar, monitor, seat.pointer->x, seat.pointer->y, seat.pointer->buttons[i]);
free(seat.pointer->buttons);
seat.pointer->buttons = NULL;
seat.pointer->size = 0;
}
void capabilites(void* data, wl_seat* wl_seat, uint32_t capabilities) {
Seat* seat = data;
int has_pointer = capabilities & WL_SEAT_CAPABILITY_POINTER;
if (!seat->pointer && has_pointer) {
seat->pointer = ecalloc(1, sizeof(Pointer));
seat->pointer->pointer = wl_seat_get_pointer(wl_seat);
seat->pointer->buttons = NULL;
seat->pointer->size = 0;
wl_pointer_add_listener(seat->pointer->pointer, &pointer_listener, seat);
return;
}
if (seat->pointer && !has_pointer) {
wl_pointer_release(seat->pointer->pointer);
seat->pointer->focused_monitor = NULL;
if (seat->pointer->buttons)
free(seat->pointer->buttons);
free(seat->pointer);
}
}
void name(void *data, zxdg_output_v1 *xdg_output, const char *name) {
Monitor *monitor = data;
monitor->xdg_name = strdup(name);
zxdg_output_v1_destroy(xdg_output);
}
void ping(void *data, struct xdg_wm_base *xdg_wm_base, uint32_t serial) {
xdg_wm_base_pong(base, serial);
}
int regHandle(void **store, HandleGlobal helper, const wl_interface *interface,
int version) {
if (strcmp(helper.interface_str, interface->name) != EQUAL)
return 0;
*store = wl_registry_bind(helper.registry, helper.name, interface, version);
return 1;
}
void onGlobalAdd(void *_, wl_registry *registry, uint32_t name,
const char *interface, uint32_t version) {
HandleGlobal helper = {registry, name, interface};
if (regHandle((void **)&compositor, helper, &wl_compositor_interface, 4))
return;
if (regHandle((void **)&shm, helper, &wl_shm_interface, 1))
return;
if (regHandle((void **)&output_manager, helper, &zxdg_output_manager_v1_interface, 3))
return;
if (regHandle((void **)&shell, helper, &zwlr_layer_shell_v1_interface, 4))
return;
if (regHandle((void **)&base, helper, &xdg_wm_base_interface, 2)) {
xdg_wm_base_add_listener(base, &xdg_wm_base_listener, NULL);
return;
}
{
wl_output *output;
if (regHandle((void **)&output, helper, &wl_output_interface, 1)) {
if (ready) {
monitorSetup(name, output);
} else {
uninitOutput *uninit = ecalloc(1, sizeof(uninitOutput));
uninit->output = output;
uninit->name = name;
wl_list_insert(&uninitializedOutputs, &uninit->link);
}
return;
}
}
{
wl_seat* seat;
if (regHandle((void**)&seat, helper, &wl_seat_interface, 7)) {
Seat* seat_ = ecalloc(1, sizeof(*seat_));
seat_->seat = seat;
seat_->registry_name = name;
wl_list_insert(&seats, &seat_->link);
wl_seat_add_listener(seat, &seat_listener, seat_);
return;
}
}
}
void onGlobalRemove(void *_, wl_registry *registry, uint32_t name) {
/* Deconstruct a monitor when it disappears */
Monitor *current_monitor, *tmp_monitor;
wl_list_for_each_safe(current_monitor, tmp_monitor, &monitors, link) {
if (current_monitor->registry_name == name) {
wl_list_remove(&current_monitor->link);
bar_destroy(current_monitor->bar);
}
}
/* Deconstruct seat when it disappears */
Seat *seat, *tmp_seat;
wl_list_for_each_safe(seat, tmp_seat, &seats, link) {
if (seat->registry_name == name) {
seat->pointer->focused_monitor = NULL;
wl_pointer_release(seat->pointer->pointer);
wl_list_remove(&seat->link);
free(seat->pointer->buttons);
free(seat->pointer);
free(seat);
}
}
}
void spawn(Monitor* monitor, const Arg *arg) {
if (fork() != 0)
return;
char* const* argv = arg->v;
setsid();
execvp(argv[0], argv);
fprintf(stderr, "bar: execvp %s", argv[0]);
perror(" failed\n");
exit(1);
}
void checkGlobal(void *global, const char *name) {
if (global)
return;
fprintf(stderr, "Wayland server did not export %s\n", name);
cleanup();
exit(1);
}
/*
* We just check and make sure we have our needed globals if any fail then we
* exit.
*/
void globalChecks() {
checkGlobal(compositor, "wl_compositor");
checkGlobal(shm, "wl_shm");
checkGlobal(base, "xdg_wm_base");
checkGlobal(shell, "wlr_layer_shell");
checkGlobal(output_manager, "zxdg_output_manager");
ready = 1;
}
void monitorSetup(uint32_t name, wl_output *output) {
Monitor *monitor = ecalloc(1, sizeof(*monitor));
monitor->bar = bar_create();
monitor->output = output;
monitor->registry_name = name;
wl_list_insert(&monitors, &monitor->link);
// So we can get the monitor name.
zxdg_output_v1 *xdg_output = zxdg_output_manager_v1_get_xdg_output(output_manager, output);
zxdg_output_v1_add_listener(xdg_output, &xdg_output_listener, monitor);
}
void set_cloexec(int fd) {
int flags = fcntl(fd, F_GETFD);
if (flags == -1)
die("FD_GETFD");
if (fcntl(fd, flags | FD_CLOEXEC) < 0)
die("FDD_SETFD");
}
void sighandler(int _) {
if (write(self_pipe[1], "0", 1) < 0)
die("sighandler");
}
void flush(void) {
wl_display_dispatch_pending(display);
if (wl_display_flush(display) < 0 && errno == EAGAIN) {
for (int i = 0; i < POLLFDS; i++) {
struct pollfd *poll = &pollfds[i];
if (poll->fd == wl_display_fd)
poll->events |= POLLOUT;
}
}
}
void setupFifo(void) {
int result, fd, i;
char *runtime_path = getenv("XDG_RUNTIME_DIR"), *file_name, *path;
for (i = 0; i < 100; i++) {
file_name = ecalloc(12, sizeof(*file_name));
sprintf(file_name, "/dwl-bar-%d", i);
path = ecalloc(strlen(runtime_path)+strlen(file_name)+1, sizeof(char));
strcat(path, runtime_path);
strcat(path, file_name);
free(file_name);
result = mkfifo(path, 0666);
if (result < 0) {
if (errno != EEXIST)
die("mkfifo");
free(path);
continue;
}
if ((fd = open(path, O_CLOEXEC | O_RDONLY | O_NONBLOCK)) < 0)
die("open fifo");
fifo_path = path;
fifo_fd = fd;
return;
}
die("setup fifo"); /* If we get here then we couldn't setup the fifo */
}
void update_monitor(Monitor* monitor) {
if (!bar_is_visible(monitor->bar)) {
bar_show(monitor->bar, monitor->output);
return;
}
bar_invalidate(monitor->bar);
return;
}
Monitor* monitor_from_name(char* name) {
Monitor* monitor;
wl_list_for_each(monitor, &monitors, link) {
if (strcmp(name, monitor->xdg_name) == EQUAL)
return monitor;
}
return NULL;
}
Monitor* monitor_from_surface(wl_surface* surface) {
Monitor* monitor;
wl_list_for_each(monitor, &monitors, link) {
if (surface == bar_get_surface(monitor->bar))
return monitor;
}
return NULL;
}
/*
* Parse and extract a substring based on a delimiter
* start_end is a ulong that we will use to base our starting location.
* Then replace as the end point to be used later on.
*/
char* to_delimiter(char* string, ulong *start_end, char delimiter) {
char* output;
ulong i, len = strlen(string);
if (*start_end > len)
return NULL;
for ( i = *start_end; i < len; i++ ) {
if (string[i] == delimiter) // We've reached the delimiter or the end.
break;
}
/* Create and copy the substring what we need */
output = strncpy(ecalloc(i - *start_end, sizeof(*output)),
string + *start_end, i - *start_end);
output[i - *start_end] = '\0'; // null terminate
*start_end = i+1;
return output;
}
/* The `getline` from stdio.h wasn't working so I've made my own. */
char* get_line(int fd) {
char *output, buffer[512], character;
int i = 0, r = i;
while ((r = read(fd, &character, 1)) > 0 && i < 512) {
buffer[i] = character;
i++;
if (character == '\n')
break;
}
/* Checking for edge cases */
if ((r == 0 && i == 0) || (i == 1 && buffer[i] == '\n'))
return NULL;
buffer[i] = '\0';
output = strncpy(ecalloc(i, sizeof(char)), buffer, i*sizeof(char));
return output;
}
void on_stdin(void) {
while (1) {
char *buffer = get_line(STDIN_FILENO);
if (!buffer || !strlen(buffer)) {
if (buffer)
free(buffer);
return;
}
handle_stdin(buffer);
free(buffer);
}
}
void handle_stdin(char* line) {
char *name, *command;
Monitor* monitor;
ulong loc = 0; /* Keep track of where we are in the string `line` */
name = to_delimiter(line, &loc, ' ');
command = to_delimiter(line, &loc, ' ');
monitor = monitor_from_name(name);
if (!monitor)
return;
free(name);
// Hate the way these if statements look. Is being this explicit worth it?
if (strcmp(command, "title") == EQUAL) {
if (line[strlen(line)-2] == ' ') {
bar_set_title(monitor->bar, "");
goto done;
}
char* title = to_delimiter(line, &loc, '\n');
bar_set_title(monitor->bar, title);
free(title);
} else if (strcmp(command, "appid") == EQUAL) {
/* Do nothing */
} else if (strcmp(command, "floating") == EQUAL) {
if (line[strlen(line)-2] == ' ') {
bar_set_floating(monitor->bar, 0);
goto done;
}
char* is_floating = to_delimiter(line, &loc, '\n');
strcmp(is_floating, "1") == EQUAL ? bar_set_floating(monitor->bar, 1) : bar_set_floating(monitor->bar, 0);
free(is_floating);
} else if (strcmp(command, "fullscreen") == EQUAL) {
/* Do nothing */
} else if (strcmp(command, "selmon") == EQUAL) {
char* selmon = to_delimiter(line, &loc, '\n');
strcmp(selmon, "1") == EQUAL ? bar_set_active(monitor->bar, 1) : bar_set_active(monitor->bar, 0);
free(selmon);
} else if (strcmp(command, "tags") == EQUAL) {
char *occupied_, *tags__, *clients_, *urgent_;
int occupied, tags_, clients, urgent, i, tag_mask;
occupied_ = to_delimiter(line, &loc, ' ');
tags__ = to_delimiter(line, &loc, ' ');
clients_ = to_delimiter(line, &loc, ' ');
urgent_ = to_delimiter(line, &loc, '\n');
occupied = atoi(occupied_);
tags_ = atoi(tags__);
clients = atoi(clients_);
urgent = atoi(urgent_);
for(i = 0; i < LENGTH(tags); i++) {
int state = TAG_INACTIVE;
tag_mask = 1 << i;
if (tags_ & tag_mask)
state |= TAG_ACTIVE;
if (urgent & tag_mask)
state |= TAG_URGENT;
bar_set_tag(monitor->bar, i, state, occupied & tag_mask ? 1: 0, clients & tag_mask ? 1 : 0);
}
free(occupied_);
free(tags__);
free(clients_);
free(urgent_);
} else if (strcmp(command, "layout") == EQUAL) {
char* layout = to_delimiter(line, &loc, '\n');
bar_set_layout(monitor->bar, layout);
free(layout);
} else {
/* TODO: Find a good way to tell the user a command wasn't recognized. Or just do nothing? */
}
done:
free(command);
update_monitor(monitor);
}
void on_status(void) {
while (1) {
char *line = get_line(fifo_fd), *command, *status;
ulong loc = 0;
if (!line || !strlen(line)) {
if (line)
free(line);
return;
}
command = to_delimiter(line, &loc, ' ');
status = to_delimiter(line, &loc, '\n');
if (strcmp(command, "status") != EQUAL)
goto done;
Monitor *current_monitor;
wl_list_for_each(current_monitor, &monitors, link) {
bar_set_status(current_monitor->bar, status);
update_monitor(current_monitor);
}
done:
free(line);
free(command);
free(status);
}
}
void setup(void) {
if (pipe(self_pipe) < 0)
die("pipe");
set_cloexec(self_pipe[0]);
set_cloexec(self_pipe[1]);
sighandle.sa_handler = &sighandler;
child_handle.sa_handler = SIG_IGN;
if (sigaction(SIGTERM, &sighandle, NULL) < 0 ||
sigaction(SIGINT, &sighandle, NULL) < 0 ||
sigaction(SIGCHLD, &child_handle, NULL) < 0)
die("sigaction");
display = wl_display_connect(NULL);
if (!display)
die("Failed to connect to a Wayland display.");
wl_display_fd = wl_display_get_fd(display);
wl_list_init(&seats);
wl_list_init(&monitors);
wl_list_init(&uninitializedOutputs);
wl_registry *registry = wl_display_get_registry(display);
wl_registry_add_listener(registry, &registry_listener, NULL);
wl_display_roundtrip(display);
setupFifo();
globalChecks(); /* Make sure we're ready to start the rest of the program */
wl_display_roundtrip(display);
// Initalize any outputs we got from registry. Then free the outputs.
uninitOutput *output, *tmp;
wl_list_for_each_safe(output, tmp, &uninitializedOutputs, link) {
monitorSetup(output->name, output->output);
wl_list_remove(&output->link);
free(output);
}
wl_display_roundtrip(display);
pollfds = ecalloc(POLLFDS, sizeof(*pollfds));
pollfds[0] = (pollfd) {STDIN_FILENO, POLLIN};
pollfds[1] = (pollfd) {wl_display_fd, POLLIN};
pollfds[2] = (pollfd) {self_pipe[0], POLLIN};
pollfds[3] = (pollfd) {fifo_fd, POLLIN};
if (fcntl(STDIN_FILENO, F_SETFL, O_NONBLOCK) < 0)
die("O_NONBLOCK");
}
void run(void) {
struct pollfd *pollfd;
int i, quitting = 0;
while (!quitting) {
flush();
if (poll(pollfds, POLLFDS, -1) < 0 && errno != EINTR)
die("poll");
for (i = 0; i < POLLFDS; i++) {
pollfd = &pollfds[i];
if (pollfd->revents & POLLNVAL)
die("POLLNVAL");
if (pollfd->fd == wl_display_fd) {
if ((pollfd->revents & POLLIN) && (wl_display_dispatch(display) < 0))
die("wl_display_dispatch");
if (pollfd->revents & POLLOUT) {
pollfd->events = POLLIN;
flush();
}
} else if (pollfd->fd == STDIN_FILENO && (pollfd->revents & POLLIN)) {
on_stdin();
} else if (pollfd->fd == fifo_fd && (pollfd->revents & POLLIN)) {
on_status();
} else if (pollfd->fd == self_pipe[0] && (pollfd->revents & POLLIN)) {
quitting = 1;
}
}
}
}
void cleanup(void) {
if (shm)
wl_shm_destroy(shm);
if (compositor)
wl_compositor_destroy(compositor);
if (base)
xdg_wm_base_destroy(base);
if (shell)
zwlr_layer_shell_v1_destroy(shell);
if (output_manager)
zxdg_output_manager_v1_destroy(output_manager);
if (fifo_fd)
close(fifo_fd);
if (fifo_path) {
unlink(fifo_path);
free(fifo_path);
}
if (display)
wl_display_disconnect(display);
}
int main(int argc, char *argv[]) {
int opt; // Just char for parsing args
while ((opt = getopt(argc, argv, "vh")) != -1) {
switch (opt) {
case 'h':
printf("Usage: %s [-h] [-v]\n", argv[0]);
printf(" -h: show this\n");
printf(" -v: get version\n");
exit(0);
case 'v':
printf("dwl %f", VERSION);
exit(0);
}
}
setup();
run();
cleanup();
return 0;
}
void die(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
fprintf(stderr, "[dwl-bar] error: ");
vfprintf(stderr, fmt, ap);
va_end(ap);
if (fmt[0] && fmt[strlen(fmt) - 1] == ':') {
fputc(' ', stderr);
perror(NULL);
} else {
fputc('\n', stderr);
}
fflush(stderr);
cleanup();
exit(1);
}
void* ecalloc(size_t amnt, size_t size) {
void *p;
if (!(p = calloc(amnt, size)))
die("calloc did not allocate");
return p;
}