c
This commit is contained in:
30
pixelpwnr.json
Normal file
30
pixelpwnr.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"host": "127.0.0.1:1337",
|
||||
"threads": null,
|
||||
"binary": true,
|
||||
"flush": false,
|
||||
"entries": [
|
||||
{
|
||||
"name": "Bad Apple",
|
||||
"source": "/home/ditin2/Projects/pixelpwnr/video/badapple/*.png",
|
||||
"fps": 30,
|
||||
"width": 150,
|
||||
"height": 150,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"threads": 8,
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "Bad Apple but bigger",
|
||||
"source": "/home/ditin2/Projects/pixelpwnr/video/badapple/*.png",
|
||||
"fps": 30,
|
||||
"width": 256,
|
||||
"height": 256,
|
||||
"x": 200,
|
||||
"y": 200,
|
||||
"threads": 8,
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
51
src/config.rs
Normal file
51
src/config.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProjectConfig {
|
||||
pub host: String,
|
||||
pub threads: Option<usize>,
|
||||
pub binary: bool,
|
||||
pub flush: bool,
|
||||
pub entries: Vec<VideoEntry>,
|
||||
}
|
||||
|
||||
impl Default for ProjectConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
host: "127.0.0.1:1234".to_string(),
|
||||
threads: None,
|
||||
binary: false,
|
||||
flush: true,
|
||||
entries: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VideoEntry {
|
||||
pub name: String,
|
||||
pub source: String,
|
||||
pub fps: u32,
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
pub x: u16,
|
||||
pub y: u16,
|
||||
pub threads: Option<usize>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
impl Default for VideoEntry {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: "Video".to_string(),
|
||||
source: String::new(),
|
||||
fps: 30,
|
||||
width: 64,
|
||||
height: 64,
|
||||
x: 0,
|
||||
y: 0,
|
||||
threads: None,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
461
src/gui.rs
Normal file
461
src/gui.rs
Normal file
@@ -0,0 +1,461 @@
|
||||
mod config;
|
||||
|
||||
use config::{ProjectConfig, VideoEntry};
|
||||
use eframe::{egui, App, Frame, NativeOptions};
|
||||
use glob::glob;
|
||||
use serde::Deserialize;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Child, Command};
|
||||
|
||||
struct GuiApp {
|
||||
config_path: String,
|
||||
status_path: String,
|
||||
config: ProjectConfig,
|
||||
child: Option<Child>,
|
||||
status: String,
|
||||
frame_stats: Vec<Option<FrameStat>>,
|
||||
frame_totals: Vec<Option<usize>>,
|
||||
last_sources: Vec<String>,
|
||||
}
|
||||
|
||||
impl GuiApp {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
config_path: "pixelpwnr.json".to_string(),
|
||||
status_path: "pixelpwnr.json.status.json".to_string(),
|
||||
config: ProjectConfig::default(),
|
||||
child: None,
|
||||
status: String::new(),
|
||||
frame_stats: Vec::new(),
|
||||
frame_totals: Vec::new(),
|
||||
last_sources: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_running(&self) -> bool {
|
||||
self.child.is_some()
|
||||
}
|
||||
|
||||
fn refresh_children(&mut self) {
|
||||
if let Some(process) = &mut self.child {
|
||||
match process.try_wait() {
|
||||
Ok(Some(_)) => {
|
||||
self.child = None;
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(err) => {
|
||||
self.status = format!("Process status error: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn load_config(&mut self) {
|
||||
match fs::read_to_string(&self.config_path) {
|
||||
Ok(data) => match serde_json::from_str::<ProjectConfig>(&data) {
|
||||
Ok(config) => {
|
||||
self.config = config;
|
||||
self.status = "Config loaded.".to_string();
|
||||
}
|
||||
Err(err) => {
|
||||
self.status = format!("Failed to parse config: {err}");
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
self.status = format!("Failed to read config: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn save_config(&mut self) {
|
||||
match serde_json::to_string_pretty(&self.config) {
|
||||
Ok(data) => match fs::write(&self.config_path, data) {
|
||||
Ok(()) => self.status = "Config saved.".to_string(),
|
||||
Err(err) => self.status = format!("Failed to write config: {err}"),
|
||||
},
|
||||
Err(err) => {
|
||||
self.status = format!("Failed to serialize config: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stop_all(&mut self) {
|
||||
if let Some(mut process) = self.child.take() {
|
||||
let mut interrupted = send_interrupt(&process);
|
||||
if interrupted {
|
||||
let start = std::time::Instant::now();
|
||||
loop {
|
||||
match process.try_wait() {
|
||||
Ok(Some(_)) => {
|
||||
interrupted = false;
|
||||
break;
|
||||
}
|
||||
Ok(None) => {
|
||||
if start.elapsed().as_millis() > 1500 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
}
|
||||
}
|
||||
|
||||
if interrupted {
|
||||
let _ = process.kill();
|
||||
}
|
||||
let _ = process.wait();
|
||||
}
|
||||
self.frame_stats.clear();
|
||||
self.frame_totals.clear();
|
||||
self.last_sources.clear();
|
||||
let _ = fs::remove_file(&self.status_path);
|
||||
self.status = "Stopped all clients.".to_string();
|
||||
}
|
||||
|
||||
fn run_all(&mut self) {
|
||||
if self.config.host.trim().is_empty() {
|
||||
self.status = "Host is required.".to_string();
|
||||
return;
|
||||
}
|
||||
|
||||
if self.config.entries.iter().all(|entry| !entry.enabled) {
|
||||
self.status = "Enable at least one video entry.".to_string();
|
||||
return;
|
||||
}
|
||||
|
||||
self.stop_all();
|
||||
let exe = match resolve_pixelpwnr_binary() {
|
||||
Some(path) => path,
|
||||
None => {
|
||||
self.status = "pixelpwnr binary not found. Build it first.".to_string();
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut command = Command::new(&exe);
|
||||
command.arg("--config").arg(&self.config_path);
|
||||
command.arg("--status").arg(&self.status_path);
|
||||
if !self.config.host.trim().is_empty() {
|
||||
command.arg(&self.config.host);
|
||||
}
|
||||
|
||||
match command.spawn() {
|
||||
Ok(child) => {
|
||||
self.child = Some(child);
|
||||
self.status = "Started client.".to_string();
|
||||
}
|
||||
Err(err) => {
|
||||
self.status = format!("Failed to start client: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct StatusFile {
|
||||
entries: Vec<StatusEntry>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct StatusEntry {
|
||||
fps: f64,
|
||||
current: usize,
|
||||
total: usize,
|
||||
}
|
||||
|
||||
impl App for GuiApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut Frame) {
|
||||
self.refresh_children();
|
||||
self.refresh_status();
|
||||
ctx.request_repaint_after(std::time::Duration::from_millis(200));
|
||||
|
||||
egui::TopBottomPanel::top("top").show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.heading("pixelpwnr");
|
||||
ui.separator();
|
||||
if ui.button("Run").clicked() {
|
||||
self.run_all();
|
||||
}
|
||||
if ui.button("Stop").clicked() {
|
||||
self.stop_all();
|
||||
}
|
||||
if self.is_running() {
|
||||
ui.label("running");
|
||||
} else {
|
||||
ui.label("stopped");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Config:");
|
||||
if ui.text_edit_singleline(&mut self.config_path).changed() {
|
||||
self.status_path = format!("{}.status.json", self.config_path);
|
||||
}
|
||||
if ui.button("Load").clicked() {
|
||||
self.load_config();
|
||||
}
|
||||
if ui.button("Save").clicked() {
|
||||
self.save_config();
|
||||
}
|
||||
});
|
||||
|
||||
if !self.status.is_empty() {
|
||||
ui.label(&self.status);
|
||||
}
|
||||
|
||||
ui.separator();
|
||||
ui.heading("Global");
|
||||
egui::Grid::new("global_settings")
|
||||
.num_columns(2)
|
||||
.striped(true)
|
||||
.show(ui, |ui| {
|
||||
ui.label("Host");
|
||||
ui.text_edit_singleline(&mut self.config.host);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Threads");
|
||||
let mut threads_str = self
|
||||
.config
|
||||
.threads
|
||||
.map(|threads| threads.to_string())
|
||||
.unwrap_or_default();
|
||||
if ui.text_edit_singleline(&mut threads_str).changed() {
|
||||
self.config.threads = threads_str.trim().parse().ok();
|
||||
}
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Binary");
|
||||
ui.checkbox(&mut self.config.binary, "Use PB protocol");
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Flush");
|
||||
ui.checkbox(&mut self.config.flush, "Flush per pixel");
|
||||
ui.end_row();
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
ui.heading("Videos");
|
||||
|
||||
let mut remove_index = None;
|
||||
egui::ScrollArea::vertical()
|
||||
.auto_shrink([false; 2])
|
||||
.show(ui, |ui| {
|
||||
let sources: Vec<String> = self
|
||||
.config
|
||||
.entries
|
||||
.iter()
|
||||
.map(|entry| entry.source.clone())
|
||||
.collect();
|
||||
self.refresh_frame_totals(&sources);
|
||||
for (index, entry) in self.config.entries.iter_mut().enumerate() {
|
||||
ui.group(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.checkbox(&mut entry.enabled, "Enabled");
|
||||
ui.label(&entry.name);
|
||||
if let Some(stat) =
|
||||
self.frame_stats.get(index).and_then(|stat| stat.clone())
|
||||
{
|
||||
ui.label(format!(
|
||||
"{:.1} fps ({}/{})",
|
||||
stat.fps, stat.current, stat.total
|
||||
));
|
||||
} else if let Some(total) =
|
||||
self.frame_totals.get(index).and_then(|total| *total)
|
||||
{
|
||||
ui.label(format!("-- fps (--/{total})"));
|
||||
} else {
|
||||
ui.label("-- fps (--/--)");
|
||||
}
|
||||
if ui.button("Remove").clicked() {
|
||||
remove_index = Some(index);
|
||||
}
|
||||
});
|
||||
|
||||
egui::Grid::new(format!("entry_{index}"))
|
||||
.num_columns(2)
|
||||
.striped(true)
|
||||
.show(ui, |ui| {
|
||||
ui.label("Name");
|
||||
ui.text_edit_singleline(&mut entry.name);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Frames folder");
|
||||
ui.text_edit_singleline(&mut entry.source);
|
||||
ui.end_row();
|
||||
|
||||
ui.label("FPS");
|
||||
ui.add(egui::DragValue::new(&mut entry.fps).clamp_range(1..=240));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Width");
|
||||
ui.add(egui::DragValue::new(&mut entry.width).clamp_range(1..=4096));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Height");
|
||||
ui.add(egui::DragValue::new(&mut entry.height).clamp_range(1..=4096));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("X");
|
||||
ui.add(egui::DragValue::new(&mut entry.x).clamp_range(0..=65535));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Y");
|
||||
ui.add(egui::DragValue::new(&mut entry.y).clamp_range(0..=65535));
|
||||
ui.end_row();
|
||||
|
||||
ui.label("Threads");
|
||||
let mut threads_str = entry
|
||||
.threads
|
||||
.map(|threads| threads.to_string())
|
||||
.unwrap_or_default();
|
||||
if ui.text_edit_singleline(&mut threads_str).changed() {
|
||||
entry.threads = threads_str.trim().parse().ok();
|
||||
}
|
||||
ui.end_row();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(index) = remove_index {
|
||||
self.config.entries.remove(index);
|
||||
self.status = "Entry removed.".to_string();
|
||||
}
|
||||
|
||||
if ui.button("Add video").clicked() {
|
||||
self.config.entries.push(VideoEntry::default());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl GuiApp {
|
||||
fn refresh_status(&mut self) {
|
||||
let data = match fs::read_to_string(&self.status_path) {
|
||||
Ok(data) => data,
|
||||
Err(_) => return,
|
||||
};
|
||||
let status: StatusFile = match serde_json::from_str(&data) {
|
||||
Ok(status) => status,
|
||||
Err(_) => return,
|
||||
};
|
||||
self.frame_stats = status
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(|entry| {
|
||||
Some(FrameStat {
|
||||
fps: entry.fps,
|
||||
current: entry.current,
|
||||
total: entry.total,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
fn refresh_frame_totals(&mut self, sources: &[String]) {
|
||||
self.last_sources.resize(sources.len(), String::new());
|
||||
self.frame_totals.resize(sources.len(), None);
|
||||
self.last_sources.truncate(sources.len());
|
||||
self.frame_totals.truncate(sources.len());
|
||||
|
||||
for (index, source) in sources.iter().enumerate() {
|
||||
if self.last_sources[index] == *source {
|
||||
continue;
|
||||
}
|
||||
self.last_sources[index] = source.clone();
|
||||
self.frame_totals[index] = count_frames(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct FrameStat {
|
||||
fps: f64,
|
||||
current: usize,
|
||||
total: usize,
|
||||
}
|
||||
|
||||
fn count_frames(source: &str) -> Option<usize> {
|
||||
if has_glob_pattern(source) {
|
||||
let count = glob(source)
|
||||
.ok()?
|
||||
.filter_map(Result::ok)
|
||||
.filter(|path| path.is_file())
|
||||
.count();
|
||||
return Some(count);
|
||||
}
|
||||
|
||||
let path = Path::new(source);
|
||||
if path.is_dir() {
|
||||
let count = fs::read_dir(path)
|
||||
.ok()?
|
||||
.filter_map(Result::ok)
|
||||
.filter(|entry| entry.path().is_file())
|
||||
.count();
|
||||
return Some(count);
|
||||
}
|
||||
if path.is_file() {
|
||||
return Some(1);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn has_glob_pattern(path: &str) -> bool {
|
||||
path.contains('*') || path.contains('?') || path.contains('[')
|
||||
}
|
||||
|
||||
fn resolve_pixelpwnr_binary() -> Option<PathBuf> {
|
||||
let filename = if cfg!(windows) {
|
||||
"pixelpwnr.exe"
|
||||
} else {
|
||||
"pixelpwnr"
|
||||
};
|
||||
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(parent) = exe.parent() {
|
||||
let sibling = parent.join(filename);
|
||||
if sibling.exists() {
|
||||
return Some(sibling);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(cwd) = std::env::current_dir() {
|
||||
let debug = cwd.join("target").join("debug").join(filename);
|
||||
if debug.exists() {
|
||||
return Some(debug);
|
||||
}
|
||||
let release = cwd.join("target").join("release").join(filename);
|
||||
if release.exists() {
|
||||
return Some(release);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn send_interrupt(child: &Child) -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let pid = child.id() as i32;
|
||||
unsafe { libc::kill(pid, libc::SIGINT) == 0 }
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let _ = child;
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let options = NativeOptions::default();
|
||||
let _ = eframe::run_native(
|
||||
"pixelpwnr",
|
||||
options,
|
||||
Box::new(|_cc| Box::new(GuiApp::new())),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user