1
0
mirror of https://github.com/bobwen-dev/hunter synced 2025-04-12 00:55:41 +02:00
hunter/src/mediaview.rs

566 lines
17 KiB
Rust

use lazy_static;
use termion::event::Key;
use failure::{self, Fail};
use crate::widget::{Widget, WidgetCore};
use crate::coordinates::Coordinates;
use crate::async_value::Stale;
use crate::fail::{HResult, HError, ErrorLog};
use crate::imgview::ImgView;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, RwLock,
mpsc::{channel, Sender}};
use std::io::{BufRead, BufReader, Write};
use std::process::Child;
#[derive(Fail, Debug, Clone)]
pub enum MediaError {
#[fail(display = "{}", _0)]
NoPreviewer(String)
}
impl From<MediaError> for HError {
fn from(e: MediaError) -> HError {
HError::Media(e)
}
}
impl std::cmp::PartialEq for MediaView {
fn eq(&self, other: &Self) -> bool {
self.core == other.core
}
}
lazy_static! {
static ref MUTE: Arc<RwLock<bool>> = Arc::new(RwLock::new(false));
static ref AUTOPLAY: Arc<RwLock<bool>> = Arc::new(RwLock::new(true));
}
pub struct MediaView {
core: WidgetCore,
imgview: Arc<Mutex<ImgView>>,
file: PathBuf,
controller: Sender<String>,
paused: bool,
media_type: MediaType,
height: Arc<Mutex<usize>>,
position: Arc<Mutex<usize>>,
duration: Arc<Mutex<usize>>,
stale: Stale,
process: Arc<Mutex<Option<Child>>>,
preview_runner: Option<Box<dyn FnOnce(bool,
bool,
Arc<Mutex<usize>>,
Arc<Mutex<usize>>,
Arc<Mutex<usize>>)
-> HResult<()> + Send + 'static>>
}
#[derive(Clone,Debug)]
pub enum MediaType {
Video,
Audio
}
impl MediaType {
pub fn to_str(&self) -> &str {
match self {
MediaType::Video => "video",
MediaType::Audio => "audio"
}
}
}
impl MediaView {
pub fn new_from_file(core: WidgetCore,
file: &Path,
media_type: MediaType) -> HResult<MediaView> {
// Check if previewer is present, or bail out to show message
let media_previewer = core.config().media_previewer;
if crate::minibuffer::find_bins(&media_previewer).is_err() {
let msg = format!("Couldn't find previewer: {}{}{}!",
crate::term::color_red(),
media_previewer,
crate::term::normal_color());
core.show_status(&msg).log();
return Err(MediaError::NoPreviewer(msg))?;
}
let imgview = ImgView {
core: core.clone(),
buffer: vec![],
file: None
};
let (tx_cmd, rx_cmd) = channel();
// Stuff that gets moved into the closure
let imgview = Arc::new(Mutex::new(imgview));
let thread_imgview = imgview.clone();
let path = file.to_string_lossy().to_string();
let sender = core.get_sender();
let stale = Stale::new();
let tstale = stale.clone();
let rx_cmd = Arc::new(Mutex::new(rx_cmd));
let process = Arc::new(Mutex::new(None));
let cprocess = process.clone();
let ctype = media_type.clone();
let ccore = core.clone();
let media_previewer = core.config().media_previewer;
let g_mode = core.config().graphics;
let run_preview = Box::new(move | auto,
mute,
height: Arc<Mutex<usize>>,
position: Arc<Mutex<usize>>,
duration: Arc<Mutex<usize>>| -> HResult<()> {
loop {
if tstale.is_stale()? {
return Ok(());
}
// Use current size. Widget could have been resized at some point
let (xsize, ysize, xpix, ypix) =
{
let view = thread_imgview.lock()?;
let (xsize, ysize) = view.core.coordinates.size_u();
let (xpix, ypix) = view.core.coordinates.size_pixels()?;
(xsize, ysize, xpix, ypix)
};
let cell_ratio = crate::term::cell_ratio()?;
let mut previewer = std::process::Command::new(&media_previewer)
.arg(format!("{}", (xsize+1)))
// Leave space for position/seek bar
.arg(format!("{}", (ysize-1)))
.arg(format!("{}", xpix))
.arg(format!("{}", ypix))
.arg(format!("{}", cell_ratio))
.arg(format!("{}", ctype.to_str()))
.arg(format!("{}", auto))
.arg(format!("{}", mute))
.arg(format!("{}", g_mode))
.arg(&path)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.spawn()
.map_err(|e| {
let msg = format!("Couldn't run {}{}{}! Error: {:?}",
crate::term::color_red(),
media_previewer,
crate::term::normal_color(),
&e.kind());
ccore.show_status(&msg).log();
MediaError::NoPreviewer(msg)
})?;
let mut stdout = BufReader::new(previewer.stdout.take()?);
let mut stdin = previewer.stdin.take()?;
cprocess.lock().map(|mut p| *p = Some(previewer))?;
let mut frame = vec![];
let newline = String::from("\n");
let mut line_buf = String::new();
let rx_cmd = rx_cmd.clone();
std::thread::spawn(move || -> HResult<()> {
for cmd in rx_cmd.lock()?.iter() {
write!(stdin, "{}", cmd)?;
write!(stdin, "\n")?;
stdin.flush()?;
}
Ok(())
});
loop {
// Check if preview-gen finished and break out of loop to restart
if let Ok(Some(code)) = cprocess.lock()?
.as_mut()?
.try_wait() {
if code.success() {
break;
} else {
let msg = String::from("hunter-media failed!");
return Err(failure::format_err!("{}", msg))?;
}
}
stdout.read_line(&mut line_buf)?;
// Newline means frame is complete
if line_buf == newline {
let new_height;
line_buf.clear();
stdout.read_line(&mut line_buf)?;
let h = line_buf.trim().parse::<usize>()?;
let mut height = height.lock().unwrap();
if *height != h {
new_height = true;
} else {
new_height = false;
}
*height = h;
line_buf.clear();
stdout.read_line(&mut line_buf)?;
let pos = &line_buf.trim();
*position.lock().unwrap() = pos
.parse::<usize>()?;
line_buf.clear();
stdout.read_line(&mut line_buf)?;
let dur = &line_buf.trim();
*duration.lock().unwrap() = dur
.parse::<usize>()?;
if let Ok(mut imgview) = thread_imgview.lock() {
if new_height {
imgview.core.clear()?;
}
imgview.set_image_data(frame);
sender.send(crate::widget::Events::WidgetReady)
.map_err(|e| HError::from(e))
.log();;
}
line_buf.clear();
frame = vec![];
continue;
} else {
frame.push(line_buf);
line_buf = String::new();
}
}
}
});
Ok(MediaView {
core: core.clone(),
imgview: imgview,
file: file.to_path_buf(),
media_type: media_type,
controller: tx_cmd,
paused: false,
height: Arc::new(Mutex::new(0)),
position: Arc::new(Mutex::new(0)),
duration: Arc::new(Mutex::new(0)),
stale: stale,
process: process,
preview_runner: Some(run_preview)
})
}
pub fn start_video(&mut self) -> HResult<()> {
let runner = self.preview_runner.take();
if runner.is_some() {
let stale = self.stale.clone();
let autoplay = self.autoplay();
let mute = self.mute();
let height = self.height.clone();
let position = self.position.clone();
let duration = self.duration.clone();
let clear = self.get_core()?.get_clearlist()?;
std::thread::spawn(move || -> HResult<()> {
// Sleep a bit to avoid overloading the system when scrolling
let sleeptime = std::time::Duration::from_millis(50);
std::thread::sleep(sleeptime);
if !stale.is_stale()? {
print!("{}", clear);
runner.map(|runner| runner(autoplay,
mute,
height,
position,
duration).log());
}
Ok(())
});
}
Ok(())
}
pub fn play(&self) -> HResult<()> {
Ok(self.controller.send(String::from("p"))?)
}
pub fn pause(&self) -> HResult<()> {
Ok(self.controller.send(String::from ("a"))?)
}
pub fn progress_bar(&self) -> HResult<String> {
let xsize = self.core.coordinates.xsize_u();
let position = self.position.lock()?.clone();
let duration = self.duration.lock()?.clone();
if duration == 0 || position == 0 {
Ok(format!("{:elements$}", "|", elements=xsize))
} else {
let element_percent = 100 as f32 / xsize as f32;
let progress_percent = position as f32 / duration as f32 * 100 as f32;
let element_count = progress_percent as f32 / element_percent as f32;
Ok(format!("{:|>elements$}|{: >empty$}",
"",
"",
empty=xsize - (element_count as usize),
elements=element_count as usize))
}
}
pub fn progress_string(&self) -> HResult<String> {
let position = self.position.lock()?.clone();
let duration = self.duration.lock()?.clone();
let fposition = self.format_secs(position);
let fduration = self.format_secs(duration);
Ok(format!("{} / {}", fposition, fduration))
}
pub fn get_icons(&self, lines: usize) -> HResult<String> {
let (xpos, ypos) = self.core.coordinates.position_u();
let (xsize, _) = self.core.coordinates.size_u();
let mute_char = "🔇";
let pause_char = "";
let play_char = "";
let mut icons = String::new();
if *MUTE.read()? == true {
icons += &crate::term::goto_xy_u(xpos+xsize-2, ypos+lines);
icons += mute_char;
} else {
// Clear the mute symbol, or it doesn't go away
icons += &crate::term::goto_xy_u(xpos+xsize-2, ypos+lines);
icons += " ";
}
if *AUTOPLAY.read()? == true {
icons += &crate::term::goto_xy_u(xpos+xsize-4, ypos+lines);
icons += play_char;
} else {
icons += &crate::term::goto_xy_u(xpos+xsize-4, ypos+lines);
icons += pause_char;
}
Ok(icons)
}
pub fn format_secs(&self, secs: usize) -> String {
let hours = if secs >= 60*60 { (secs / 60) / 60 } else { 0 };
let mins = if secs >= 60 { (secs / 60) %60 } else { 0 };
format!("{:02}:{:02}:{:02}", hours, mins, secs % 60)
}
pub fn toggle_pause(&mut self) -> HResult<()> {
let auto = AUTOPLAY.read()?.clone();
let pos = self.position.lock()?.clone();
// This combination means only first frame was shown, since
// self.paused will be false, even with autoplay off
if pos == 0 && auto == false && self.paused == false {
self.toggle_autoplay();
self.start_video()?;
self.paused = false;
self.play()?;
return Ok(())
}
if self.paused {
self.toggle_autoplay();
self.play()?;
self.paused = false;
} else {
self.pause()?;
self.toggle_autoplay();
self.paused = true;
}
Ok(())
}
pub fn quit(&self) -> HResult<()> {
Ok(self.controller.send(String::from("q"))?)
}
pub fn seek_forward(&self) -> HResult<()> {
Ok(self.controller.send(String::from(">"))?)
}
pub fn seek_backward(&self) -> HResult<()> {
Ok(self.controller.send(String::from("<"))?)
}
pub fn autoplay(&self) -> bool {
if let Ok(autoplay) = AUTOPLAY.read() {
return *autoplay;
}
return true;
}
pub fn mute(&self) -> bool {
if let Ok(mute) = MUTE.read() {
return *mute;
}
return false;
}
pub fn toggle_autoplay(&self) {
if let Ok(mut autoplay) = AUTOPLAY.write() {
*autoplay = !*autoplay;
}
}
pub fn toggle_mute(&self) {
if let Ok(mut mute) = MUTE.write() {
*mute = !*mute;
if *mute {
self.controller.send(String::from("m")).ok();
} else {
self.controller.send(String::from("u")).ok();
}
}
}
pub fn kill(&mut self) -> HResult<()> {
let proc = self.process.clone();
std::thread::spawn(move || -> HResult<()> {
proc.lock()?
.as_mut()
.map(|p| {
p.kill().map_err(|e| HError::from(e)).log();
p.wait().map_err(|e| HError::from(e)).log();
});
Ok(())
});
Ok(())
}
}
impl Widget for MediaView {
fn get_core(&self) -> HResult<&WidgetCore> {
Ok(&self.core)
}
fn get_core_mut(&mut self) -> HResult<&mut WidgetCore> {
Ok(&mut self.core)
}
fn set_coordinates(&mut self, coordinates: &Coordinates) -> HResult<()> {
if &self.core.coordinates == coordinates { return Ok(()); }
self.core.coordinates = coordinates.clone();
let mut imgview = self.imgview.lock()?;
imgview.set_image_data(vec![]);
imgview.set_coordinates(&coordinates)?;
let (xsize, ysize) = self.core.coordinates.size_u();
let (xpix, ypix) = self.core.coordinates.size_pixels()?;
let cell_ratio = crate::term::cell_ratio()?;
let xystring = format!("xy\n{}\n{}\n{}\n{}\n{}\n",
xsize+1,
ysize-1,
xpix,
ypix,
cell_ratio);
self.controller.send(xystring)?;
Ok(())
}
fn refresh(&mut self) -> HResult<()> {
self.start_video().log();
Ok(())
}
fn get_drawlist(&self) -> HResult<String> {
let (xpos, ypos) = self.core.coordinates.position_u();
let height = *self.height.lock()?;
let progress_str = self.progress_string()?;
let progress_bar = self.progress_bar()?;
let frame= self.imgview
.lock()
.map(|img| img.get_drawlist())?;
let mut frame = frame?;
frame += &crate::term::goto_xy_u(xpos, ypos+height);
frame += &progress_str;
frame += &self.get_icons(height)?;
frame += &crate::term::goto_xy_u(xpos, ypos+height+1);
frame += &progress_bar;
Ok(frame)
}
fn on_key(&mut self, key: Key) -> HResult<()> {
self.do_key(key)
}
}
impl Drop for MediaView {
fn drop(&mut self) {
self.stale.set_stale().ok();
self.kill().log();
self.core.clear().log();
}
}
use crate::keybind::*;
impl Acting for MediaView {
type Action = MediaAction;
fn search_in(&self) -> Bindings<Self::Action> {
self.core.config().keybinds.media
}
fn do_action(&mut self, action: &MediaAction) -> HResult<()> {
use MediaAction::*;
match action {
SeekForward => self.seek_forward()?,
SeekBackward => self.seek_backward()?,
TogglePause => self.toggle_pause()?,
ToggleMute => self.toggle_mute(),
}
Ok(())
}
}