Initial Commit

This commit is contained in:
LordMZTE 2022-07-11 14:34:11 +02:00
commit 7ab71c53a8
Signed by: LordMZTE
GPG Key ID: B64802DC33A64FF6
6 changed files with 486 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
zig-*

20
README.md Normal file
View File

@ -0,0 +1,20 @@
# rofi_nheko
A rofi plugins for [nheko](https://github.com/Nheko-Reborn/nheko) rooms.
## Build
Requirements:
- a master build of the zig compiler
- rofi and it's dependencies
- `gdbus-codegen`
```bash
zig build -Drelease fast
```
Find the artifacts in `zig-out/lib`.
## Installation
The build shared object must be copied to rofi's plugin path. By default, this is `/usr/lib/rofi`, but this can be expanded by using the `ROFI_PLUGIN_PATH` environment variable.
## Running
This plugin can be ran simply by executing `rofi -show nheko`.
Make sure nheko's DBus interface is enabled in the settings.

83
build.zig Normal file
View File

@ -0,0 +1,83 @@
const std = @import("std");
pub fn build(b: *std.build.Builder) !void {
// Standard release options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
const mode = b.standardReleaseOptions();
const lib = b.addSharedLibrary("rofi_nheko", "src/main.zig", .unversioned);
lib.setBuildMode(mode);
lib.linkLibC();
lib.linkSystemLibrary("glib-2.0");
lib.linkSystemLibrary("gio-unix-2.0");
lib.linkSystemLibrary("rofi");
lib.linkSystemLibrary("cairo");
lib.install();
lib.step.dependOn(&(try NhdbusGen.init(lib)).step);
const main_tests = b.addTest("src/main.zig");
main_tests.setBuildMode(mode);
const test_step = b.step("test", "Run library tests");
test_step.dependOn(&main_tests.step);
}
const NhdbusGen = struct {
step: std.build.Step,
lib: *std.build.LibExeObjStep,
pub fn init(lib: *std.build.LibExeObjStep) !*NhdbusGen {
const self = try lib.builder.allocator.create(NhdbusGen);
self.* = .{
.step = std.build.Step.init(.custom, "nhdbus-gen", lib.builder.allocator, make),
.lib = lib,
};
return self;
}
fn make(step: *std.build.Step) !void {
_ = step;
const self = @fieldParentPtr(NhdbusGen, "step", step);
const b = self.lib.builder;
const c_basename = "nhdbus";
const nhdbus_path = try std.fmt.allocPrint(
b.allocator,
"{s}/c/{s}.c",
.{ b.cache_root, c_basename },
);
defer b.allocator.free(nhdbus_path);
const include_path = nhdbus_path[0..nhdbus_path.len - c_basename.len - ".c".len];
try std.fs.cwd().makePath(include_path);
self.lib.addIncludePath(include_path);
self.lib.addCSourceFile(nhdbus_path, &.{});
var child = std.ChildProcess.init(&.{
"gdbus-codegen",
"--interface-prefix",
"im.nheko.Nheko",
"--generate-c-code",
c_basename,
"--c-namespace",
c_basename,
"--c-generate-object-manager",
"--output-directory",
include_path,
"nheko-dbus.xml",
}, b.allocator);
const term = try child.spawnAndWait();
switch (term) {
.Exited => |code| if (code != 0) return error.UnexpectedExitCode,
else => return error.UncleanExit,
}
}
};

65
nheko-dbus.xml Normal file
View File

@ -0,0 +1,65 @@
<!DOCTYPE
node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="im.nheko.Nheko">
<method name="apiVersion">
<arg type="s" direction="out" />
</method>
<method name="nhekoVersion">
<arg type="s" direction="out" />
</method>
<method name="rooms">
<arg type="a(sss(iiibiiay)i)" direction="out" />
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0"
value="QVector&lt;nheko::dbus::RoomInfoItem&gt;" />
</method>
<method name="activateRoom">
<arg name="alias" type="s" direction="in" />
</method>
<method name="joinRoom">
<arg name="alias" type="s" direction="in" />
</method>
<method name="directChat">
<arg name="userId" type="s" direction="in" />
</method>
</interface>
<interface name="org.freedesktop.DBus.Properties">
<method name="Get">
<arg name="interface_name" type="s" direction="in" />
<arg name="property_name" type="s" direction="in" />
<arg name="value" type="v" direction="out" />
</method>
<method name="Set">
<arg name="interface_name" type="s" direction="in" />
<arg name="property_name" type="s" direction="in" />
<arg name="value" type="v" direction="in" />
</method>
<method name="GetAll">
<arg name="interface_name" type="s" direction="in" />
<arg name="values" type="a{sv}" direction="out" />
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0"
value="QVariantMap" />
</method>
<signal name="PropertiesChanged">
<arg name="interface_name" type="s" direction="out" />
<arg name="changed_properties" type="a{sv}"
direction="out" />
<annotation name="org.qtproject.QtDBus.QtTypeName.Out1"
value="QVariantMap" />
<arg name="invalidated_properties" type="as"
direction="out" />
</signal>
</interface>
<interface name="org.freedesktop.DBus.Introspectable">
<method name="Introspect">
<arg name="xml_data" type="s" direction="out" />
</method>
</interface>
<interface name="org.freedesktop.DBus.Peer">
<method name="Ping" />
<method name="GetMachineId">
<arg name="machine_uuid" type="s" direction="out" />
</method>
</interface>
</node>

18
src/ffi.zig Normal file
View File

@ -0,0 +1,18 @@
const std = @import("std");
pub const c = @cImport({
@cInclude("nhdbus.h");
@cInclude("rofi/mode.h");
@cInclude("rofi/mode-private.h");
@cInclude("rofi/helper.h");
@cInclude("cairo.h");
});
// This is defined in view.h, which is currently missing in the Arch package.
pub extern fn rofi_view_reload() void;
pub fn checkGError(err: ?*c.GError) !void {
if (err) |e| {
std.log.err("GLib error: {s}\n", .{e.message});
return error.GError;
}
}

299
src/main.zig Normal file
View File

@ -0,0 +1,299 @@
const std = @import("std");
const ffi = @import("ffi.zig");
const c = ffi.c;
const Mode = c.Mode;
export var mode: Mode = .{
.abi_version = c.ABI_VERSION,
// elegant much
.name = &struct {
var name = "nheko".*;
}.name,
.cfg_name_key = [_]u8{0} ** 128,
.display_name = null,
._init = init,
._destroy = deinit,
._get_num_entries = getNumEntries,
._result = result,
._token_match = tokenMatch,
._get_display_value = getDisplayValue,
._get_icon = getIcon,
._get_completion = null,
._preprocess_input = null,
._get_message = null,
.free = null,
.ed = null,
.module = null,
.private_data = null,
};
const State = struct {
rooms: std.ArrayList(Room),
dbus: *c.nhdbus,
};
const Room = struct {
id: [*c]c.gchar,
alias: [*c]c.gchar,
name: [*c]c.gchar,
image: Image,
notifications: c_int,
pub fn deinit(self: *Room) void {
c.g_free(self.id);
c.g_free(self.alias);
c.g_free(self.name);
self.image.deinit();
}
};
const Image = struct {
width: c_int,
height: c_int,
has_alpha: c.gboolean,
bit_depth: c_int,
stride: c_int,
data: []u8,
pub fn deinit(self: *Image) void {
std.heap.c_allocator.free(self.data);
}
};
fn init(m: [*c]Mode) callconv(.C) c_int {
const state_p = std.heap.c_allocator.create(State) catch return 0;
state_p.rooms = std.ArrayList(Room).init(std.heap.c_allocator);
var err: ?*c.GError = null;
const proxy = c.nhdbus__proxy_new_for_bus_sync(
c.G_BUS_TYPE_SESSION,
c.G_DBUS_PROXY_FLAGS_NONE,
"im.nheko.Nheko",
"/",
null,
&err,
);
ffi.checkGError(err) catch return 0;
state_p.dbus = proxy.?;
c.mode_set_private_data(m, state_p);
c.nhdbus__call_rooms(
proxy,
null,
@ptrCast(c.GAsyncReadyCallback, onRoomsReceived),
state(m),
);
return 1;
}
fn onRoomsReceived(proxy: *c.nhdbus, res: *c.GAsyncResult, s: *State) void {
var rooms: ?*c.GVariant = null;
var err: ?*c.GError = null;
_ = c.nhdbus__call_rooms_finish(proxy, &rooms, res, &err);
ffi.checkGError(err) catch return;
var iter = c.g_variant_iter_new(rooms);
defer c.g_variant_iter_free(iter);
s.rooms.ensureUnusedCapacity(
@intCast(usize, c.g_variant_iter_n_children(iter)),
) catch return;
var room: ?*c.GVariant = null;
while (true) {
room = c.g_variant_iter_next_value(iter);
if (room == null)
break;
defer c.g_variant_unref(room);
var r: Room = undefined;
var data_iter: *c.GVariantIter = undefined;
c.g_variant_get(
room,
"(sss(iiibiiay)i)",
&r.id,
&r.alias,
&r.name,
&r.image.width,
&r.image.height,
&r.image.stride,
&r.image.has_alpha,
&r.image.bit_depth,
@as(?*anyopaque, null), // channel count
&data_iter,
&r.notifications,
);
const bytes = std.heap.c_allocator.alloc(
u8,
@intCast(usize, c.g_variant_iter_n_children(data_iter)),
) catch return;
var byte: u8 = 0;
var i: usize = 0;
while (c.g_variant_iter_next(data_iter, "y", &byte) != 0) {
bytes[i] = byte;
i += 1;
}
if (r.image.has_alpha != 0)
rgbaToCairo(bytes);
r.image.data = bytes;
s.rooms.append(r) catch return;
}
std.sort.sort(Room, s.rooms.items, {}, compareRoom);
ffi.rofi_view_reload();
}
fn compareRoom(_: void, lhs: Room, rhs: Room) bool {
if (lhs.notifications == rhs.notifications) {
return std.ascii.lessThanIgnoreCase(
std.mem.sliceTo(lhs.name, 0),
std.mem.sliceTo(rhs.name, 0),
);
} else {
return lhs.notifications > rhs.notifications;
}
}
fn deinit(m: [*c]Mode) callconv(.C) void {
for (state(m).rooms.items) |*r|
r.deinit();
state(m).rooms.deinit();
c.g_object_unref(state(m).dbus);
}
fn getNumEntries(m: [*c]const Mode) callconv(.C) c_uint {
return @intCast(c_uint, state(m).rooms.items.len);
}
fn result(
m: [*c]Mode,
_: c_int, // menu_retv
_: [*c][*c]u8, // input
selected_line: c_uint,
) callconv(.C) c.ModeMode {
// no line selecte
if (selected_line == std.math.maxInt(c_uint))
return c.MODE_EXIT;
var err: ?*c.GError = null;
_ = c.nhdbus__call_activate_room_sync(
state(m).dbus,
state(m).rooms.items[selected_line].id,
null,
&err,
);
ffi.checkGError(err) catch {};
return c.MODE_EXIT;
}
fn tokenMatch(
m: [*c]const Mode,
tokens: [*c][*c]c.rofi_int_matcher,
index: c_uint,
) callconv(.C) c_int {
return c.helper_token_match(tokens, state(m).rooms.items[index].name);
}
fn getDisplayValue(
m: [*c]const Mode,
selected_line: c_uint,
display_state: [*c]c_int,
_: [*c][*c]c.GList, // attribute_list
get_entry: c_int,
) callconv(.C) [*c]u8 {
if (get_entry == 0)
return null;
const room = state(m).rooms.items[selected_line];
if (room.notifications > 0) {
display_state.* |= 8; // STATE_MARKUP
return (std.fmt.allocPrintZ(
std.heap.c_allocator,
"<span color='red'>({})</span> {s}",
.{ room.notifications, room.name },
) catch @panic("Failed to allocate display value")).ptr;
} else return c.g_strdup(room.name);
}
fn getIcon(
m: [*c]const Mode,
selected_line: c_uint,
height: c_int,
) callconv(.C) ?*c.cairo_surface_t {
const img = &state(m).rooms.items[selected_line].image;
if (img.data.len == 0)
return null;
const format =
if (img.has_alpha != 0) c.CAIRO_FORMAT_ARGB32 else c.CAIRO_FORMAT_RGB24;
const img_surface = c.cairo_image_surface_create_for_data(
img.data.ptr,
format,
img.width,
img.height,
img.stride,
).?;
defer c.cairo_surface_destroy(img_surface);
const resized_surface = c.cairo_image_surface_create(format, height, height);
const cr = c.cairo_create(resized_surface);
c.cairo_scale(
cr,
@intToFloat(f64, height) / @intToFloat(f64, img.width),
@intToFloat(f64, height) / @intToFloat(f64, img.height),
);
c.cairo_set_source_surface(cr, img_surface, 0.0, 0.0);
c.cairo_paint(cr);
return resized_surface;
}
fn state(m: [*c]const Mode) *State {
const p = c.mode_get_private_data(m);
return @ptrCast(*State, @alignCast(@alignOf(State), p));
}
fn rgbaToCairo(data: []u8) void {
std.debug.assert(data.len % 4 == 0);
var i: usize = 0;
while (i < data.len) : (i += 4) {
const rgba = data[i .. i + 4];
std.mem.copy(
u8,
rgba,
switch (@import("builtin").cpu.arch.endian()) {
.Little => &[_]u8{
premultiply(rgba[2], rgba[3]),
premultiply(rgba[1], rgba[3]),
premultiply(rgba[0], rgba[3]),
rgba[3],
},
.Big => &[_]u8{
premultiply(rgba[0], rgba[3]),
premultiply(rgba[1], rgba[3]),
premultiply(rgba[2], rgba[3]),
rgba[3],
},
},
);
}
}
fn premultiply(color: u8, alpha: u8) u8 {
const z = @intCast(u16, color) * @intCast(u16, alpha) + 0x80;
return @intCast(u8, (z + (z >> 8)) >> 8);
}