forked from external/yambar
1059 lines
30 KiB
C
1059 lines
30 KiB
C
#include <assert.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
#include <threads.h>
|
||
#include <unistd.h>
|
||
|
||
#include <fcntl.h>
|
||
#include <sys/types.h>
|
||
|
||
#include <tllist.h>
|
||
|
||
#define LOG_MODULE "i3"
|
||
#define LOG_ENABLE_DBG 0
|
||
#include "../bar/bar.h"
|
||
#include "../config-verify.h"
|
||
#include "../config.h"
|
||
#include "../log.h"
|
||
#include "../particles/dynlist.h"
|
||
#include "../plugin.h"
|
||
|
||
#include "i3-common.h"
|
||
#include "i3-ipc.h"
|
||
|
||
enum sort_mode { SORT_NONE, SORT_NATIVE, SORT_ASCENDING, SORT_DESCENDING };
|
||
|
||
struct ws_content {
|
||
char *name;
|
||
struct particle *content;
|
||
};
|
||
|
||
struct workspace {
|
||
int id;
|
||
char *name;
|
||
int name_as_int; /* -1 if name is not a decimal number */
|
||
bool persistent;
|
||
|
||
char *output;
|
||
bool visible;
|
||
bool focused;
|
||
bool urgent;
|
||
bool empty;
|
||
|
||
struct {
|
||
unsigned id;
|
||
char *title;
|
||
char *application;
|
||
pid_t pid;
|
||
} window;
|
||
};
|
||
|
||
struct private
|
||
{
|
||
int left_spacing;
|
||
int right_spacing;
|
||
|
||
bool dirty;
|
||
|
||
char *mode;
|
||
|
||
struct {
|
||
struct ws_content *v;
|
||
size_t count;
|
||
} ws_content;
|
||
|
||
bool strip_workspace_numbers;
|
||
enum sort_mode sort_mode;
|
||
tll(struct workspace) workspaces;
|
||
|
||
size_t persistent_count;
|
||
char **persistent_workspaces;
|
||
};
|
||
|
||
static int
|
||
workspace_name_as_int(const char *name)
|
||
{
|
||
int name_as_int = 0;
|
||
|
||
/* First check for N:name pattern (set $ws1 “1:foobar”) */
|
||
const char *colon = strchr(name, ':');
|
||
if (colon != NULL) {
|
||
for (const char *p = name; p < colon; p++) {
|
||
if (!(*p >= '0' && *p < '9'))
|
||
return -1;
|
||
|
||
name_as_int *= 10;
|
||
name_as_int += *p - '0';
|
||
}
|
||
|
||
return name_as_int;
|
||
}
|
||
|
||
/* Then, if the name is a number *only* (set $ws1 1) */
|
||
for (const char *p = name; *p != '\0'; p++) {
|
||
if (!(*p >= '0' && *p <= '9'))
|
||
return -1;
|
||
|
||
name_as_int *= 10;
|
||
name_as_int += *p - '0';
|
||
}
|
||
|
||
return name_as_int;
|
||
}
|
||
|
||
static bool
|
||
workspace_from_json(const struct json_object *json, struct workspace *ws)
|
||
{
|
||
/* Always present */
|
||
struct json_object *id, *name, *output;
|
||
if (!json_object_object_get_ex(json, "id", &id) || !json_object_object_get_ex(json, "name", &name)
|
||
|| !json_object_object_get_ex(json, "output", &output)) {
|
||
LOG_ERR("workspace reply/event without 'name' and/or 'output' "
|
||
"properties");
|
||
return false;
|
||
}
|
||
|
||
/* Sway only */
|
||
struct json_object *focus = NULL;
|
||
json_object_object_get_ex(json, "focus", &focus);
|
||
|
||
/* Optional */
|
||
struct json_object *visible = NULL, *focused = NULL, *urgent = NULL;
|
||
json_object_object_get_ex(json, "visible", &visible);
|
||
json_object_object_get_ex(json, "focused", &focused);
|
||
json_object_object_get_ex(json, "urgent", &urgent);
|
||
|
||
const char *name_as_string = json_object_get_string(name);
|
||
|
||
const size_t node_count = focus != NULL ? json_object_array_length(focus) : 0;
|
||
|
||
const bool is_empty = node_count == 0;
|
||
int name_as_int = workspace_name_as_int(name_as_string);
|
||
|
||
*ws = (struct workspace){
|
||
.id = json_object_get_int(id),
|
||
.name = strdup(name_as_string),
|
||
.name_as_int = name_as_int,
|
||
.persistent = false,
|
||
.output = strdup(json_object_get_string(output)),
|
||
.visible = json_object_get_boolean(visible),
|
||
.focused = json_object_get_boolean(focused),
|
||
.urgent = json_object_get_boolean(urgent),
|
||
.empty = is_empty && json_object_get_boolean(focused),
|
||
.window = {.title = NULL, .pid = -1},
|
||
};
|
||
|
||
return true;
|
||
}
|
||
|
||
static void
|
||
workspace_free_persistent(struct workspace *ws)
|
||
{
|
||
free(ws->output);
|
||
ws->output = NULL;
|
||
free(ws->window.title);
|
||
ws->window.title = NULL;
|
||
free(ws->window.application);
|
||
ws->window.application = NULL;
|
||
ws->id = -1;
|
||
}
|
||
|
||
static void
|
||
workspace_free(struct workspace *ws)
|
||
{
|
||
workspace_free_persistent(ws);
|
||
free(ws->name);
|
||
ws->name = NULL;
|
||
}
|
||
|
||
static void
|
||
workspaces_free(struct private *m, bool free_persistent)
|
||
{
|
||
tll_foreach(m->workspaces, it)
|
||
{
|
||
if (free_persistent || !it->item.persistent) {
|
||
workspace_free(&it->item);
|
||
tll_remove(m->workspaces, it);
|
||
}
|
||
}
|
||
}
|
||
|
||
static void
|
||
workspace_add(struct private *m, struct workspace ws)
|
||
{
|
||
switch (m->sort_mode) {
|
||
case SORT_NONE:
|
||
tll_push_back(m->workspaces, ws);
|
||
return;
|
||
|
||
case SORT_NATIVE:
|
||
if (ws.name_as_int >= 0) {
|
||
tll_foreach(m->workspaces, it)
|
||
{
|
||
if (it->item.name_as_int < 0)
|
||
continue;
|
||
if (it->item.name_as_int > ws.name_as_int) {
|
||
tll_insert_before(m->workspaces, it, ws);
|
||
return;
|
||
}
|
||
}
|
||
};
|
||
|
||
tll_push_back(m->workspaces, ws);
|
||
return;
|
||
|
||
case SORT_ASCENDING:
|
||
if (ws.name_as_int >= 0) {
|
||
tll_foreach(m->workspaces, it)
|
||
{
|
||
if (it->item.name_as_int < 0)
|
||
continue;
|
||
if (it->item.name_as_int > ws.name_as_int) {
|
||
tll_insert_before(m->workspaces, it, ws);
|
||
return;
|
||
}
|
||
}
|
||
} else {
|
||
tll_foreach(m->workspaces, it)
|
||
{
|
||
if (strcoll(it->item.name, ws.name) > 0 || it->item.name_as_int >= 0) {
|
||
tll_insert_before(m->workspaces, it, ws);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
tll_push_back(m->workspaces, ws);
|
||
return;
|
||
|
||
case SORT_DESCENDING:
|
||
if (ws.name_as_int >= 0) {
|
||
tll_foreach(m->workspaces, it)
|
||
{
|
||
if (it->item.name_as_int < ws.name_as_int) {
|
||
tll_insert_before(m->workspaces, it, ws);
|
||
return;
|
||
}
|
||
}
|
||
} else {
|
||
tll_foreach(m->workspaces, it)
|
||
{
|
||
if (it->item.name_as_int >= 0)
|
||
continue;
|
||
if (strcoll(it->item.name, ws.name) < 0) {
|
||
tll_insert_before(m->workspaces, it, ws);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
tll_push_back(m->workspaces, ws);
|
||
return;
|
||
}
|
||
}
|
||
|
||
static void
|
||
workspace_del(struct private *m, int id)
|
||
{
|
||
tll_foreach(m->workspaces, it)
|
||
{
|
||
struct workspace *ws = &it->item;
|
||
|
||
if (ws->id != id)
|
||
continue;
|
||
|
||
workspace_free(ws);
|
||
tll_remove(m->workspaces, it);
|
||
break;
|
||
}
|
||
}
|
||
|
||
static struct workspace *
|
||
workspace_lookup(struct private *m, int id)
|
||
{
|
||
tll_foreach(m->workspaces, it)
|
||
{
|
||
struct workspace *ws = &it->item;
|
||
if (ws->id == id)
|
||
return ws;
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
static struct workspace *
|
||
workspace_lookup_by_name(struct private *m, const char *name)
|
||
{
|
||
tll_foreach(m->workspaces, it)
|
||
{
|
||
struct workspace *ws = &it->item;
|
||
if (strcmp(ws->name, name) == 0)
|
||
return ws;
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
static bool
|
||
handle_get_version_reply(int sock, int type, const struct json_object *json, void *_m)
|
||
{
|
||
struct json_object *version;
|
||
if (!json_object_object_get_ex(json, "human_readable", &version)) {
|
||
LOG_ERR("version reply without 'humand_readable' property");
|
||
return false;
|
||
}
|
||
|
||
LOG_INFO("i3: %s", json_object_get_string(version));
|
||
return true;
|
||
}
|
||
|
||
static bool
|
||
handle_subscribe_reply(int sock, int type, const struct json_object *json, void *_m)
|
||
{
|
||
struct json_object *success;
|
||
if (!json_object_object_get_ex(json, "success", &success)) {
|
||
LOG_ERR("subscribe reply without 'success' property");
|
||
return false;
|
||
}
|
||
|
||
if (!json_object_get_boolean(success)) {
|
||
LOG_ERR("failed to subscribe");
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
static bool
|
||
workspace_update_or_add(struct private *m, const struct json_object *ws_json)
|
||
{
|
||
struct json_object *_id;
|
||
if (!json_object_object_get_ex(ws_json, "id", &_id))
|
||
return false;
|
||
|
||
const int id = json_object_get_int(_id);
|
||
struct workspace *already_exists = workspace_lookup(m, id);
|
||
|
||
if (already_exists == NULL) {
|
||
/*
|
||
* No workspace with this ID.
|
||
*
|
||
* Try looking it up again, but this time using the name. If
|
||
* we get a match, check if it’s an empty, persistent
|
||
* workspace, and if so, use it.
|
||
*
|
||
* This is necessary, since empty, persistent workspaces don’t
|
||
* exist in the i3/Sway server, and thus we don’t _have_ an
|
||
* ID.
|
||
*/
|
||
struct json_object *_name;
|
||
if (json_object_object_get_ex(ws_json, "name", &_name)) {
|
||
const char *name = json_object_get_string(_name);
|
||
if (name != NULL) {
|
||
struct workspace *maybe_persistent = workspace_lookup_by_name(m, name);
|
||
|
||
if (maybe_persistent != NULL && maybe_persistent->persistent && maybe_persistent->id < 0) {
|
||
already_exists = maybe_persistent;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (already_exists != NULL) {
|
||
bool persistent = already_exists->persistent;
|
||
assert(persistent);
|
||
|
||
workspace_free(already_exists);
|
||
if (!workspace_from_json(ws_json, already_exists))
|
||
return false;
|
||
already_exists->persistent = persistent;
|
||
} else {
|
||
struct workspace ws;
|
||
if (!workspace_from_json(ws_json, &ws))
|
||
return false;
|
||
|
||
workspace_add(m, ws);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
static bool
|
||
handle_get_workspaces_reply(int sock, int type, const struct json_object *json, void *_mod)
|
||
{
|
||
struct module *mod = _mod;
|
||
struct private *m = mod->private;
|
||
|
||
mtx_lock(&mod->lock);
|
||
|
||
workspaces_free(m, false);
|
||
m->dirty = true;
|
||
|
||
size_t count = json_object_array_length(json);
|
||
|
||
for (size_t i = 0; i < count; i++) {
|
||
if (!workspace_update_or_add(m, json_object_array_get_idx(json, i)))
|
||
goto err;
|
||
}
|
||
|
||
mtx_unlock(&mod->lock);
|
||
return true;
|
||
|
||
err:
|
||
workspaces_free(m, false);
|
||
mtx_unlock(&mod->lock);
|
||
return false;
|
||
}
|
||
|
||
static bool
|
||
handle_workspace_event(int sock, int type, const struct json_object *json, void *_mod)
|
||
{
|
||
struct module *mod = _mod;
|
||
struct private *m = mod->private;
|
||
|
||
struct json_object *change;
|
||
if (!json_object_object_get_ex(json, "change", &change)) {
|
||
LOG_ERR("workspace event without 'change' property");
|
||
return false;
|
||
}
|
||
|
||
const char *change_str = json_object_get_string(change);
|
||
|
||
bool is_init = strcmp(change_str, "init") == 0;
|
||
bool is_empty = strcmp(change_str, "empty") == 0;
|
||
bool is_focused = strcmp(change_str, "focus") == 0;
|
||
bool is_rename = strcmp(change_str, "rename") == 0;
|
||
bool is_move = strcmp(change_str, "move") == 0;
|
||
bool is_urgent = strcmp(change_str, "urgent") == 0;
|
||
bool is_reload = strcmp(change_str, "reload") == 0;
|
||
|
||
struct json_object *current, *_current_id;
|
||
if ((!json_object_object_get_ex(json, "current", ¤t)
|
||
|| !json_object_object_get_ex(current, "id", &_current_id))
|
||
&& !is_reload) {
|
||
LOG_ERR("workspace event without 'current' and/or 'id' properties");
|
||
return false;
|
||
}
|
||
|
||
int current_id = json_object_get_int(_current_id);
|
||
|
||
mtx_lock(&mod->lock);
|
||
|
||
if (is_init) {
|
||
if (!workspace_update_or_add(m, current))
|
||
goto err;
|
||
}
|
||
|
||
else if (is_empty) {
|
||
struct workspace *ws = workspace_lookup(m, current_id);
|
||
assert(ws != NULL);
|
||
|
||
if (!ws->persistent)
|
||
workspace_del(m, current_id);
|
||
else {
|
||
workspace_free_persistent(ws);
|
||
ws->empty = true;
|
||
}
|
||
}
|
||
|
||
else if (is_focused) {
|
||
struct json_object *old, *_old_id, *urgent;
|
||
if (!json_object_object_get_ex(json, "old", &old) || !json_object_object_get_ex(old, "id", &_old_id)
|
||
|| !json_object_object_get_ex(current, "urgent", &urgent)) {
|
||
LOG_ERR("workspace 'focused' event without 'old', 'name' and/or 'urgent' property");
|
||
mtx_unlock(&mod->lock);
|
||
return false;
|
||
}
|
||
|
||
struct workspace *w = workspace_lookup(m, current_id);
|
||
assert(w != NULL);
|
||
|
||
LOG_DBG("w: %s", w->name);
|
||
|
||
/* Mark all workspaces on current's output invisible */
|
||
tll_foreach(m->workspaces, it)
|
||
{
|
||
struct workspace *ws = &it->item;
|
||
if (ws->output != NULL && strcmp(ws->output, w->output) == 0)
|
||
ws->visible = false;
|
||
}
|
||
|
||
w->urgent = json_object_get_boolean(urgent);
|
||
w->focused = true;
|
||
w->visible = true;
|
||
|
||
/* Old workspace is no longer focused */
|
||
int old_id = json_object_get_int(_old_id);
|
||
struct workspace *old_w = workspace_lookup(m, old_id);
|
||
if (old_w != NULL)
|
||
old_w->focused = false;
|
||
}
|
||
|
||
else if (is_rename) {
|
||
struct workspace *w = workspace_lookup(m, current_id);
|
||
assert(w != NULL);
|
||
|
||
struct json_object *_current_name;
|
||
if (!json_object_object_get_ex(current, "name", &_current_name)) {
|
||
LOG_ERR("workspace 'rename' event without 'name' property");
|
||
mtx_unlock(&mod->lock);
|
||
return false;
|
||
}
|
||
|
||
free(w->name);
|
||
w->name = strdup(json_object_get_string(_current_name));
|
||
w->name_as_int = workspace_name_as_int(w->name);
|
||
|
||
/* Re-add the workspace to ensure correct sorting */
|
||
struct workspace ws = *w;
|
||
tll_foreach(m->workspaces, it)
|
||
{
|
||
if (it->item.id == current_id) {
|
||
tll_remove(m->workspaces, it);
|
||
break;
|
||
}
|
||
}
|
||
workspace_add(m, ws);
|
||
}
|
||
|
||
else if (is_move) {
|
||
struct workspace *w = workspace_lookup(m, current_id);
|
||
assert(w != NULL);
|
||
|
||
struct json_object *_current_output;
|
||
if (!json_object_object_get_ex(current, "output", &_current_output)) {
|
||
LOG_ERR("workspace 'move' event without 'output' property");
|
||
mtx_unlock(&mod->lock);
|
||
return false;
|
||
}
|
||
|
||
free(w->output);
|
||
w->output = strdup(json_object_get_string(_current_output));
|
||
|
||
/*
|
||
* If the moved workspace was focused, schedule a full update because
|
||
* visibility for other workspaces may have changed.
|
||
*/
|
||
if (w->focused) {
|
||
i3_send_pkg(sock, I3_IPC_MESSAGE_TYPE_GET_WORKSPACES, NULL);
|
||
}
|
||
}
|
||
|
||
else if (is_urgent) {
|
||
struct json_object *urgent;
|
||
if (!json_object_object_get_ex(current, "urgent", &urgent)) {
|
||
LOG_ERR("workspace 'urgent' event without 'urgent' property");
|
||
mtx_unlock(&mod->lock);
|
||
return false;
|
||
}
|
||
|
||
struct workspace *w = workspace_lookup(m, current_id);
|
||
w->urgent = json_object_get_boolean(urgent);
|
||
}
|
||
|
||
else if (is_reload) {
|
||
/* Schedule full update to check if anything was changed
|
||
* during reload */
|
||
i3_send_pkg(sock, I3_IPC_MESSAGE_TYPE_GET_WORKSPACES, NULL);
|
||
}
|
||
|
||
else {
|
||
LOG_WARN("unimplemented workspace event '%s'", change_str);
|
||
}
|
||
|
||
m->dirty = true;
|
||
mtx_unlock(&mod->lock);
|
||
return true;
|
||
|
||
err:
|
||
mtx_unlock(&mod->lock);
|
||
return false;
|
||
}
|
||
|
||
static bool
|
||
handle_window_event(int sock, int type, const struct json_object *json, void *_mod)
|
||
{
|
||
struct module *mod = _mod;
|
||
struct private *m = mod->private;
|
||
|
||
struct json_object *change;
|
||
if (!json_object_object_get_ex(json, "change", &change)) {
|
||
LOG_ERR("window event without 'change' property");
|
||
return false;
|
||
}
|
||
|
||
const char *change_str = json_object_get_string(change);
|
||
bool is_focus = strcmp(change_str, "focus") == 0;
|
||
bool is_close = strcmp(change_str, "close") == 0;
|
||
bool is_title = strcmp(change_str, "title") == 0;
|
||
|
||
if (!is_focus && !is_close && !is_title)
|
||
return true;
|
||
|
||
mtx_lock(&mod->lock);
|
||
|
||
struct workspace *ws = NULL;
|
||
size_t focused = 0;
|
||
tll_foreach(m->workspaces, it)
|
||
{
|
||
if (it->item.focused) {
|
||
ws = &it->item;
|
||
focused++;
|
||
}
|
||
}
|
||
|
||
assert(focused == 1);
|
||
assert(ws != NULL);
|
||
|
||
struct json_object *container, *id, *name;
|
||
if (!json_object_object_get_ex(json, "container", &container) || !json_object_object_get_ex(container, "id", &id)
|
||
|| !json_object_object_get_ex(container, "name", &name)) {
|
||
mtx_unlock(&mod->lock);
|
||
LOG_ERR("window event without 'container' with 'id' and 'name'");
|
||
return false;
|
||
}
|
||
|
||
if ((is_close || is_title) && ws->window.id != json_object_get_int(id)) {
|
||
/* Ignore close event and title changed event if it's not current window */
|
||
mtx_unlock(&mod->lock);
|
||
return true;
|
||
}
|
||
|
||
if (is_close) {
|
||
free(ws->window.title);
|
||
free(ws->window.application);
|
||
|
||
ws->window.id = -1;
|
||
ws->window.title = ws->window.application = NULL;
|
||
ws->window.pid = -1;
|
||
|
||
m->dirty = true;
|
||
mtx_unlock(&mod->lock);
|
||
return true;
|
||
}
|
||
|
||
free(ws->window.title);
|
||
|
||
const char *title = json_object_get_string(name);
|
||
ws->window.title = title != NULL ? strdup(title) : NULL;
|
||
ws->window.id = json_object_get_int(id);
|
||
|
||
/*
|
||
* Sway only!
|
||
*
|
||
* Use 'app_id' for 'application' tag, if it exists.
|
||
*
|
||
* Otherwise, use 'pid' if it exists, and read application name
|
||
* from /proc/zpid>/comm
|
||
*/
|
||
|
||
struct json_object *app_id;
|
||
struct json_object *pid;
|
||
|
||
if (json_object_object_get_ex(container, "app_id", &app_id) && json_object_get_string(app_id) != NULL) {
|
||
free(ws->window.application);
|
||
ws->window.application = strdup(json_object_get_string(app_id));
|
||
LOG_DBG("application: \"%s\", via 'app_id'", ws->window.application);
|
||
}
|
||
|
||
/* If PID has changed, update application name from /proc/<pid>/comm */
|
||
else if (json_object_object_get_ex(container, "pid", &pid) && ws->window.pid != json_object_get_int(pid)) {
|
||
ws->window.pid = json_object_get_int(pid);
|
||
|
||
char path[64];
|
||
snprintf(path, sizeof(path), "/proc/%u/comm", ws->window.pid);
|
||
|
||
int fd = open(path, O_RDONLY);
|
||
if (fd == -1) {
|
||
/* Application may simply have terminated */
|
||
free(ws->window.application);
|
||
ws->window.application = NULL;
|
||
ws->window.pid = -1;
|
||
|
||
m->dirty = true;
|
||
mtx_unlock(&mod->lock);
|
||
return true;
|
||
}
|
||
|
||
char application[128];
|
||
ssize_t bytes = read(fd, application, sizeof(application));
|
||
assert(bytes >= 0);
|
||
|
||
application[bytes - 1] = '\0';
|
||
free(ws->window.application);
|
||
ws->window.application = strdup(application);
|
||
close(fd);
|
||
LOG_DBG("application: \"%s\", via 'pid'", ws->window.application);
|
||
}
|
||
|
||
m->dirty = true;
|
||
mtx_unlock(&mod->lock);
|
||
return true;
|
||
}
|
||
|
||
static bool
|
||
handle_mode_event(int sock, int type, const struct json_object *json, void *_mod)
|
||
{
|
||
struct module *mod = _mod;
|
||
struct private *m = mod->private;
|
||
|
||
struct json_object *change;
|
||
if (!json_object_object_get_ex(json, "change", &change)) {
|
||
LOG_ERR("mode event without 'change' property");
|
||
return false;
|
||
}
|
||
|
||
const char *current_mode = json_object_get_string(change);
|
||
|
||
mtx_lock(&mod->lock);
|
||
{
|
||
free(m->mode);
|
||
m->mode = strdup(current_mode);
|
||
m->dirty = true;
|
||
}
|
||
mtx_unlock(&mod->lock);
|
||
return true;
|
||
}
|
||
|
||
static void
|
||
burst_done(void *_mod)
|
||
{
|
||
struct module *mod = _mod;
|
||
struct private *m = mod->private;
|
||
|
||
if (m->dirty) {
|
||
m->dirty = false;
|
||
mod->bar->refresh(mod->bar);
|
||
}
|
||
}
|
||
|
||
static int
|
||
run(struct module *mod)
|
||
{
|
||
struct sockaddr_un addr;
|
||
if (!i3_get_socket_address(&addr))
|
||
return 1;
|
||
|
||
int sock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
||
if (sock == -1) {
|
||
LOG_ERRNO("failed to create UNIX socket");
|
||
return 1;
|
||
}
|
||
|
||
int r = connect(sock, (const struct sockaddr *)&addr, sizeof(addr));
|
||
if (r == -1) {
|
||
LOG_ERRNO("failed to connect to i3 socket");
|
||
close(sock);
|
||
return 1;
|
||
}
|
||
|
||
struct private *m = mod->private;
|
||
for (size_t i = 0; i < m->persistent_count; i++) {
|
||
const char *name_as_string = m->persistent_workspaces[i];
|
||
|
||
int name_as_int = workspace_name_as_int(name_as_string);
|
||
if (m->strip_workspace_numbers) {
|
||
const char *colon = strchr(name_as_string, ':');
|
||
if (colon != NULL)
|
||
name_as_string = colon++;
|
||
}
|
||
|
||
struct workspace ws = {
|
||
.id = -1,
|
||
.name = strdup(name_as_string),
|
||
.name_as_int = name_as_int,
|
||
.persistent = true,
|
||
.empty = true,
|
||
};
|
||
workspace_add(m, ws);
|
||
}
|
||
|
||
i3_send_pkg(sock, I3_IPC_MESSAGE_TYPE_GET_VERSION, NULL);
|
||
i3_send_pkg(sock, I3_IPC_MESSAGE_TYPE_SUBSCRIBE, "[\"workspace\", \"window\", \"mode\"]");
|
||
i3_send_pkg(sock, I3_IPC_MESSAGE_TYPE_GET_WORKSPACES, NULL);
|
||
|
||
static const struct i3_ipc_callbacks callbacks = {
|
||
.burst_done = &burst_done,
|
||
.reply_version = &handle_get_version_reply,
|
||
.reply_subscribe = &handle_subscribe_reply,
|
||
.reply_workspaces = &handle_get_workspaces_reply,
|
||
.event_workspace = &handle_workspace_event,
|
||
.event_window = &handle_window_event,
|
||
.event_mode = &handle_mode_event,
|
||
};
|
||
|
||
bool ret = i3_receive_loop(mod->abort_fd, sock, &callbacks, mod);
|
||
close(sock);
|
||
return ret ? 0 : 1;
|
||
}
|
||
|
||
static void
|
||
destroy(struct module *mod)
|
||
{
|
||
struct private *m = mod->private;
|
||
|
||
for (size_t i = 0; i < m->ws_content.count; i++) {
|
||
struct particle *p = m->ws_content.v[i].content;
|
||
p->destroy(p);
|
||
free(m->ws_content.v[i].name);
|
||
}
|
||
|
||
free(m->ws_content.v);
|
||
workspaces_free(m, true);
|
||
|
||
for (size_t i = 0; i < m->persistent_count; i++)
|
||
free(m->persistent_workspaces[i]);
|
||
free(m->persistent_workspaces);
|
||
|
||
free(m->mode);
|
||
free(m);
|
||
module_default_destroy(mod);
|
||
}
|
||
|
||
static struct ws_content *
|
||
ws_content_for_name(struct private *m, const char *name)
|
||
{
|
||
for (size_t i = 0; i < m->ws_content.count; i++) {
|
||
struct ws_content *content = &m->ws_content.v[i];
|
||
if (strcmp(content->name, name) == 0)
|
||
return content;
|
||
}
|
||
|
||
return NULL;
|
||
}
|
||
|
||
static const char *
|
||
description(const struct module *mod)
|
||
{
|
||
return "i3/sway";
|
||
}
|
||
|
||
static struct exposable *
|
||
content(struct module *mod)
|
||
{
|
||
struct private *m = mod->private;
|
||
|
||
mtx_lock(&mod->lock);
|
||
|
||
size_t particle_count = 0;
|
||
struct exposable *particles[tll_length(m->workspaces) + 1];
|
||
struct exposable *current = NULL;
|
||
|
||
tll_foreach(m->workspaces, it)
|
||
{
|
||
struct workspace *ws = &it->item;
|
||
const struct ws_content *template = NULL;
|
||
|
||
/* Lookup content template for workspace. Fall back to default
|
||
* template if this workspace doesn't have a specific
|
||
* template */
|
||
if (ws->name == NULL) {
|
||
LOG_ERR("%d %d", ws->name_as_int, ws->id);
|
||
}
|
||
template = ws_content_for_name(m, ws->name);
|
||
if (template == NULL) {
|
||
LOG_DBG("no ws template for %s, using default template", ws->name);
|
||
template = ws_content_for_name(m, "");
|
||
}
|
||
|
||
const char *state = ws->urgent ? "urgent" : ws->visible ? ws->focused ? "focused" : "unfocused" : "invisible";
|
||
|
||
LOG_DBG("name=%s (name-as-int=%d): visible=%s, focused=%s, urgent=%s, empty=%s, state=%s, "
|
||
"application=%s, title=%s, mode=%s",
|
||
ws->name, ws->name_as_int, ws->visible ? "yes" : "no", ws->focused ? "yes" : "no",
|
||
ws->urgent ? "yes" : "no", ws->empty ? "yes" : "no", state, ws->window.application, ws->window.title,
|
||
m->mode);
|
||
|
||
const char *name = ws->name;
|
||
|
||
if (m->strip_workspace_numbers) {
|
||
const char *colon = strchr(name, ':');
|
||
if (colon != NULL)
|
||
name = colon + 1;
|
||
}
|
||
|
||
struct tag_set tags = {
|
||
.tags = (struct tag *[]){
|
||
tag_new_string(mod, "name", name),
|
||
tag_new_bool(mod, "visible", ws->visible),
|
||
tag_new_bool(mod, "focused", ws->focused),
|
||
tag_new_bool(mod, "urgent", ws->urgent),
|
||
tag_new_bool(mod, "empty", ws->empty),
|
||
tag_new_string(mod, "state", state),
|
||
|
||
tag_new_string(mod, "application", ws->window.application),
|
||
tag_new_string(mod, "title", ws->window.title),
|
||
|
||
tag_new_string(mod, "mode", m->mode),
|
||
},
|
||
.count = 9,
|
||
};
|
||
|
||
if (ws->focused) {
|
||
const struct ws_content *cur = ws_content_for_name(m, "current");
|
||
if (cur != NULL)
|
||
current = cur->content->instantiate(cur->content, &tags);
|
||
}
|
||
|
||
if (template == NULL) {
|
||
LOG_WARN("no ws template for %s, and no default template available", ws->name);
|
||
} else {
|
||
particles[particle_count++] = template->content->instantiate(template->content, &tags);
|
||
}
|
||
|
||
tag_set_destroy(&tags);
|
||
}
|
||
|
||
if (current != NULL)
|
||
particles[particle_count++] = current;
|
||
|
||
mtx_unlock(&mod->lock);
|
||
return dynlist_exposable_new(particles, particle_count, m->left_spacing, m->right_spacing);
|
||
}
|
||
|
||
/* Maps workspace name to a content particle. */
|
||
struct i3_workspaces {
|
||
const char *name;
|
||
struct particle *content;
|
||
};
|
||
|
||
static struct module *
|
||
i3_new(struct i3_workspaces workspaces[], size_t workspace_count, int left_spacing, int right_spacing,
|
||
enum sort_mode sort_mode, size_t persistent_count, const char *persistent_workspaces[static persistent_count],
|
||
bool strip_workspace_numbers)
|
||
{
|
||
struct private *m = calloc(1, sizeof(*m));
|
||
|
||
m->mode = strdup("default");
|
||
m->left_spacing = left_spacing;
|
||
m->right_spacing = right_spacing;
|
||
|
||
m->ws_content.count = workspace_count;
|
||
m->ws_content.v = malloc(workspace_count * sizeof(m->ws_content.v[0]));
|
||
|
||
for (size_t i = 0; i < workspace_count; i++) {
|
||
m->ws_content.v[i].name = strdup(workspaces[i].name);
|
||
m->ws_content.v[i].content = workspaces[i].content;
|
||
}
|
||
|
||
m->strip_workspace_numbers = strip_workspace_numbers;
|
||
m->sort_mode = sort_mode;
|
||
|
||
m->persistent_count = persistent_count;
|
||
m->persistent_workspaces = calloc(persistent_count, sizeof(m->persistent_workspaces[0]));
|
||
|
||
for (size_t i = 0; i < persistent_count; i++)
|
||
m->persistent_workspaces[i] = strdup(persistent_workspaces[i]);
|
||
|
||
struct module *mod = module_common_new();
|
||
mod->private = m;
|
||
mod->run = &run;
|
||
mod->destroy = &destroy;
|
||
mod->content = &content;
|
||
mod->description = &description;
|
||
return mod;
|
||
}
|
||
|
||
static struct module *
|
||
from_conf(const struct yml_node *node, struct conf_inherit inherited)
|
||
{
|
||
const struct yml_node *c = yml_get_value(node, "content");
|
||
const struct yml_node *spacing = yml_get_value(node, "spacing");
|
||
const struct yml_node *left_spacing = yml_get_value(node, "left-spacing");
|
||
const struct yml_node *right_spacing = yml_get_value(node, "right-spacing");
|
||
const struct yml_node *sort = yml_get_value(node, "sort");
|
||
const struct yml_node *persistent = yml_get_value(node, "persistent");
|
||
const struct yml_node *strip_workspace_number = yml_get_value(node, "strip-workspace-numbers");
|
||
|
||
int left = spacing != NULL ? yml_value_as_int(spacing) : left_spacing != NULL ? yml_value_as_int(left_spacing) : 0;
|
||
int right = spacing != NULL ? yml_value_as_int(spacing)
|
||
: right_spacing != NULL ? yml_value_as_int(right_spacing)
|
||
: 0;
|
||
|
||
const char *sort_value = sort != NULL ? yml_value_as_string(sort) : NULL;
|
||
enum sort_mode sort_mode = sort_value == NULL ? SORT_NONE
|
||
: strcmp(sort_value, "none") == 0 ? SORT_NONE
|
||
: strcmp(sort_value, "native") == 0 ? SORT_NATIVE
|
||
: strcmp(sort_value, "ascending") == 0 ? SORT_ASCENDING
|
||
: SORT_DESCENDING;
|
||
|
||
const size_t persistent_count = persistent != NULL ? yml_list_length(persistent) : 0;
|
||
const char *persistent_workspaces[persistent_count];
|
||
|
||
if (persistent != NULL) {
|
||
size_t idx = 0;
|
||
for (struct yml_list_iter it = yml_list_iter(persistent); it.node != NULL; yml_list_next(&it), idx++) {
|
||
persistent_workspaces[idx] = yml_value_as_string(it.node);
|
||
}
|
||
}
|
||
|
||
struct i3_workspaces workspaces[yml_dict_length(c)];
|
||
|
||
size_t idx = 0;
|
||
for (struct yml_dict_iter it = yml_dict_iter(c); it.key != NULL; yml_dict_next(&it), idx++) {
|
||
workspaces[idx].name = yml_value_as_string(it.key);
|
||
workspaces[idx].content = conf_to_particle(it.value, inherited);
|
||
}
|
||
|
||
return i3_new(workspaces, yml_dict_length(c), left, right, sort_mode, persistent_count, persistent_workspaces,
|
||
(strip_workspace_number != NULL ? yml_value_as_bool(strip_workspace_number) : false));
|
||
}
|
||
|
||
static bool
|
||
verify_content(keychain_t *chain, const struct yml_node *node)
|
||
{
|
||
if (!yml_is_dict(node)) {
|
||
LOG_ERR("%s: must be a dictionary of workspace-name: particle mappings", conf_err_prefix(chain, node));
|
||
return false;
|
||
}
|
||
|
||
for (struct yml_dict_iter it = yml_dict_iter(node); it.key != NULL; yml_dict_next(&it)) {
|
||
const char *key = yml_value_as_string(it.key);
|
||
if (key == NULL) {
|
||
LOG_ERR("%s: key must be a string (a i3 workspace name)", conf_err_prefix(chain, it.key));
|
||
return false;
|
||
}
|
||
|
||
if (!conf_verify_particle(chain_push(chain, key), it.value))
|
||
return false;
|
||
|
||
chain_pop(chain);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
static bool
|
||
verify_sort(keychain_t *chain, const struct yml_node *node)
|
||
{
|
||
return conf_verify_enum(chain, node, (const char *[]){"none", "native", "ascending", "descending"}, 4);
|
||
}
|
||
|
||
static bool
|
||
verify_persistent(keychain_t *chain, const struct yml_node *node)
|
||
{
|
||
return conf_verify_list(chain, node, &conf_verify_string);
|
||
}
|
||
|
||
static bool
|
||
verify_conf(keychain_t *chain, const struct yml_node *node)
|
||
{
|
||
static const struct attr_info attrs[] = {
|
||
{"spacing", false, &conf_verify_unsigned},
|
||
{"left-spacing", false, &conf_verify_unsigned},
|
||
{"right-spacing", false, &conf_verify_unsigned},
|
||
{"sort", false, &verify_sort},
|
||
{"persistent", false, &verify_persistent},
|
||
{"strip-workspace-numbers", false, &conf_verify_bool},
|
||
{"content", true, &verify_content},
|
||
{"anchors", false, NULL},
|
||
{NULL, false, NULL},
|
||
};
|
||
|
||
return conf_verify_dict(chain, node, attrs);
|
||
}
|
||
|
||
const struct module_iface module_i3_iface = {
|
||
.verify_conf = &verify_conf,
|
||
.from_conf = &from_conf,
|
||
};
|
||
|
||
#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES)
|
||
extern const struct module_iface iface __attribute__((weak, alias("module_i3_iface")));
|
||
#endif
|