diff --git a/scripts/playtwitch/.gitignore b/scripts/playtwitch/.gitignore deleted file mode 100644 index 1ae1b10..0000000 --- a/scripts/playtwitch/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -zig-cache/ -zig-out/ -deps.zig -gyro.lock -.gyro diff --git a/scripts/playtwitch/assets/playtwitch.desktop b/scripts/playtwitch/assets/playtwitch.desktop deleted file mode 100644 index a2c9e4a..0000000 --- a/scripts/playtwitch/assets/playtwitch.desktop +++ /dev/null @@ -1,6 +0,0 @@ -[Desktop Entry] -Name=Playtwitch -Comment=Launch a twitch stream -Type=Application -Exec=playtwitch -Icon=playtwitch diff --git a/scripts/playtwitch/assets/playtwitch.svg b/scripts/playtwitch/assets/playtwitch.svg deleted file mode 100644 index 7dee1a9..0000000 --- a/scripts/playtwitch/assets/playtwitch.svg +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/scripts/playtwitch/build.zig b/scripts/playtwitch/build.zig deleted file mode 100644 index a0b132e..0000000 --- a/scripts/playtwitch/build.zig +++ /dev/null @@ -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); -} diff --git a/scripts/playtwitch/src/State.zig b/scripts/playtwitch/src/State.zig deleted file mode 100644 index 3d7c94c..0000000 --- a/scripts/playtwitch/src/State.zig +++ /dev/null @@ -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; -} diff --git a/scripts/playtwitch/src/config.zig b/scripts/playtwitch/src/config.zig deleted file mode 100644 index e1d42b9..0000000 --- a/scripts/playtwitch/src/config.zig +++ /dev/null @@ -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); -} diff --git a/scripts/playtwitch/src/ffi.zig b/scripts/playtwitch/src/ffi.zig deleted file mode 100644 index fec9b2a..0000000 --- a/scripts/playtwitch/src/ffi.zig +++ /dev/null @@ -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"); -}); diff --git a/scripts/playtwitch/src/gui.zig b/scripts/playtwitch/src/gui.zig deleted file mode 100644 index 7df935a..0000000 --- a/scripts/playtwitch/src/gui.zig +++ /dev/null @@ -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); - }, - } -} diff --git a/scripts/playtwitch/src/ig_util.zig b/scripts/playtwitch/src/ig_util.zig deleted file mode 100644 index 161afc6..0000000 --- a/scripts/playtwitch/src/ig_util.zig +++ /dev/null @@ -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); -} diff --git a/scripts/playtwitch/src/launch.zig b/scripts/playtwitch/src/launch.zig deleted file mode 100644 index 99ff735..0000000 --- a/scripts/playtwitch/src/launch.zig +++ /dev/null @@ -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(""); - 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}); - }; -} diff --git a/scripts/playtwitch/src/live.zig b/scripts/playtwitch/src/live.zig deleted file mode 100644 index 0002ff8..0000000 --- a/scripts/playtwitch/src/live.zig +++ /dev/null @@ -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; - } -} diff --git a/scripts/playtwitch/src/main.zig b/scripts/playtwitch/src/main.zig deleted file mode 100644 index 0461e33..0000000 --- a/scripts/playtwitch/src/main.zig +++ /dev/null @@ -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 }); -} diff --git a/scripts/playtwitch/src/theme.zig b/scripts/playtwitch/src/theme.zig deleted file mode 100644 index ca785f6..0000000 --- a/scripts/playtwitch/src/theme.zig +++ /dev/null @@ -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; -} diff --git a/setup/commands/install-scripts.rkt b/setup/commands/install-scripts.rkt index 814e5a2..7fcf64d 100644 --- a/setup/commands/install-scripts.rkt +++ b/setup/commands/install-scripts.rkt @@ -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")