diff --git a/scripts/playtwitch/src/State.zig b/scripts/playtwitch/src/State.zig index 11e228f..3206d4e 100644 --- a/scripts/playtwitch/src/State.zig +++ b/scripts/playtwitch/src/State.zig @@ -3,6 +3,13 @@ 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, @@ -23,7 +30,7 @@ chatty: bool, chatty_alive: bool, /// an array of channels, composed of slices into `channels_file_data` -channels: ?[]ChannelEntry, +channels: ?[]Entry, /// the data of the channels configuration file channels_file_data: ?[]u8, diff --git a/scripts/playtwitch/src/config.zig b/scripts/playtwitch/src/config.zig index d084b75..a6f2cd4 100644 --- a/scripts/playtwitch/src/config.zig +++ b/scripts/playtwitch/src/config.zig @@ -26,16 +26,16 @@ pub fn configLoaderThread(state: *State) !void { defer file.close(); const channels_data = try file.readToEndAlloc(std.heap.c_allocator, std.math.maxInt(usize)); - var channels = std.ArrayList(State.ChannelEntry).init(std.heap.c_allocator); + var channels = std.ArrayList(State.Entry).init(std.heap.c_allocator); - var channels_iter = std.mem.split(u8, channels_data, "\n"); + var channels_iter = std.mem.tokenize(u8, channels_data, "\n"); while (channels_iter.next()) |line| { - var line_iter = std.mem.split(u8, 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] == '#') + if (channel_trimmed.len <= 0 or channel_trimmed[0] == '#') continue; const comment_trimmed = blk: { @@ -49,16 +49,24 @@ pub fn configLoaderThread(state: *State) !void { break :blk comment_trimmed; }; - try channels.append(.{ + // 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} channels in {d}ms", + "Loaded {d} channel items in {d}ms", .{ channels.items.len, end_time - start_time }, ); diff --git a/scripts/playtwitch/src/gui.zig b/scripts/playtwitch/src/gui.zig index 4f73db4..3872ea0 100644 --- a/scripts/playtwitch/src/gui.zig +++ b/scripts/playtwitch/src/gui.zig @@ -16,85 +16,100 @@ pub fn winContent(state: *State) !void { var start: StartType = .none; - // Chatty checkbox - _ = c.igCheckbox("Start Chatty", &state.chatty); - - // Quality input - igu.sliceText("Quality "); - c.igSameLine(0.0, 0.0); - - if (c.igInputText( - "##quality_input", - &state.quality_buf, - state.quality_buf.len, - c.ImGuiInputTextFlags_EnterReturnsTrue, - null, - null, + if (c.igBeginTable( + "##text_inputs", + 2, + 0, + .{ .x = 0.0, .y = 0.0 }, + 0.0, )) { - start = .channel_bar; - } + defer c.igEndTable(); - 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.igTableSetupColumn("##label", c.ImGuiTableColumnFlags_WidthFixed, 85.0, 0); + c.igTableSetupColumn("##input", 0, 0.0, 0); - 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); + _ = c.igTableNextRow(0, 0.0); - var btn_size: c.ImVec2 = undefined; - c.igGetItemRectSize(&btn_size); + // Quality input + _ = c.igTableSetColumnIndex(0); + igu.sliceText("Quality"); - const preset_qualities = [_][:0]const u8{ - "best", - "1080p60", - "720p60", - "480p", - "360p", - "worst", - "audio_only", - }; + _ = c.igTableSetColumnIndex(1); + if (c.igInputText( + "##quality_input", + &state.quality_buf, + state.quality_buf.len, + c.ImGuiInputTextFlags_EnterReturnsTrue, + null, + null, + )) { + start = .channel_bar; + } - quality_popup_pos.y += quality_popup_size.y; - quality_popup_size.x += btn_size.x; - quality_popup_size.y += 5 + (quality_popup_size.y * @intToFloat( - f32, - preset_qualities.len, - )); + 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.igSetNextWindowPos(quality_popup_pos, c.ImGuiCond_Always, .{ .x = 0.0, .y = 0.0 }); - c.igSetNextWindowSize(quality_popup_size, c.ImGuiCond_Always); + 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); - if (c.igBeginPopup("quality_popup", c.ImGuiWindowFlags_NoMove)) { - defer c.igEndPopup(); + var btn_size: c.ImVec2 = undefined; + c.igGetItemRectSize(&btn_size); - for (preset_qualities) |quality| { - if (c.igSelectable_Bool(quality.ptr, false, 0, .{ .x = 0.0, .y = 0.0 })) { - std.mem.set(u8, &state.quality_buf, 0); - std.mem.copy(u8, &state.quality_buf, quality); + 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 * @intToFloat( + f32, + 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 })) { + std.mem.set(u8, &state.quality_buf, 0); + std.mem.copy(u8, &state.quality_buf, quality); + } } } - } - igu.sliceText("Play Channel "); - c.igSameLine(0.0, 0.0); - 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; + _ = 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) { @@ -104,8 +119,13 @@ pub fn winContent(state: *State) !void { (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 }, @@ -133,52 +153,91 @@ pub fn winContent(state: *State) !void { _ = c.igTableSetColumnIndex(2); c.igTableHeader("Live?"); - for (state.channels.?) |ch, i| { - var ch_buf: [256]u8 = undefined; - const formatted = try std.fmt.bufPrintZ( - &ch_buf, - "{s}", - .{ch.name}, - ); - + for (state.channels.?) |entry, i| { c.igPushID_Int(@intCast(c_int, i)); defer c.igPopID(); _ = 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 }; + 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 }, + }; + const live_label = switch (ch.live) { + .loading => "Loading...", + .live => "Live", + .offline => "Offline", + }; + + 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(); + } + }, } - - _ = 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 }, - }; - const live_label = switch (ch.live) { - .loading => "Loading...", - .live => "Live", - .offline => "Offline", - }; - - 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; } } @@ -227,7 +286,7 @@ pub fn winContent(state: *State) !void { }, .channels_idx => |idx| { c.glfwHideWindow(state.win); - try launch.launchChildren(state, state.channels.?[idx].name); + try launch.launchChildren(state, state.channels.?[idx].channel.name); }, } } diff --git a/scripts/playtwitch/src/live.zig b/scripts/playtwitch/src/live.zig index 3be19f1..ecac552 100644 --- a/scripts/playtwitch/src/live.zig +++ b/scripts/playtwitch/src/live.zig @@ -9,7 +9,10 @@ pub fn reloadLiveThread(s: *State) !void { defer s.mutex.unlock(); for (s.channels.?) |*chan| { - chan.live = .loading; + switch (chan.*) { + .channel => |*ch| ch.live = .loading, + else => {}, + } } } @@ -45,7 +48,9 @@ pub fn fetchChannelsLive(s: *State) !void { // 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.?) |*chan| { + for (s.channels.?) |*entry| { + const chan = if (entry.* == .channel) &entry.channel else continue; + page_buf.clearRetainingCapacity(); log.info("requesting live state for channel {s}", .{chan.name});