rewrite playtwitch in zirewrite playtwitch in zigg

This commit is contained in:
LordMZTE 2022-05-17 17:08:53 +02:00
parent 56a7af267e
commit 6c0a78bbb4
Signed by: LordMZTE
GPG key ID: B64802DC33A64FF6
9 changed files with 314 additions and 225 deletions

View file

@ -11,7 +11,7 @@ yaml-language-server
install-scripts target=(`echo $HOME` + "/.local/bin"): build-scripts install-scripts target=(`echo $HOME` + "/.local/bin"): build-scripts
cp scripts/randomwallpaper/target/release/randomwallpaper {{target}}/randomwallpaper cp scripts/randomwallpaper/target/release/randomwallpaper {{target}}/randomwallpaper
cp scripts/playtwitch/target/release/playtwitch {{target}}/playtwitch cp scripts/playtwitch/zig-out/bin/playtwitch {{target}}/playtwitch
ln -sf \ ln -sf \
`pwd`/scripts/{start-joshuto,withjava} \ `pwd`/scripts/{start-joshuto,withjava} \
@ -20,7 +20,7 @@ install-scripts target=(`echo $HOME` + "/.local/bin"): build-scripts
build-scripts: build-scripts:
cargo build --release --manifest-path scripts/randomwallpaper/Cargo.toml cargo build --release --manifest-path scripts/randomwallpaper/Cargo.toml
cargo build --release --manifest-path scripts/playtwitch/Cargo.toml cd scripts/playtwitch && zig build -Drelease-fast
install-lsps-paru: install-lsps-paru:
#!/bin/sh #!/bin/sh

View file

@ -1,2 +1,2 @@
target/ zig-cache/
Cargo.lock zig-out/

View file

@ -1,13 +0,0 @@
[package]
name = "playtwitch"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.56"
clap = { version = "3.1.6", features = ["derive"] }
dirs = "4.0.0"
gtk4 = "0.4.7"

View file

@ -0,0 +1,38 @@
const std = @import("std");
pub fn build(b: *std.build.Builder) void {
// Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options
// for restricting supported target set are available.
const target = b.standardTargetOptions(.{});
// Standard release options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
const mode = b.standardReleaseOptions();
const exe = b.addExecutable("playtwitch", "src/main.zig");
exe.setTarget(target);
exe.setBuildMode(mode);
exe.linkLibC();
exe.linkSystemLibrary("gtk4");
exe.install();
const run_cmd = exe.run();
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);
const exe_tests = b.addTest("src/main.zig");
exe_tests.setTarget(target);
exe_tests.setBuildMode(mode);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&exe_tests.step);
}

View file

@ -0,0 +1,16 @@
// partially yoinked from https://github.com/Swoogan/ziggtk
pub const c = @cImport({
@cInclude("gtk/gtk.h");
});
/// Could not get `g_signal_connect` to work. Zig says "use of undeclared identifier". Reimplemented here
pub fn connectSignal(
instance: c.gpointer,
detailed_signal: [*c]const c.gchar,
c_handler: c.GCallback,
data: c.gpointer,
) void {
var zero: u32 = 0;
const flags: *c.GConnectFlags = @ptrCast(*c.GConnectFlags, &zero);
_ = c.g_signal_connect_data(instance, detailed_signal, c_handler, data, null, flags.*);
}

View file

@ -1,130 +0,0 @@
use std::{cell::Cell, rc::Rc, thread::JoinHandle};
use gtk4::prelude::*;
use crate::start_streamlink;
#[derive(Clone)]
pub struct GuiInitData {
pub quality: String,
pub chatty: bool,
pub channels: Vec<String>,
}
pub fn run_gui(init: GuiInitData) {
let streamlink_handle = Rc::new(Cell::new(None));
let app = gtk4::Application::new(Some("de.mzte.playtwitch"), Default::default());
let streamlink_handle_ = streamlink_handle.clone();
app.connect_activate(move |app| build_ui(app, &init, streamlink_handle_.clone()));
app.run();
if let Some(handle) = streamlink_handle.take() {
handle.join().unwrap();
}
}
fn build_ui(
app: &gtk4::Application,
init: &GuiInitData,
streamlink_handle_out: Rc<Cell<Option<JoinHandle<()>>>>,
) {
let win = gtk4::ApplicationWindow::builder()
.application(app)
.title("Pick a stream!")
.build();
let vbox = gtk4::Box::new(gtk4::Orientation::Vertical, 5);
win.set_child(Some(&vbox));
let quality_box = gtk4::Box::new(gtk4::Orientation::Horizontal, 5);
let chatty_box = gtk4::Box::new(gtk4::Orientation::Horizontal, 5);
let titlebar = gtk4::HeaderBar::new();
titlebar.pack_start(&quality_box);
titlebar.pack_end(&chatty_box);
win.set_titlebar(Some(&titlebar));
quality_box.append(&gtk4::Label::new(Some("Quality")));
let quality_entry = gtk4::Entry::new();
quality_box.append(&quality_entry);
quality_entry.set_hexpand(true);
quality_entry.set_text(&init.quality);
let chatty_switch = gtk4::Switch::builder()
.state(init.chatty)
.tooltip_text("Start Chatty with the given channel")
.build();
chatty_box.append(&chatty_switch);
chatty_box.append(&gtk4::Label::new(Some("Start Chatty")));
let other_channel = gtk4::Entry::builder()
.placeholder_text("Other Channel...")
.hexpand(true)
.build();
vbox.append(&other_channel);
// focus other channel initially
vbox.set_focus_child(Some(&other_channel));
let win_ = win.clone();
let quality_entry_ = quality_entry.clone();
let streamlink_handle_out_ = streamlink_handle_out.clone();
let chatty_switch_ = chatty_switch.clone();
other_channel.connect_activate(move |this| {
let channel = this.text().to_string();
let quality = quality_entry_.text().to_string();
let chatty = chatty_switch_.state();
streamlink_handle_out_.set(Some(std::thread::spawn(move || {
if let Err(e) = start_streamlink(&channel, &quality, chatty) {
eprintln!("Streamlink Error: {:?}", e);
}
})));
win_.close();
});
let list = gtk4::ListBox::new();
vbox.append(
&gtk4::Frame::builder()
.child(
&gtk4::ScrolledWindow::builder()
.child(&list)
.vexpand(true)
.vscrollbar_policy(gtk4::PolicyType::Always)
.hscrollbar_policy(gtk4::PolicyType::Automatic)
.build(),
)
.label("Quick Channels")
.build(),
);
for channel in init.channels.iter() {
let entry = gtk4::ListBoxRow::new();
entry.set_child(Some(
&gtk4::Label::builder()
.label(channel)
.halign(gtk4::Align::Start)
.build(),
));
list.append(&entry);
}
let win_ = win.clone();
list.connect_row_activated(move |_, row| {
let label = row.child().unwrap().downcast::<gtk4::Label>().unwrap();
let quality = quality_entry.text().to_string();
let channel = label.text().to_string();
let chatty = chatty_switch.state();
streamlink_handle_out.set(Some(std::thread::spawn(move || {
if let Err(e) = start_streamlink(&channel, &quality, chatty) {
eprintln!("Streamlink Error: {:?}", e);
}
})));
win_.close();
});
win.show();
}

View file

@ -0,0 +1,179 @@
const std = @import("std");
const ffi = @import("ffi.zig");
const c = ffi.c;
pub const GuiState = struct {
alloc: std.mem.Allocator,
/// An arena allocator used to store userdata for widgets of the UI
udata_arena: std.mem.Allocator,
streamlink_child: ?*std.ChildProcess = null,
chatty_child: ?*std.ChildProcess = null,
};
pub fn activate(app: *c.GtkApplication, state: *GuiState) void {
const win = c.gtk_application_window_new(app);
c.gtk_window_set_title(@ptrCast(*c.GtkWindow, win), "Pick a stream!");
const titlebar = c.gtk_header_bar_new();
c.gtk_window_set_titlebar(@ptrCast(*c.GtkWindow, win), titlebar);
const left_titlebar = c.gtk_box_new(c.GTK_ORIENTATION_HORIZONTAL, 5);
c.gtk_header_bar_pack_start(@ptrCast(*c.GtkHeaderBar, titlebar), left_titlebar);
c.gtk_box_append(@ptrCast(*c.GtkBox, left_titlebar), c.gtk_label_new("Quality"));
const quality_buffer = c.gtk_entry_buffer_new("best", -1);
const quality_entry = c.gtk_entry_new_with_buffer(quality_buffer);
c.gtk_box_append(@ptrCast(*c.GtkBox, left_titlebar), quality_entry);
const right_titlebar = c.gtk_box_new(c.GTK_ORIENTATION_HORIZONTAL, 5);
c.gtk_header_bar_pack_end(@ptrCast(*c.GtkHeaderBar, titlebar), right_titlebar);
const chatty_switch = c.gtk_switch_new();
c.gtk_box_append(@ptrCast(*c.GtkBox, right_titlebar), chatty_switch);
c.gtk_switch_set_active(@ptrCast(*c.GtkSwitch, chatty_switch), 1);
c.gtk_box_append(@ptrCast(*c.GtkBox, right_titlebar), c.gtk_label_new("Start Chatty"));
const content = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 5);
c.gtk_window_set_child(@ptrCast(*c.GtkWindow, win), content);
const other_stream_buffer = c.gtk_entry_buffer_new(null, -1);
const other_stream_entry = c.gtk_entry_new_with_buffer(other_stream_buffer);
c.gtk_box_append(@ptrCast(*c.GtkBox, content), other_stream_entry);
c.gtk_entry_set_placeholder_text(@ptrCast(*c.GtkEntry, other_stream_entry), "Other Channel...");
const other_act_data = state.udata_arena.create(OtherStreamActivateData) catch return;
other_act_data.* = OtherStreamActivateData{
.state = state,
.buf = other_stream_buffer,
.win = @ptrCast(*c.GtkWindow, win),
.chatty_switch = @ptrCast(*c.GtkSwitch, chatty_switch),
};
ffi.connectSignal(
other_stream_entry,
"activate",
@ptrCast(c.GCallback, on_other_stream_activate),
other_act_data,
);
const frame = c.gtk_frame_new("Quick Pick");
c.gtk_box_append(@ptrCast(*c.GtkBox, content), frame);
const scroll = c.gtk_scrolled_window_new();
c.gtk_frame_set_child(@ptrCast(*c.GtkFrame, frame), scroll);
c.gtk_widget_set_hexpand(scroll, 1);
c.gtk_widget_set_vexpand(scroll, 1);
c.gtk_scrolled_window_set_policy(
@ptrCast(*c.GtkScrolledWindow, scroll),
c.GTK_POLICY_AUTOMATIC,
c.GTK_POLICY_ALWAYS,
);
const list = c.gtk_list_box_new();
c.gtk_scrolled_window_set_child(@ptrCast(*c.GtkScrolledWindow, scroll), list);
const act_data = state.udata_arena.create(RowActivateData) catch return;
act_data.* = RowActivateData{
.state = state,
.win = @ptrCast(*c.GtkWindow, win),
.chatty_switch = @ptrCast(*c.GtkSwitch, chatty_switch),
};
ffi.connectSignal(list, "row-activated", @ptrCast(c.GCallback, on_row_activate), act_data);
channels: {
const channels_data = read_channels(state.alloc) catch |e| {
std.log.err("Failed to read channels: {}", .{e});
break :channels;
};
defer state.alloc.free(channels_data);
var name_buf: [64]u8 = undefined;
var channels_iter = std.mem.split(u8, channels_data, "\n");
while (channels_iter.next()) |s| {
if (s.len > 63) {
@panic("Can't have channel name >63 chars!");
}
std.mem.copy(u8, &name_buf, s);
name_buf[s.len] = 0;
const label = c.gtk_label_new(&name_buf);
c.gtk_list_box_append(@ptrCast(*c.GtkListBox, list), label);
c.gtk_widget_set_halign(label, c.GTK_ALIGN_START);
}
}
c.gtk_widget_show(win);
}
fn read_channels(alloc: std.mem.Allocator) ![]u8 {
const home = try std.os.getenv("HOME") orelse error.HomeNotSet;
const fname = try std.fmt.allocPrint(alloc, "{s}/.config/playtwitch/channels", .{home});
defer alloc.free(fname);
const file = try std.fs.cwd().openFile(fname, .{});
return try file.readToEndAlloc(alloc, 1024 * 1024 * 5);
}
const RowActivateData = struct {
state: *GuiState,
win: *c.GtkWindow,
chatty_switch: *c.GtkSwitch,
};
fn on_row_activate(list: *c.GtkListBox, row: *c.GtkListBoxRow, data: *RowActivateData) void {
_ = list;
const label = c.gtk_list_box_row_get_child(row);
const channel_name = c.gtk_label_get_text(@ptrCast(*c.GtkLabel, label));
start(
data.state,
if (c.gtk_switch_get_active(data.chatty_switch) == 0) false else true,
std.mem.sliceTo(channel_name, 0),
) catch |err| std.log.err("Failed to start children: {}", .{err});
c.gtk_window_close(data.win);
}
const OtherStreamActivateData = struct {
state: *GuiState,
buf: *c.GtkEntryBuffer,
win: *c.GtkWindow,
chatty_switch: *c.GtkSwitch,
};
fn on_other_stream_activate(entry: *c.GtkEntry, data: *OtherStreamActivateData) void {
_ = entry;
start(
data.state,
if (c.gtk_switch_get_active(data.chatty_switch) == 0) false else true,
c.gtk_entry_buffer_get_text(data.buf)[0..c.gtk_entry_buffer_get_length(data.buf)],
) catch |err| std.log.err("Failed to start children: {}", .{err});
c.gtk_window_close(data.win);
}
fn start(state: *GuiState, chatty: bool, channel: []const u8) !void {
if (channel.len == 0) {
return;
}
const url = try std.fmt.allocPrint(state.alloc, "https://twitch.tv/{s}", .{channel});
const streamlink_argv = [_][]const u8{ "streamlink", url };
const streamlink_child = try std.ChildProcess.init(&streamlink_argv, state.alloc);
try streamlink_child.spawn();
state.streamlink_child = streamlink_child;
if (chatty) {
const chatty_argv = [_][]const u8{ "chatty", "-connect", "-channel", channel };
const chatty_child = try std.ChildProcess.init(&chatty_argv, state.alloc);
try chatty_child.spawn();
state.chatty_child = chatty_child;
}
}

View file

@ -1,78 +0,0 @@
use std::{
fs::File,
io::{BufRead, BufReader},
process::Command,
};
use anyhow::Context;
use clap::Parser;
use gui::GuiInitData;
mod gui;
#[derive(Parser)]
struct Opt {
/// Name of the channel to play. If omitted, a GUI selector is opened.
channel: Option<String>,
/// Quality of the stream. See streamlink docs.
#[clap(default_value = "best")]
quality: String,
/// Start chatty with the given channel
#[clap(short, long)]
chatty: bool,
}
fn main() -> anyhow::Result<()> {
let opt = Opt::parse();
if let Some(channel) = opt.channel {
start_streamlink(&channel, &opt.quality, opt.chatty)?;
} else {
let channels_path = dirs::config_dir()
.context("Couldn't get config path")?
.join("playtwitch/channels");
let channels = BufReader::new(File::open(channels_path)?)
.lines()
.collect::<Result<Vec<_>, _>>()?;
gui::run_gui(GuiInitData {
quality: opt.quality,
chatty: opt.chatty,
channels,
});
}
Ok(())
}
fn start_streamlink(channel: &str, quality: &str, chatty: bool) -> anyhow::Result<()> {
println!(
"Starting streamlink with channel {} and quality {}",
channel, quality
);
let mut streamlink = Command::new("streamlink")
.arg(format!("https://twitch.tv/{}", channel))
.arg(quality)
.spawn()?;
let chatty = if chatty {
Some(
Command::new("chatty")
.args(["-connect", "-channel", channel])
.spawn()?,
)
} else {
None
};
streamlink.wait()?;
if let Some(mut chatty) = chatty {
chatty.wait()?;
}
Ok(())
}

View file

@ -0,0 +1,77 @@
const std = @import("std");
const ffi = @import("ffi.zig");
const c = ffi.c;
const gui = @import("gui.zig");
pub fn log(
comptime level: std.log.Level,
comptime scope: @TypeOf(.EnumLiteral),
comptime format: []const u8,
args: anytype,
) void {
const g_level = switch (level) {
.err => c.G_LOG_LEVEL_ERROR,
.warn => c.G_LOG_LEVEL_WARNING,
.info => c.G_LOG_LEVEL_INFO,
.debug => c.G_LOG_LEVEL_DEBUG,
};
const s = std.fmt.allocPrintZ(
std.heap.c_allocator,
format,
args,
) catch return;
defer std.heap.c_allocator.free(s);
var fields = [_]c.GLogField{
c.GLogField{
.key = "GLIB_DOMAIN",
.value = "playtwitch-" ++ @tagName(scope),
.length = -1,
},
c.GLogField{
.key = "MESSAGE",
.value = @ptrCast(*const anyopaque, s),
.length = -1,
},
};
c.g_log_structured_array(
g_level,
&fields,
fields.len,
);
}
pub fn main() !u8 {
var udata_arena = std.heap.ArenaAllocator.init(std.heap.c_allocator);
defer udata_arena.deinit();
var state = gui.GuiState {
.alloc = std.heap.c_allocator,
.udata_arena = udata_arena.allocator(),
};
const app = c.gtk_application_new("de.mzte.playtwitch", c.G_APPLICATION_FLAGS_NONE);
defer c.g_object_unref(app);
ffi.connectSignal(app, "activate", @ptrCast(c.GCallback, gui.activate), &state);
const status = c.g_application_run(
@ptrCast(*c.GApplication, app),
@intCast(i32, std.os.argv.len),
@ptrCast([*c][*c]u8, std.os.argv.ptr),
);
if (state.streamlink_child) |ch| {
defer ch.deinit();
_ = try ch.wait();
}
if (state.chatty_child) |ch| {
defer ch.deinit();
_ = try ch.wait();
}
return @intCast(u8, status);
}