mirror of
https://mzte.de/git/LordMZTE/dotfiles.git
synced 2024-12-14 16:43:41 +01:00
improve playtwitch
This commit is contained in:
parent
faa5c8cb9a
commit
711e8b49fb
6 changed files with 140 additions and 48 deletions
|
@ -1,6 +1,12 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const c = @import("ffi.zig").c;
|
const c = @import("ffi.zig").c;
|
||||||
const config = @import("config.zig");
|
const config = @import("config.zig");
|
||||||
|
const log = std.log.scoped(.state);
|
||||||
|
|
||||||
|
pub const ChannelEntry = struct {
|
||||||
|
name: []const u8,
|
||||||
|
comment: ?[]const u8,
|
||||||
|
};
|
||||||
|
|
||||||
mutex: std.Thread.Mutex,
|
mutex: std.Thread.Mutex,
|
||||||
win: *c.GLFWwindow,
|
win: *c.GLFWwindow,
|
||||||
|
@ -10,7 +16,7 @@ chatty: bool,
|
||||||
chatty_alive: bool,
|
chatty_alive: bool,
|
||||||
|
|
||||||
/// an array of channels, composed of slices into `channels_file_data`
|
/// an array of channels, composed of slices into `channels_file_data`
|
||||||
channels: ?[][]const u8,
|
channels: ?[]*ChannelEntry,
|
||||||
|
|
||||||
/// the data of the channels configuration file
|
/// the data of the channels configuration file
|
||||||
channels_file_data: ?[]u8,
|
channels_file_data: ?[]u8,
|
||||||
|
@ -24,6 +30,8 @@ streamlink_out: ?[]align(std.mem.page_size) u8,
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
pub fn init(win: *c.GLFWwindow) !*Self {
|
pub fn init(win: *c.GLFWwindow) !*Self {
|
||||||
|
log.info("creating state", .{});
|
||||||
|
|
||||||
// on the heap so this thing doesn't move.
|
// on the heap so this thing doesn't move.
|
||||||
const self = try std.heap.c_allocator.create(Self);
|
const self = try std.heap.c_allocator.create(Self);
|
||||||
self.* = .{
|
self.* = .{
|
||||||
|
@ -54,11 +62,13 @@ pub fn init(win: *c.GLFWwindow) !*Self {
|
||||||
|
|
||||||
pub fn freeStreamlinkMemfd(self: *Self) void {
|
pub fn freeStreamlinkMemfd(self: *Self) void {
|
||||||
if (self.streamlink_out) |mem| {
|
if (self.streamlink_out) |mem| {
|
||||||
|
log.info("unmapping streamlink output", .{});
|
||||||
std.os.munmap(mem);
|
std.os.munmap(mem);
|
||||||
self.streamlink_out = null;
|
self.streamlink_out = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.streamlink_memfd) |fd| {
|
if (self.streamlink_memfd) |fd| {
|
||||||
|
log.info("closing streamlink output", .{});
|
||||||
fd.close();
|
fd.close();
|
||||||
self.streamlink_memfd = null;
|
self.streamlink_memfd = null;
|
||||||
}
|
}
|
||||||
|
@ -68,6 +78,9 @@ pub fn deinit(self: *Self) void {
|
||||||
self.freeStreamlinkMemfd();
|
self.freeStreamlinkMemfd();
|
||||||
|
|
||||||
if (self.channels) |ch| {
|
if (self.channels) |ch| {
|
||||||
|
for (ch) |e| {
|
||||||
|
std.heap.c_allocator.destroy(e);
|
||||||
|
}
|
||||||
std.heap.c_allocator.free(ch);
|
std.heap.c_allocator.free(ch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,23 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const c = @import("ffi.zig").c;
|
const c = @import("ffi.zig").c;
|
||||||
const State = @import("State.zig");
|
const State = @import("State.zig");
|
||||||
|
const log = std.log.scoped(.config);
|
||||||
|
|
||||||
pub fn configLoaderThread(state: *State) !void {
|
pub fn configLoaderThread(state: *State) !void {
|
||||||
const home = std.os.getenv("HOME") orelse return error.HomeNotSet;
|
const home = std.os.getenv("HOME") orelse return error.HomeNotSet;
|
||||||
const channels_path = try std.fs.path.join(
|
const channels_path = try std.fs.path.join(
|
||||||
std.heap.c_allocator,
|
std.heap.c_allocator,
|
||||||
&.{ home, ".config", "playtwitch", "channels" },
|
&.{ home, ".config", "playtwitch", "channels.cfg" },
|
||||||
);
|
);
|
||||||
defer std.heap.c_allocator.free(channels_path);
|
defer std.heap.c_allocator.free(channels_path);
|
||||||
|
|
||||||
|
log.info("reading config from '{s}'", .{channels_path});
|
||||||
|
const start_time = std.time.milliTimestamp();
|
||||||
|
|
||||||
const file = std.fs.cwd().openFile(channels_path, .{}) catch |e| {
|
const file = std.fs.cwd().openFile(channels_path, .{}) catch |e| {
|
||||||
switch (e) {
|
switch (e) {
|
||||||
error.FileNotFound => {
|
error.FileNotFound => {
|
||||||
std.log.warn("Channels config file not found at {s}, skipping.", .{channels_path});
|
log.warn("channels config file not found at {s}, skipping.", .{channels_path});
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
else => return e,
|
else => return e,
|
||||||
|
@ -22,15 +26,46 @@ pub fn configLoaderThread(state: *State) !void {
|
||||||
defer file.close();
|
defer file.close();
|
||||||
|
|
||||||
const channels_data = try file.readToEndAlloc(std.heap.c_allocator, std.math.maxInt(usize));
|
const channels_data = try file.readToEndAlloc(std.heap.c_allocator, std.math.maxInt(usize));
|
||||||
var channels = std.ArrayList([]const u8).init(std.heap.c_allocator);
|
var channels = std.ArrayList(*State.ChannelEntry).init(std.heap.c_allocator);
|
||||||
|
|
||||||
var channels_iter = std.mem.split(u8, channels_data, "\n");
|
var channels_iter = std.mem.split(u8, channels_data, "\n");
|
||||||
while (channels_iter.next()) |channel| {
|
while (channels_iter.next()) |line| {
|
||||||
const trimmed = std.mem.trim(u8, channel, " \n\r");
|
var line_iter = std.mem.split(u8, line, ":");
|
||||||
if (trimmed.len > 0)
|
|
||||||
try channels.append(trimmed);
|
const channel = line_iter.next() orelse continue;
|
||||||
|
const channel_trimmed = std.mem.trim(u8, channel, " \n\r");
|
||||||
|
|
||||||
|
if (channel_trimmed.len == 0 or channel_trimmed[0] == '#')
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const comment_trimmed = blk: {
|
||||||
|
const comment = line_iter.next() orelse break :blk null;
|
||||||
|
|
||||||
|
var comment_trimmed = std.mem.trim(u8, comment, " \n\r");
|
||||||
|
|
||||||
|
if (comment_trimmed.len == 0)
|
||||||
|
break :blk null;
|
||||||
|
|
||||||
|
break :blk comment_trimmed;
|
||||||
|
};
|
||||||
|
|
||||||
|
const entry = try std.heap.c_allocator.create(State.ChannelEntry);
|
||||||
|
|
||||||
|
entry.* = .{
|
||||||
|
.name = channel_trimmed,
|
||||||
|
.comment = comment_trimmed,
|
||||||
|
};
|
||||||
|
|
||||||
|
try channels.append(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const end_time = std.time.milliTimestamp();
|
||||||
|
|
||||||
|
log.info(
|
||||||
|
"Loaded {d} channels in {d}ms",
|
||||||
|
.{ channels.items.len, end_time - start_time },
|
||||||
|
);
|
||||||
|
|
||||||
state.mutex.lock();
|
state.mutex.lock();
|
||||||
defer state.mutex.unlock();
|
defer state.mutex.unlock();
|
||||||
|
|
||||||
|
|
|
@ -103,22 +103,49 @@ pub fn winContent(state: *State) !void {
|
||||||
true,
|
true,
|
||||||
0,
|
0,
|
||||||
)) {
|
)) {
|
||||||
for (state.channels.?) |ch, i| {
|
_ = c.igBeginTable(
|
||||||
if (ch.len >= 128) {
|
"##qp_table",
|
||||||
std.log.err("name '{s}' too long!", .{ch});
|
2,
|
||||||
return error.ChannelNameTooLong;
|
c.ImGuiTableFlags_Resizable,
|
||||||
}
|
.{ .x = 0.0, .y = 0.0 },
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
defer c.igEndTable();
|
||||||
|
|
||||||
// add null byte
|
c.igTableHeadersRow();
|
||||||
var ch_buf: [128]u8 = undefined;
|
_ = c.igTableSetColumnIndex(0);
|
||||||
std.mem.copy(u8, &ch_buf, ch);
|
c.igTableHeader("Channel");
|
||||||
ch_buf[ch.len] = 0;
|
_ = c.igTableSetColumnIndex(1);
|
||||||
|
c.igTableHeader("Comment");
|
||||||
|
|
||||||
|
for (state.channels.?) |ch, i| {
|
||||||
|
var ch_buf: [256]u8 = undefined;
|
||||||
|
const formatted = try std.fmt.bufPrintZ(
|
||||||
|
&ch_buf,
|
||||||
|
"{s}",
|
||||||
|
.{ch.name},
|
||||||
|
);
|
||||||
|
|
||||||
c.igPushID_Int(@intCast(c_int, i));
|
c.igPushID_Int(@intCast(c_int, i));
|
||||||
defer c.igPopID();
|
defer c.igPopID();
|
||||||
if (c.igSelectable_Bool(&ch_buf, false, 0, .{ .x = 0.0, .y = 0.0 })) {
|
|
||||||
|
_ = c.igTableNextRow(0, 0.0);
|
||||||
|
_ = c.igTableSetColumnIndex(0);
|
||||||
|
|
||||||
|
if (c.igSelectable_Bool(
|
||||||
|
formatted.ptr,
|
||||||
|
false,
|
||||||
|
c.ImGuiSelectableFlags_SpanAllColumns,
|
||||||
|
.{ .x = 0.0, .y = 0.0 },
|
||||||
|
)) {
|
||||||
start = .{ .channels_idx = i };
|
start = .{ .channels_idx = i };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = c.igTableSetColumnIndex(1);
|
||||||
|
|
||||||
|
if (ch.comment) |comment| {
|
||||||
|
igu.sliceText(comment);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,7 +194,7 @@ pub fn winContent(state: *State) !void {
|
||||||
},
|
},
|
||||||
.channels_idx => |idx| {
|
.channels_idx => |idx| {
|
||||||
c.glfwHideWindow(state.win);
|
c.glfwHideWindow(state.win);
|
||||||
try launch.launchChildren(state, state.channels.?[idx]);
|
try launch.launchChildren(state, state.channels.?[idx].name);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const c = @import("ffi.zig").c;
|
const c = @import("ffi.zig").c;
|
||||||
const State = @import("State.zig");
|
const State = @import("State.zig");
|
||||||
|
const log = std.log.scoped(.launch);
|
||||||
|
|
||||||
pub fn launchChildren(state: *State, channel: []const u8) !void {
|
pub fn launchChildren(state: *State, channel: []const u8) !void {
|
||||||
std.log.info(
|
log.info(
|
||||||
"Starting for channel {s} with quality {s} (chatty: {})",
|
"starting for channel {s} with quality {s} (chatty: {})",
|
||||||
.{ channel, std.mem.sliceTo(&state.quality_buf, 0), state.chatty },
|
.{ channel, std.mem.sliceTo(&state.quality_buf, 0), state.chatty },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -13,7 +14,7 @@ pub fn launchChildren(state: *State, channel: []const u8) !void {
|
||||||
|
|
||||||
if (state.chatty and !state.chatty_alive) {
|
if (state.chatty and !state.chatty_alive) {
|
||||||
var chatty_arena = std.heap.ArenaAllocator.init(std.heap.c_allocator);
|
var chatty_arena = std.heap.ArenaAllocator.init(std.heap.c_allocator);
|
||||||
const channel_d = try chatty_arena.allocator().dupe(u8, channel);
|
const channel_d = try std.ascii.allocLowerString(chatty_arena.allocator(), channel);
|
||||||
const chatty_argv = try chatty_arena.allocator().dupe(
|
const chatty_argv = try chatty_arena.allocator().dupe(
|
||||||
[]const u8,
|
[]const u8,
|
||||||
&.{ "chatty", "-connect", "-channel", channel_d },
|
&.{ "chatty", "-connect", "-channel", channel_d },
|
||||||
|
@ -56,7 +57,10 @@ fn streamlinkThread(state: *State, channel: []const u8) !void {
|
||||||
state.mutex.lock();
|
state.mutex.lock();
|
||||||
defer state.mutex.unlock();
|
defer state.mutex.unlock();
|
||||||
|
|
||||||
const url = try std.fmt.allocPrintZ(arg_arena.allocator(), "https://twitch.tv/{s}", .{channel});
|
var ch_buf: [128]u8 = undefined;
|
||||||
|
const lower_channel = std.ascii.lowerString(&ch_buf, channel);
|
||||||
|
|
||||||
|
const url = try std.fmt.allocPrintZ(arg_arena.allocator(), "https://twitch.tv/{s}", .{lower_channel});
|
||||||
const quality = try std.cstr.addNullByte(arg_arena.allocator(), std.mem.sliceTo(&state.quality_buf, 0));
|
const quality = try std.cstr.addNullByte(arg_arena.allocator(), std.mem.sliceTo(&state.quality_buf, 0));
|
||||||
|
|
||||||
const streamlink_argv = try arg_arena.allocator().allocSentinel(
|
const streamlink_argv = try arg_arena.allocator().allocSentinel(
|
||||||
|
@ -80,31 +84,34 @@ fn streamlinkThread(state: *State, channel: []const u8) !void {
|
||||||
break :spawn pid;
|
break :spawn pid;
|
||||||
};
|
};
|
||||||
|
|
||||||
const success = std.os.waitpid(pid, 0).status == 0;
|
var success = std.os.waitpid(pid, 0).status == 0;
|
||||||
|
|
||||||
|
var size = (try memfile.stat()).size;
|
||||||
|
if (size == 0) {
|
||||||
|
try memfile.writeAll("<no output>");
|
||||||
|
size = (try memfile.stat()).size;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mem = try std.os.mmap(
|
||||||
|
null,
|
||||||
|
size,
|
||||||
|
std.os.PROT.READ,
|
||||||
|
std.os.MAP.PRIVATE,
|
||||||
|
memfd,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// If the stream ends, this silly program still exits with a non-zero status.
|
||||||
|
success = success or std.mem.containsAtLeast(u8, mem, 1, "Stream ended");
|
||||||
|
|
||||||
state.mutex.lock();
|
state.mutex.lock();
|
||||||
defer state.mutex.unlock();
|
defer state.mutex.unlock();
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
std.log.info("Streamlink exited successfully, closing.", .{});
|
std.os.munmap(mem);
|
||||||
|
log.info("streamlink exited successfully, closing.", .{});
|
||||||
c.glfwSetWindowShouldClose(state.win, 1);
|
c.glfwSetWindowShouldClose(state.win, 1);
|
||||||
} else {
|
} else {
|
||||||
var size = (try memfile.stat()).size;
|
|
||||||
|
|
||||||
if (size == 0) {
|
|
||||||
try memfile.writeAll("<no output>");
|
|
||||||
size = (try memfile.stat()).size;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mem = try std.os.mmap(
|
|
||||||
null,
|
|
||||||
size,
|
|
||||||
std.os.PROT.READ,
|
|
||||||
std.os.MAP.PRIVATE,
|
|
||||||
memfd,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
|
|
||||||
state.streamlink_memfd = memfile;
|
state.streamlink_memfd = memfile;
|
||||||
state.streamlink_out = mem;
|
state.streamlink_out = mem;
|
||||||
c.glfwShowWindow(state.win);
|
c.glfwShowWindow(state.win);
|
||||||
|
|
|
@ -2,8 +2,10 @@ const std = @import("std");
|
||||||
const c = @import("ffi.zig").c;
|
const c = @import("ffi.zig").c;
|
||||||
const gui = @import("gui.zig");
|
const gui = @import("gui.zig");
|
||||||
const State = @import("State.zig");
|
const State = @import("State.zig");
|
||||||
|
const log = std.log.scoped(.main);
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
|
log.info("initializing GLFW", .{});
|
||||||
_ = c.glfwSetErrorCallback(&glfwErrorCb);
|
_ = c.glfwSetErrorCallback(&glfwErrorCb);
|
||||||
if (c.glfwInit() == 0) {
|
if (c.glfwInit() == 0) {
|
||||||
return error.GlfwInit;
|
return error.GlfwInit;
|
||||||
|
@ -13,18 +15,21 @@ pub fn main() !void {
|
||||||
c.glfwWindowHint(c.GLFW_CONTEXT_VERSION_MINOR, 3);
|
c.glfwWindowHint(c.GLFW_CONTEXT_VERSION_MINOR, 3);
|
||||||
c.glfwWindowHint(c.GLFW_TRANSPARENT_FRAMEBUFFER, c.GLFW_TRUE);
|
c.glfwWindowHint(c.GLFW_TRANSPARENT_FRAMEBUFFER, c.GLFW_TRUE);
|
||||||
|
|
||||||
|
log.info("creating window", .{});
|
||||||
const win = c.glfwCreateWindow(500, 500, "playtwitch", null, null);
|
const win = c.glfwCreateWindow(500, 500, "playtwitch", null, null);
|
||||||
defer c.glfwTerminate();
|
defer c.glfwTerminate();
|
||||||
|
|
||||||
c.glfwMakeContextCurrent(win);
|
c.glfwMakeContextCurrent(win);
|
||||||
c.glfwSwapInterval(1);
|
c.glfwSwapInterval(1);
|
||||||
|
|
||||||
|
log.info("initializing GLEW", .{});
|
||||||
const glew_err = c.glewInit();
|
const glew_err = c.glewInit();
|
||||||
if (glew_err != c.GLEW_OK) {
|
if (glew_err != c.GLEW_OK) {
|
||||||
std.log.err("GLEW init error: {s}", .{c.glewGetErrorString(glew_err)});
|
std.log.err("GLEW init error: {s}", .{c.glewGetErrorString(glew_err)});
|
||||||
return error.GlewInit;
|
return error.GlewInit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.info("initializing ImGui", .{});
|
||||||
const ctx = c.igCreateContext(null);
|
const ctx = c.igCreateContext(null);
|
||||||
defer c.igDestroyContext(ctx);
|
defer c.igDestroyContext(ctx);
|
||||||
|
|
||||||
|
@ -60,7 +65,7 @@ pub fn main() !void {
|
||||||
c.igNewFrame();
|
c.igNewFrame();
|
||||||
|
|
||||||
const win_visible = c.igBegin(
|
const win_visible = c.igBegin(
|
||||||
"##",
|
"##main_win",
|
||||||
null,
|
null,
|
||||||
c.ImGuiWindowFlags_NoMove |
|
c.ImGuiWindowFlags_NoMove |
|
||||||
c.ImGuiWindowFlags_NoResize |
|
c.ImGuiWindowFlags_NoResize |
|
||||||
|
@ -97,5 +102,5 @@ pub fn main() !void {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn glfwErrorCb(e: c_int, d: [*c]const u8) callconv(.C) void {
|
fn glfwErrorCb(e: c_int, d: [*c]const u8) callconv(.C) void {
|
||||||
std.log.err("GLFW error {d}: {s}", .{ e, d });
|
log.err("GLFW error {d}: {s}", .{ e, d });
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
|
const std = @import("std");
|
||||||
const c = @import("ffi.zig").c;
|
const c = @import("ffi.zig").c;
|
||||||
|
const log = std.log.scoped(.theme);
|
||||||
|
|
||||||
pub fn loadTheme(colors: [*]c.ImVec4) void {
|
pub fn loadTheme(colors: [*]c.ImVec4) void {
|
||||||
colors[c.ImGuiCol_WindowBg] = c.ImVec4{ .x = 0.12, .y = 0.0, .z = 0.23, .w = 0.8 };
|
log.info("loading theme", .{});
|
||||||
|
|
||||||
|
colors[c.ImGuiCol_ButtonHovered] = c.ImVec4{ .x = 0.7, .y = 0.49, .z = 0.9, .w = 1.0 };
|
||||||
|
colors[c.ImGuiCol_Button] = c.ImVec4{ .x = 0.33, .y = 0.14, .z = 0.51, .w = 1.0 };
|
||||||
colors[c.ImGuiCol_ChildBg] = c.ImVec4{ .x = 0.1, .y = 0.0, .z = 0.2, .w = 0.85 };
|
colors[c.ImGuiCol_ChildBg] = c.ImVec4{ .x = 0.1, .y = 0.0, .z = 0.2, .w = 0.85 };
|
||||||
colors[c.ImGuiCol_FrameBg] = c.ImVec4{ .x = 0.45, .y = 0.2, .z = 0.69, .w = 1.0 };
|
colors[c.ImGuiCol_FrameBg] = c.ImVec4{ .x = 0.45, .y = 0.2, .z = 0.69, .w = 1.0 };
|
||||||
colors[c.ImGuiCol_Button] = c.ImVec4{ .x = 0.33, .y = 0.14, .z = 0.51, .w = 1.0 };
|
|
||||||
colors[c.ImGuiCol_ButtonHovered] = c.ImVec4{ .x = 0.7, .y = 0.49, .z = 0.9, .w = 1.0 };
|
|
||||||
colors[c.ImGuiCol_TitleBgActive] = c.ImVec4{ .x = 0.33, .y = 0.14, .z = 0.51, .w = 1.0 };
|
|
||||||
colors[c.ImGuiCol_Header] = c.ImVec4{ .x = 0.26, .y = 0.1, .z = 0.43, .w = 1.0 };
|
|
||||||
colors[c.ImGuiCol_HeaderHovered] = c.ImVec4{ .x = 0.45, .y = 0.2, .z = 0.69, .w = 1.0 };
|
colors[c.ImGuiCol_HeaderHovered] = c.ImVec4{ .x = 0.45, .y = 0.2, .z = 0.69, .w = 1.0 };
|
||||||
|
colors[c.ImGuiCol_Header] = c.ImVec4{ .x = 0.26, .y = 0.1, .z = 0.43, .w = 1.0 };
|
||||||
|
colors[c.ImGuiCol_TableHeaderBg] = c.ImVec4{ .x = 0.45, .y = 0.2, .z = 0.69, .w = 0.8 };
|
||||||
|
colors[c.ImGuiCol_TitleBgActive] = c.ImVec4{ .x = 0.33, .y = 0.14, .z = 0.51, .w = 1.0 };
|
||||||
|
colors[c.ImGuiCol_WindowBg] = c.ImVec4{ .x = 0.12, .y = 0.0, .z = 0.23, .w = 0.8 };
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue