Skip to content

Commit

Permalink
Add i3 support
Browse files Browse the repository at this point in the history
This commit adds I3Exec, it's tests, integration into main() and some
utilities (one of which is now used in Dmenu).

closes #143
  • Loading branch information
meator committed Jan 19, 2024
1 parent 8acf7d0 commit e8e98c7
Show file tree
Hide file tree
Showing 10 changed files with 538 additions and 31 deletions.
18 changes: 17 additions & 1 deletion j4-dmenu-desktop.1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.Dd $Mdocdate: July 18 2021$
.Dd $Mdocdate: January 10 2024$
.Dt J4-DMENU-DESKTOP 1
.Os
.Sh NAME
Expand Down Expand Up @@ -38,11 +38,27 @@ Must point to a path where a file can be created. In this mode no menu will be s
to be written to (use echo > path). Every time this happens a menu will be shown. Desktop files are parsed ahead of time. Performing 'echo -n q > path' will exit the program.
.It Fl Fl wrapper Ar wrapper
A wrapper binary. Useful in case you want to wrap into 'i3 exec'.
.It Fl I , Fl Fl i3-ipc
Execute desktop entries through i3 IPC. Requires i3 to be running.
.It Fl Fl skip-i3-exec-check
Disable the check for
.Cm \-\-wrapper Qq i3 exec .
j4-dmenu-desktop has direct support for i3 through the
.Fl I
flag which should be used instead of the
.Fl \-wrapper
option. j4-dmenu-desktop detects this and exits.
This flag overrides this behaviour.
.It Fl i , Fl Fl case-insensitive
Sort applications case insensitively
.It Fl h , Fl Fl help
Display help message.
.El
.Sh ENVIRONMENT
.Bl -tag -width Fl
.It Ev I3SOCK
This variable overwrites the i3 IPC socket path.
.El
.Sh SEE ALSO
.Lk https://github.com/enkore/j4-dmenu-desktop
.Sh COPYRIGHT
Expand Down
23 changes: 3 additions & 20 deletions src/Dmenu.cc
Original file line number Diff line number Diff line change
Expand Up @@ -17,31 +17,14 @@

#include "Dmenu.hh"

static int write_proper(const int fd, const char *buf, size_t size) {
while (size) {
ssize_t written = write(fd, buf, size);
if (written == -1) {
if ((errno == EINTR) || (errno == EWOULDBLOCK) ||
(errno == EAGAIN)) {
continue;
} else {
break;
}
}

buf += written;
size -= written;
}

return size ? -1 : 0;
}
#include "Utilities.hh"

Dmenu::Dmenu(const std::string &dmenu_command, const char *sh)
: dmenu_command(dmenu_command), shell(sh) {}

void Dmenu::write(std::string_view what) {
write_proper(this->outpipe[1], what.data(), what.size());
write_proper(this->outpipe[1], "\n", 1);
writen(this->outpipe[1], what.data(), what.size());
writen(this->outpipe[1], "\n", 1);
}

void Dmenu::display() {
Expand Down
67 changes: 67 additions & 0 deletions src/FSUtils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,71 @@ bool compare_files(const char *a, const char *b) {
return false;
}
}

static void rmdir_impl(const std::string &path) {
DIR *d = opendir(path.c_str());
if (d == NULL)
throw std::runtime_error("Error while calling opendir() on '" + path +
"': " + strerror(errno));

OnExit closed = [d]() { closedir(d); };

dirent *dirinfo;
errno = 0;
while ((dirinfo = readdir(d)) != NULL) {
if (strcmp(dirinfo->d_name, ".") == 0 ||
strcmp(dirinfo->d_name, "..") == 0)
continue;

string subpath = path + '/' + dirinfo->d_name;

enum class file_type { file, directory } ft;
switch (dirinfo->d_type) {
case DT_DIR:
ft = file_type::file;
break;
case DT_UNKNOWN:
struct stat info;
if (stat(subpath.c_str(), &info) == -1)
throw std::runtime_error("Error while calling stat() on '" +
subpath + "': " + strerror(errno));
ft = S_ISDIR(info.st_mode) ? file_type::directory : file_type::file;
break;
default:
ft = file_type::file;
break;
}

switch (ft) {
case file_type::directory:
rmdir_recursive(subpath.c_str());
if (rmdir(subpath.c_str()) == -1)
throw std::runtime_error("Error while calling rmdir() on '" +
subpath + "': " + strerror(errno));
break;
case file_type::file:
if (unlink(subpath.c_str()) == -1)
throw std::runtime_error("Error while calling unlink() on '" +
subpath + "': " + strerror(errno));
}

errno = 0;
}
if (errno != 0)
throw std::runtime_error("Error while calling readdir() on '" + path +
"': " + strerror(errno));
}

void rmdir_recursive(const char *dirname) {
try {
rmdir_impl(dirname);
} catch (const std::runtime_error &e) {
throw std::runtime_error(
(string) "Error while recursively removing directory '" + dirname +
"': " + e.what());
}
if (rmdir(dirname) == -1)
throw std::runtime_error((string) "Error while calling rmdir() on '" +
dirname + "': " + strerror(errno));
}
}; // namespace FSUtils
1 change: 1 addition & 0 deletions src/FSUtils.hh
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ namespace FSUtils
void copy_file_fd(int in, int out);
void copy_file(const char *from, const char *to);
bool compare_files(const char *a, const char *b);
void rmdir_recursive(const char *dirname);
}; // namespace FSUtils

#endif
229 changes: 229 additions & 0 deletions src/I3Exec.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
#include "I3Exec.hh"

#include <inttypes.h>
#include <limits>
#include <memory>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <unistd.h>

#include "Utilities.hh"

using std::string;

string i3_get_ipc_socket_path() {
int pipefd[2];
if (pipe(pipefd) == -1)
pfatale("pipe");

auto pid = fork();
switch (pid) {
case -1:
pfatale("fork");
case 0:
close(STDIN_FILENO);
close(pipefd[0]);

if (dup2(pipefd[1], STDOUT_FILENO) == -1)
pfatale("dup2");

execlp("i3", "i3", "--get-socketpath", (char *)NULL);
perror("Couldn't execute 'i3 --get-socketpath'");
abort();
}
// This is the parent process.
close(pipefd[1]);

int status;
if (waitpid(pid, &status, 0) == -1)
pfatale("waitpid");
if (!WIFEXITED(status)) {
fprintf(stderr, "'i3 --get-socketpath' has exited abnormally!\n");
exit(EXIT_FAILURE);
}
if (WEXITSTATUS(status) != 0) {
fprintf(stderr,
"'i3 --get-socketpath' has exited with exit status %d!\n",
WEXITSTATUS(status));
exit(EXIT_FAILURE);
}

string result;
char buf[512];
ssize_t size;
while ((size = read(pipefd[0], buf, sizeof buf)) > 0)
result.append(buf, size);
if (size == -1)
pfatale("read");

if (result.empty()) {
fprintf(stderr, "Got no output from 'i3 --get-socketpath'!\n");
abort();
}
if (result.back() != '\n') {
fprintf(stderr, "'i3 --get-socketpath': Expected a newline!\n");
abort();
}
result.pop_back();
return result;
}

struct JSONError : public std::exception
{
using std::exception::exception;
};

// This function expects iter to be after the starting " of a string literal.
static string read_JSON_string(string::const_iterator iter,
const string::const_iterator &end_iter) {

if (iter == end_iter)
abort();

bool escaping = false;
string result;
while (true) {
const char &ch = *iter;
if (escaping) {
switch (ch) {
case '"':
result += '"';
break;
case '\\':
result += '\\';
break;
case '/':
result += '/';
break;
case 'b':
result += '\b';
break;
case 'f':
result += '\f';
break;
case 'n':
result += '\n';
break;
case 'r':
result += '\r';
break;
case 't':
result += '\t';
break;
case 'u':
// Not implemented. This is unlikely to appear in i3 response.
throw JSONError();
}

escaping = false;
} else {
switch (ch) {
case '\\':
escaping = true;
break;
case '"':
return result;
default:
result += ch;
break;
}
}

if (++iter == end_iter)
throw JSONError();
}
}

void i3_exec(const std::string &command, const std::string &socket_path) {
if (command.size() > std::numeric_limits<unsigned int>::max()) {
fprintf(
stderr, "Command '%s' is too long! (expected <= %lu, got %u)\n",
command.c_str(), (unsigned long)std::numeric_limits<int32_t>::max(),
(unsigned int)command.size());
abort();
}

#ifdef DEBUG
fprintf(stderr, "DEBUG: I3 IPC path: %s\n", socket_path.c_str());
#endif

if (socket_path.size() >= sizeof(sockaddr_un::sun_path)) {
fprintf(stderr,
"Socket address '%s' returned by 'i3 --get-socketpath' is too "
"long! (expected <= %lu, got %lu)\n",
socket_path.c_str(), sizeof(sockaddr_un::sun_path),
socket_path.size());
abort();
}

int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
if (sfd == -1)
pfatale("socket");

OnExit close_sfd = [sfd]() { close(sfd); };

struct sockaddr_un addr;
memset(&addr, 0, sizeof(sockaddr_un));
addr.sun_family = AF_UNIX;
memcpy(addr.sun_path, socket_path.data(), socket_path.size());

if (connect(sfd, (struct sockaddr *)&addr, sizeof(sockaddr_un)) == -1)
pfatale("connect");

auto payload_size = 16 + command.size();
auto payload = std::make_unique<char[]>(payload_size);

sprintf(payload.get(), "i3-ipc%" PRId32 "%" PRId32 "%.*s", (int32_t)0,
(int32_t)command.size(), (int)command.size(), command.data());

if (writen(sfd, payload.get(), payload_size) <= 0)
pfatale("write");

string response;
char buf[512];
while (true) {
auto ret = read(sfd, buf, sizeof buf);
if (ret == -1) {
if (errno == EINTR)
continue;
else
pfatale("read");
} else if (ret == 0)
break;
else
response.append(buf, ret);
}

#ifdef DEBUG
fprintf(stderr, "DEBUG: I3 IPC response: %s\n", response.c_str());
#endif

// Ideally a JSON parser should be employed here. But I don't want to add
// additional dependencies. If any breakage through this custom parsing
// should arise, please file a GitHub issue.
if (response.find(R"("success":true)") != string::npos)
return;
else if (response.find(R"("success":false)") != string::npos) {
fprintf(stderr, "An error occured while communicating with i3!\n");
auto where = response.find(R"("error":)");
if (where != string::npos) {
try {
string error = read_JSON_string(response.cbegin() + where + 8,
response.cend());
fprintf(stderr, "%s\n", error.c_str());
} catch (const JSONError &) {
fprintf(stderr,
"Error whil executing '%s' through I3 IPC: Invalid "
"response.\n",
command.c_str());
abort();
}
}
abort();
} else {
fprintf(stderr,
"A parsing error occured while reading i3's response!\n");
abort();
}
}
Loading

0 comments on commit e8e98c7

Please sign in to comment.