delete playtwitch. not worth the trouble

This commit is contained in:
LordMZTE 2024-03-20 19:39:41 +01:00
parent 336fe9d864
commit 105fb94ef7
Signed by: LordMZTE
GPG Key ID: B64802DC33A64FF6
14 changed files with 0 additions and 1053 deletions

View File

@ -1,5 +0,0 @@
zig-cache/
zig-out/
deps.zig
gyro.lock
.gyro

View File

@ -1,6 +0,0 @@
[Desktop Entry]
Name=Playtwitch
Comment=Launch a twitch stream
Type=Application
Exec=playtwitch
Icon=playtwitch

View File

@ -1,79 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="512"
height="512"
viewBox="0 0 135.46666 135.46667"
version="1.1"
id="svg5"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14, custom)"
sodipodi:docname="logo.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="1.1250517"
inkscape:cx="230.21164"
inkscape:cy="251.09957"
inkscape:window-width="1876"
inkscape:window-height="1018"
inkscape:window-x="1940"
inkscape:window-y="40"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2">
<linearGradient
inkscape:collect="always"
id="linearGradient2011">
<stop
style="stop-color:#9146ff;stop-opacity:1;"
offset="0"
id="stop2007" />
<stop
style="stop-color:#d146ff;stop-opacity:1;"
offset="1"
id="stop2009" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2011"
id="linearGradient2013"
x1="8.4666662"
y1="67.733333"
x2="127"
y2="67.733333"
gradientUnits="userSpaceOnUse" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:url(#linearGradient2013);fill-opacity:1;stroke:none;stroke-width:6.83395;stroke-linejoin:round;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
id="rect1982"
width="118.53333"
height="118.53333"
x="8.4666662"
y="8.4666662"
rx="2.1166666"
ry="2.1166666" />
<path
id="rect131"
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:5.55387;stroke-linejoin:round;stroke-miterlimit:2;stroke-dasharray:none;stroke-opacity:1"
d="M 15.897361,15.897361 V 119.56931 H 119.56931 V 15.897361 Z M 41.6205,36.946187 93.846169,67.733336 41.6205,98.520488 Z" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1,42 +0,0 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "playtwitch",
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
exe.linkLibC();
exe.linkSystemLibrary("cimgui");
exe.linkSystemLibrary("glfw3");
exe.linkSystemLibrary("glew");
exe.linkSystemLibrary("curl");
b.installArtifact(exe);
var logo_install_step = b.addInstallFile(
.{ .path = "assets/playtwitch.svg" },
"share/icons/hicolor/scalable/apps/playtwitch.svg",
);
b.getInstallStep().dependOn(&logo_install_step.step);
var desktop_entry_install_step = b.addInstallFile(
.{ .path = "assets/playtwitch.desktop" },
"share/applications/playtwitch.desktop",
);
b.getInstallStep().dependOn(&desktop_entry_install_step.step);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}

View File

@ -1,109 +0,0 @@
const std = @import("std");
const c = @import("ffi.zig").c;
const config = @import("config.zig");
const log = std.log.scoped(.state);
pub const Entry = union(enum) {
channel: ChannelEntry,
/// a seperator in the channel list, the optional string is a heading.
separator: ?[]const u8,
};
pub const ChannelEntry = struct {
name: []const u8,
comment: ?[]const u8,
live: Live = .loading,
};
pub const Live = enum {
live,
offline,
loading,
err,
};
mutex: std.Thread.Mutex,
win: *c.GLFWwindow,
/// start chatty if true
chatty: bool,
chatty_alive: bool,
/// an array of channels, composed of slices into `channels_file_data`
channels: ?[]Entry,
/// the data of the channels configuration file
channels_file_data: ?[]u8,
quality_buf: [64]u8,
channel_name_buf: [64]u8,
streamlink_memfd: ?std.fs.File,
streamlink_out: ?[]align(std.mem.page_size) u8,
/// If the status of the channels is being loaded currently
live_status_loading: bool,
const Self = @This();
pub fn init(win: *c.GLFWwindow) !*Self {
log.info("creating state", .{});
// on the heap so this thing doesn't move.
const self = try std.heap.c_allocator.create(Self);
self.* = .{
.mutex = .{},
.win = win,
.chatty = true,
.chatty_alive = false,
// initialized by config loader thread
.channels = null,
.channels_file_data = null,
.quality_buf = std.mem.zeroes([64]u8),
.channel_name_buf = std.mem.zeroes([64]u8),
.streamlink_memfd = null,
.streamlink_out = null,
.live_status_loading = true,
};
@memcpy(self.quality_buf[0..4], "best");
const thread = try std.Thread.spawn(.{}, config.configLoaderThread, .{self});
thread.detach();
return self;
}
pub fn freeStreamlinkMemfd(self: *Self) void {
if (self.streamlink_out) |mem| {
log.info("unmapping streamlink output", .{});
std.os.munmap(mem);
self.streamlink_out = null;
}
if (self.streamlink_memfd) |fd| {
log.info("closing streamlink output", .{});
fd.close();
self.streamlink_memfd = null;
}
}
pub fn deinit(self: *Self) void {
self.freeStreamlinkMemfd();
if (self.channels) |ch| {
std.heap.c_allocator.free(ch);
}
if (self.channels_file_data) |d| {
std.heap.c_allocator.free(d);
}
self.* = undefined;
}

View File

@ -1,82 +0,0 @@
const std = @import("std");
const c = @import("ffi.zig").c;
const State = @import("State.zig");
const log = std.log.scoped(.config);
pub fn configLoaderThread(state: *State) !void {
const home = std.os.getenv("HOME") orelse return error.HomeNotSet;
const channels_path = try std.fs.path.join(
std.heap.c_allocator,
&.{ home, ".config", "playtwitch", "channels.cfg" },
);
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| {
switch (e) {
error.FileNotFound => {
log.warn("channels config file not found at {s}, skipping.", .{channels_path});
return;
},
else => return e,
}
};
defer file.close();
const channels_data = try file.readToEndAlloc(std.heap.c_allocator, std.math.maxInt(usize));
var channels = std.ArrayList(State.Entry).init(std.heap.c_allocator);
var channels_iter = std.mem.tokenize(u8, channels_data, "\n");
while (channels_iter.next()) |line| {
var line_iter = std.mem.tokenize(u8, line, ":");
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;
const comment_trimmed = std.mem.trim(u8, comment, " \n\r");
if (comment_trimmed.len == 0)
break :blk null;
break :blk comment_trimmed;
};
// dashes act as separator
if (std.mem.allEqual(u8, channel_trimmed, '-')) {
// separators can have comments to act as headings
try channels.append(.{ .separator = comment_trimmed });
continue;
}
try channels.append(.{ .channel = .{
.name = channel_trimmed,
.comment = comment_trimmed,
} });
}
const end_time = std.time.milliTimestamp();
log.info(
"Loaded {d} channel items in {d}ms",
.{ channels.items.len, end_time - start_time },
);
{
state.mutex.lock();
defer state.mutex.unlock();
state.channels_file_data = channels_data;
state.channels = try channels.toOwnedSlice();
}
@import("live.zig").tryFetchChannelsLive(state);
}

View File

@ -1,13 +0,0 @@
pub const c = @cImport({
@cInclude("curl/curl.h");
@cInclude("GL/glew.h");
@cInclude("GLFW/glfw3.h");
@cDefine("CIMGUI_DEFINE_ENUMS_AND_STRUCTS", "");
@cInclude("cimgui.h");
@cDefine("CIMGUI_USE_GLFW", "");
@cDefine("CIMGUI_USE_OPENGL3", "");
@cInclude("cimgui_impl.h");
});

View File

@ -1,291 +0,0 @@
const std = @import("std");
const c = @import("ffi.zig").c;
const igu = @import("ig_util.zig");
const launch = @import("launch.zig");
const State = @import("State.zig");
const StartType = union(enum) {
none,
channel_bar,
channels_idx: usize,
};
pub fn winContent(state: *State) !void {
state.mutex.lock();
defer state.mutex.unlock();
var start: StartType = .none;
if (c.igBeginTable(
"##text_inputs",
2,
0,
.{ .x = 0.0, .y = 0.0 },
0.0,
)) {
defer c.igEndTable();
c.igTableSetupColumn("##label", c.ImGuiTableColumnFlags_WidthFixed, 85.0, 0);
c.igTableSetupColumn("##input", 0, 0.0, 0);
_ = c.igTableNextRow(0, 0.0);
// Quality input
_ = c.igTableSetColumnIndex(0);
igu.sliceText("Quality");
_ = c.igTableSetColumnIndex(1);
if (c.igInputText(
"##quality_input",
&state.quality_buf,
state.quality_buf.len,
c.ImGuiInputTextFlags_EnterReturnsTrue,
null,
null,
)) {
start = .channel_bar;
}
var quality_popup_pos: c.ImVec2 = undefined;
c.igGetItemRectMin(&quality_popup_pos);
var quality_popup_size: c.ImVec2 = undefined;
c.igGetItemRectSize(&quality_popup_size);
c.igSameLine(0.0, 0.0);
if (c.igArrowButton("##open_quality_popup", c.ImGuiDir_Down)) {
c.igOpenPopup_Str("quality_popup", 0);
}
// open popup on arrow button click
c.igOpenPopupOnItemClick("quality_popup", 0);
var btn_size: c.ImVec2 = undefined;
c.igGetItemRectSize(&btn_size);
const preset_qualities = [_][:0]const u8{
"best",
"1080p60",
"720p60",
"480p",
"360p",
"worst",
"audio_only",
};
quality_popup_pos.y += quality_popup_size.y;
quality_popup_size.x += btn_size.x;
quality_popup_size.y += 5 + (quality_popup_size.y * @as(f32, @floatFromInt(preset_qualities.len)));
c.igSetNextWindowPos(quality_popup_pos, c.ImGuiCond_Always, .{ .x = 0.0, .y = 0.0 });
c.igSetNextWindowSize(quality_popup_size, c.ImGuiCond_Always);
if (c.igBeginPopup("quality_popup", c.ImGuiWindowFlags_NoMove)) {
defer c.igEndPopup();
for (preset_qualities) |quality| {
if (c.igSelectable_Bool(quality.ptr, false, 0, .{ .x = 0.0, .y = 0.0 })) {
@memcpy(state.quality_buf[0..quality.len], quality);
state.quality_buf[quality.len] = 0;
}
}
}
_ = c.igTableNextRow(0, 0.0);
_ = c.igTableSetColumnIndex(0);
igu.sliceText("Play Channel");
_ = c.igTableSetColumnIndex(1);
if (c.igInputText(
"##play_channel_input",
&state.channel_name_buf,
state.channel_name_buf.len,
c.ImGuiInputTextFlags_EnterReturnsTrue,
null,
null,
)) {
start = .channel_bar;
}
c.igSameLine(0.0, 0.0);
if (c.igButton("Play!", .{ .x = 0.0, .y = 0.0 })) {
start = .channel_bar;
}
}
if (state.channels != null) {
c.igBeginDisabled(state.live_status_loading);
defer c.igEndDisabled();
if (c.igButton("Refresh Status", .{ .x = 0.0, .y = 0.0 })) {
(try std.Thread.spawn(.{}, @import("live.zig").reloadLiveThread, .{state}))
.detach();
}
c.igSameLine(0, 5.0);
}
// Chatty checkbox
_ = c.igCheckbox("Start Chatty", &state.chatty);
if (state.channels != null and c.igBeginChild_Str(
"Quick Pick",
.{ .x = 0.0, .y = 0.0 },
true,
0,
)) {
_ = c.igBeginTable(
"##qp_table",
3,
c.ImGuiTableFlags_Resizable,
.{ .x = 0.0, .y = 0.0 },
0.0,
);
defer c.igEndTable();
c.igTableSetupColumn("Channel", 0, 0.0, 0);
c.igTableSetupColumn("Comment", 0, 0.0, 0);
c.igTableSetupColumn("Live?", c.ImGuiTableColumnFlags_WidthFixed, 80.0, 0);
c.igTableHeadersRow();
_ = c.igTableSetColumnIndex(0);
c.igTableHeader("Channel");
_ = c.igTableSetColumnIndex(1);
c.igTableHeader("Comment");
_ = c.igTableSetColumnIndex(2);
c.igTableHeader("Live?");
for (state.channels.?, 0..) |entry, i| {
c.igPushID_Int(@intCast(i));
defer c.igPopID();
_ = c.igTableNextRow(0, 0.0);
_ = c.igTableSetColumnIndex(0);
switch (entry) {
.channel => |ch| {
var ch_buf: [256]u8 = undefined;
const formatted = try std.fmt.bufPrintZ(
&ch_buf,
"{s}",
.{ch.name},
);
if (c.igSelectable_Bool(
formatted.ptr,
false,
c.ImGuiSelectableFlags_SpanAllColumns,
.{ .x = 0.0, .y = 0.0 },
)) {
start = .{ .channels_idx = i };
}
_ = c.igTableSetColumnIndex(1);
if (ch.comment) |comment| {
igu.sliceText(comment);
}
_ = c.igTableSetColumnIndex(2);
const live_color = switch (ch.live) {
.loading => c.ImVec4{ .x = 1.0, .y = 1.0, .z = 0.0, .w = 1.0 },
.live => c.ImVec4{ .x = 0.0, .y = 1.0, .z = 0.0, .w = 1.0 },
.offline => c.ImVec4{ .x = 1.0, .y = 0.0, .z = 0.0, .w = 1.0 },
.err => c.ImVec4{ .x = 0.8, .y = 0.0, .z = 0.0, .w = 1.0 },
};
const live_label = switch (ch.live) {
.loading => "Loading...",
.live => "Live",
.offline => "Offline",
.err => "Error",
};
const prev_col = c.igGetStyle().*.Colors[c.ImGuiCol_Text];
c.igGetStyle().*.Colors[c.ImGuiCol_Text] = live_color;
igu.sliceText(live_label);
c.igGetStyle().*.Colors[c.ImGuiCol_Text] = prev_col;
},
.separator => |heading| {
if (heading) |h| {
const spacer_size = c.ImVec2{ .x = 0.0, .y = 2.0 };
c.igDummy(spacer_size);
const prev_col = c.igGetStyle().*.Colors[c.ImGuiCol_Text];
c.igGetStyle().*.Colors[c.ImGuiCol_Text] = c.ImVec4{
.x = 0.7,
.y = 0.2,
.z = 0.9,
.w = 1.0,
};
igu.sliceText(h);
c.igGetStyle().*.Colors[c.ImGuiCol_Text] = prev_col;
// TODO: is this the best way to do the alignment?
c.igSeparator();
_ = c.igTableSetColumnIndex(1);
c.igDummy(spacer_size);
c.igDummy(c.ImVec2{ .x = 0.0, .y = c.igGetTextLineHeight() });
c.igSeparator();
_ = c.igTableSetColumnIndex(2);
c.igDummy(spacer_size);
c.igDummy(c.ImVec2{ .x = 0.0, .y = c.igGetTextLineHeight() });
c.igSeparator();
} else {
c.igSeparator();
_ = c.igTableSetColumnIndex(1);
c.igSeparator();
_ = c.igTableSetColumnIndex(2);
c.igSeparator();
}
},
}
}
}
if (state.channels != null)
c.igEndChild(); // END THE CHILD MWAAHAHA
if (state.streamlink_out) |out| {
c.igSetNextWindowSize(.{ .x = 400.0, .y = 150.0 }, c.ImGuiCond_Appearing);
var open = true;
if (c.igBeginPopupModal(
"Streamlink Crashed!",
&open,
c.ImGuiWindowFlags_Modal,
)) {
defer c.igEndPopup();
if (c.igBeginChild_Str(
"##output",
.{ .x = 0.0, .y = 0.0 },
true,
c.ImGuiWindowFlags_HorizontalScrollbar,
))
igu.sliceText(out);
c.igEndChild();
} else {
c.igOpenPopup_Str("Streamlink Crashed!", 0);
}
if (!open) {
state.freeStreamlinkMemfd();
}
}
if (start == .channel_bar and state.channel_name_buf[0] == 0) {
std.log.warn("Tried to start an empty stream!", .{});
start = .none;
}
switch (start) {
.none => {},
.channel_bar => {
c.glfwHideWindow(state.win);
try launch.launchChildren(
state,
std.mem.sliceTo(&state.channel_name_buf, 0),
);
},
.channels_idx => |idx| {
c.glfwHideWindow(state.win);
try launch.launchChildren(state, state.channels.?[idx].channel.name);
},
}
}

View File

@ -1,6 +0,0 @@
const std = @import("std");
const c = @import("ffi.zig").c;
pub fn sliceText(text: []const u8) void {
c.igTextUnformatted(text.ptr, text.ptr + text.len);
}

View File

@ -1,131 +0,0 @@
const std = @import("std");
const c = @import("ffi.zig").c;
const State = @import("State.zig");
const log = std.log.scoped(.launch);
pub fn launchChildren(state: *State, channel: []const u8) !void {
log.info(
"starting for channel {s} with quality {s} (chatty: {})",
.{ channel, std.mem.sliceTo(&state.quality_buf, 0), state.chatty },
);
// just to be safe...
state.freeStreamlinkMemfd();
if (state.chatty and !state.chatty_alive) {
var chatty_arena = std.heap.ArenaAllocator.init(std.heap.c_allocator);
const channel_d = try std.ascii.allocLowerString(chatty_arena.allocator(), channel);
const chatty_argv = try chatty_arena.allocator().dupe(
[]const u8,
&.{ "chatty", "-connect", "-channel", channel_d },
);
const chatty_child = std.ChildProcess.init(chatty_argv, std.heap.c_allocator);
const chatty_thread = try std.Thread.spawn(
.{},
chattyThread,
.{ state, chatty_child, chatty_arena },
);
chatty_thread.detach();
}
const channel_d = try std.heap.c_allocator.dupe(u8, channel);
const streamlink_thread = try std.Thread.spawn(
.{},
streamlinkThread,
.{ state, channel_d },
);
streamlink_thread.detach();
}
fn streamlinkThread(state: *State, channel: []const u8) !void {
defer std.heap.c_allocator.free(channel);
errdefer {
state.mutex.lock();
defer state.mutex.unlock();
c.glfwShowWindow(state.win);
}
const memfd = try std.os.memfd_create("streamlink_out", 0);
errdefer std.os.close(memfd);
const memfile = std.fs.File{ .handle = memfd };
var arg_arena = std.heap.ArenaAllocator.init(std.heap.c_allocator);
defer arg_arena.deinit();
const pid = spawn: {
state.mutex.lock();
defer state.mutex.unlock();
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 arg_arena.allocator().dupeZ(u8, std.mem.sliceTo(&state.quality_buf, 0));
const streamlink_argv = try arg_arena.allocator().allocSentinel(
?[*:0]const u8,
3,
null,
);
streamlink_argv[0] = "streamlink";
streamlink_argv[1] = url;
streamlink_argv[2] = quality;
// Doing it the C way because zig's ChildProcess ain't got this
const pid = try std.os.fork();
if (pid == 0) {
try std.os.dup2(memfd, 1);
try std.os.dup2(memfd, 2);
return std.os.execvpeZ(streamlink_argv[0].?, streamlink_argv, std.c.environ);
}
break :spawn pid;
};
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,
.{ .TYPE = .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();
defer state.mutex.unlock();
if (success) {
std.os.munmap(mem);
log.info("streamlink exited successfully, closing.", .{});
c.glfwSetWindowShouldClose(state.win, 1);
} else {
state.streamlink_memfd = memfile;
state.streamlink_out = mem;
c.glfwShowWindow(state.win);
}
}
fn chattyThread(state: *State, child: std.ChildProcess, arena: std.heap.ArenaAllocator) !void {
// no need to get the mutex here, chatty_alive is atomic
@atomicStore(bool, &state.chatty_alive, true, .unordered);
defer @atomicStore(bool, &state.chatty_alive, false, .unordered);
var ch = child;
defer arena.deinit();
_ = ch.spawnAndWait() catch |e| {
std.log.err("Spawning Chatty: {!}", .{e});
};
}

View File

@ -1,125 +0,0 @@
const std = @import("std");
const State = @import("State.zig");
const c = @import("ffi.zig").c;
const log = std.log.scoped(.live);
pub fn reloadLiveThread(s: *State) !void {
{
s.mutex.lock();
defer s.mutex.unlock();
for (s.channels.?) |*chan| {
switch (chan.*) {
.channel => |*ch| ch.live = .loading,
else => {},
}
}
}
tryFetchChannelsLive(s);
}
pub fn tryFetchChannelsLive(s: *State) void {
fetchChannelsLive(s) catch |e| {
log.err("fetching status: {}", .{e});
s.mutex.lock();
defer s.mutex.unlock();
for (s.channels.?) |*chan| {
switch (chan.*) {
.channel => |*ch| ch.live = .err,
else => {},
}
}
};
}
fn fetchChannelsLive(s: *State) !void {
@atomicStore(bool, &s.live_status_loading, true, .unordered);
defer @atomicStore(bool, &s.live_status_loading, false, .unordered);
log.info("initiaizing cURL", .{});
const curl = c.curl_easy_init();
if (curl == null)
return error.CurlInitError;
defer c.curl_easy_cleanup(curl);
try handleCurlErr(c.curl_easy_setopt(
curl,
c.CURLOPT_WRITEFUNCTION,
&curlWriteCb,
));
try handleCurlErr(c.curl_easy_setopt(curl, c.CURLOPT_NOPROGRESS, @as(c_long, 1)));
try handleCurlErr(c.curl_easy_setopt(curl, c.CURLOPT_FOLLOWLOCATION, @as(c_long, 1)));
// the twitch info grabbinator works by downloading the web page
// and checking if it contains a string. this is the bufffer for the page.
//
// Fuck you, twitch! amazing API design!
var page_buf = std.ArrayList(u8).init(std.heap.c_allocator);
defer page_buf.deinit();
try handleCurlErr(c.curl_easy_setopt(curl, c.CURLOPT_WRITEDATA, &page_buf));
// we shouldn't need to aquire the mutex here, this data isnt being read and we're
// only doing atomic writes.
var fmt_buf: [512]u8 = undefined;
for (s.channels.?) |*entry| {
const chan = if (entry.* == .channel) &entry.channel else continue;
log.info("requesting live state for channel {s}", .{chan.name});
const url = try std.fmt.bufPrintZ(
&fmt_buf,
"https://www.twitch.tv/{s}",
.{chan.name},
);
try handleCurlErr(c.curl_easy_setopt(curl, c.CURLOPT_URL, url.ptr));
var tries: u8 = 3;
while (tries > 0) : (tries -= 1) {
page_buf.clearRetainingCapacity();
try handleCurlErr(c.curl_easy_perform(curl));
var response: c_long = 0;
try handleCurlErr(c.curl_easy_getinfo(curl, c.CURLINFO_RESPONSE_CODE, &response));
if (response != 200) {
log.warn(
"got error response {}, retrying ({} tries left)",
.{ response, tries },
);
continue;
}
break;
}
if (tries == 0) {
@atomicStore(State.Live, &chan.live, .err, .unordered);
}
if (std.mem.containsAtLeast(u8, page_buf.items, 1, "live_user")) {
@atomicStore(State.Live, &chan.live, .live, .unordered);
} else {
@atomicStore(State.Live, &chan.live, .offline, .unordered);
}
}
}
fn curlWriteCb(
data: [*]const u8,
size: usize,
nmemb: usize,
out: *std.ArrayList(u8),
) callconv(.C) usize {
const realsize = size * nmemb;
out.writer().writeAll(data[0..realsize]) catch return 0;
return realsize;
}
fn handleCurlErr(code: c.CURLcode) !void {
if (code != c.CURLE_OK) {
log.err("Curl error: {s}", .{c.curl_easy_strerror(code)});
return error.CurlError;
}
}

View File

@ -1,116 +0,0 @@
const std = @import("std");
const c = @import("ffi.zig").c;
const gui = @import("gui.zig");
const State = @import("State.zig");
const log = std.log.scoped(.main);
pub const std_options = std.Options{
.log_level = .debug,
};
pub fn main() !void {
log.info("initializing GLFW", .{});
_ = c.glfwSetErrorCallback(&glfwErrorCb);
if (c.glfwInit() == 0) {
return error.GlfwInit;
}
c.glfwWindowHint(c.GLFW_CONTEXT_VERSION_MAJOR, 3);
c.glfwWindowHint(c.GLFW_CONTEXT_VERSION_MINOR, 3);
c.glfwWindowHint(c.GLFW_TRANSPARENT_FRAMEBUFFER, c.GLFW_TRUE);
log.info("creating window", .{});
const win = c.glfwCreateWindow(500, 500, "playtwitch", null, null);
defer c.glfwTerminate();
c.glfwMakeContextCurrent(win);
c.glfwSwapInterval(1);
log.info("initializing GLEW", .{});
const glew_err = c.glewInit();
if (glew_err != c.GLEW_OK) {
std.log.err("GLEW init error: {s}", .{c.glewGetErrorString(glew_err)});
return error.GlewInit;
}
log.info("initializing ImGui", .{});
const ctx = c.igCreateContext(null);
defer c.igDestroyContext(ctx);
const io = c.igGetIO();
io.*.ConfigFlags |= c.ImGuiConfigFlags_NavEnableKeyboard;
io.*.IniFilename = null;
io.*.LogFilename = null;
_ = c.ImGui_ImplGlfw_InitForOpenGL(win, true);
defer c.ImGui_ImplGlfw_Shutdown();
_ = c.ImGui_ImplOpenGL3_Init("#version 330 core");
defer c.ImGui_ImplOpenGL3_Shutdown();
c.igStyleColorsDark(null);
@import("theme.zig").loadTheme(&c.igGetStyle().*.Colors);
const font = try @import("theme.zig").loadFont();
const state = try State.init(win.?);
defer state.deinit();
while (c.glfwWindowShouldClose(win) == 0) {
if (c.glfwGetWindowAttrib(win, c.GLFW_VISIBLE) == 0)
continue;
c.glfwPollEvents();
var win_width: c_int = 0;
var win_height: c_int = 0;
c.glfwGetWindowSize(win, &win_width, &win_height);
c.ImGui_ImplOpenGL3_NewFrame();
c.ImGui_ImplGlfw_NewFrame();
c.igNewFrame();
if (font) |f|
c.igPushFont(f);
const win_visible = c.igBegin(
"##main_win",
null,
c.ImGuiWindowFlags_NoMove |
c.ImGuiWindowFlags_NoResize |
c.ImGuiWindowFlags_NoDecoration |
c.ImGuiWindowFlags_NoBringToFrontOnFocus |
c.ImGuiWindowFlags_NoNavFocus,
);
c.igSetWindowPos_Vec2(
.{ .x = 0.0, .y = 0.0 },
c.ImGuiCond_Always,
);
c.igSetWindowSize_Vec2(
.{ .x = @floatFromInt(win_width), .y = @floatFromInt(win_height) },
c.ImGuiCond_Always,
);
if (win_visible) {
try gui.winContent(state);
}
if (font != null)
c.igPopFont();
c.igEnd();
c.igEndFrame();
c.glViewport(0, 0, win_width, win_height);
c.glClear(c.GL_COLOR_BUFFER_BIT);
c.glClearColor(0.0, 0.0, 0.0, 0.0);
c.igRender();
c.ImGui_ImplOpenGL3_RenderDrawData(c.igGetDrawData());
c.glfwSwapBuffers(win);
}
}
fn glfwErrorCb(e: c_int, d: [*c]const u8) callconv(.C) void {
log.err("GLFW error {d}: {s}", .{ e, d });
}

View File

@ -1,47 +0,0 @@
const std = @import("std");
const c = @import("ffi.zig").c;
const log = std.log.scoped(.theme);
pub fn loadTheme(colors: [*]c.ImVec4) void {
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_FrameBg] = 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 };
}
pub fn loadFont() !?*c.ImFont {
log.info("loading fonts", .{});
const fonts = [_][:0]const u8{
"/usr/share/fonts/TTF/IosevkaNerdFont-Regular.ttf",
"/usr/share/fonts/noto/NotoSans-Regular.ttf",
};
for (fonts) |font| {
const found = if (std.fs.accessAbsolute(font, .{})) |_|
true
else |e| if (e == error.FileNotFound)
false
else
return e;
if (found) {
log.info("using font {s}", .{font});
return c.ImFontAtlas_AddFontFromFileTTF(
c.igGetIO().*.Fonts,
font.ptr,
16,
null,
null,
);
}
}
return null;
}

View File

@ -23,7 +23,6 @@
(install-zig "scripts/mzteinit")
(install-zig "scripts/mzteriver")
(install-zig "scripts/openbrowser")
(install-zig "scripts/playtwitch")
(install-zig "scripts/playvid")
(install-zig "scripts/prompt")
(install-zig "scripts/randomwallpaper")