mirror of https://github.com/bobwen-dev/hunter
modularize media preview generator into own workspace
This commit is contained in:
parent
cd01a21f68
commit
11f5bd081b
|
@ -453,9 +453,6 @@ dependencies = [
|
|||
"dirs-2 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"gstreamer 0.11.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"gstreamer-app 0.11.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"image 0.21.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lscolors 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
@ -474,6 +471,17 @@ dependencies = [
|
|||
"users 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hunter-media"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"gstreamer 0.11.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"gstreamer-app 0.11.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"image 0.21.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"termion 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.21.2"
|
||||
|
|
29
Cargo.toml
29
Cargo.toml
|
@ -1,3 +1,5 @@
|
|||
cargo-features = ["default-run"]
|
||||
|
||||
[package]
|
||||
name = "hunter"
|
||||
version = "1.2.1"
|
||||
|
@ -10,6 +12,8 @@ readme = "README.md"
|
|||
license = "WTFPL"
|
||||
keywords = ["cli", "terminal", "file"]
|
||||
categories = ["command-line-utilities"]
|
||||
default-run = "hunter"
|
||||
|
||||
|
||||
[dependencies]
|
||||
termion = "1.5.1"
|
||||
|
@ -36,29 +40,16 @@ pathbuftools = "0.1"
|
|||
clap = "2.33"
|
||||
mime = "0.3.13"
|
||||
|
||||
|
||||
|
||||
image = { version = "0.21.1", optional = true }
|
||||
gstreamer = { version = "0.11.2", optional = true }
|
||||
gstreamer-app = { version = "0.11.2", optional = true }
|
||||
|
||||
|
||||
|
||||
|
||||
[features]
|
||||
default = ["img", "video"]
|
||||
img = ["image"]
|
||||
video = ["img", "gstreamer", "gstreamer-app"]
|
||||
default = ["video"]
|
||||
video = []
|
||||
|
||||
|
||||
[[bin]]
|
||||
name = "hunter"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "preview-gen"
|
||||
path = "src/preview-gen.rs"
|
||||
required-features = ["img"]
|
||||
|
||||
[workspace]
|
||||
members = [".", "hunter-media"]
|
||||
default-members = [".", "hunter-media"]
|
||||
|
||||
|
||||
[patch.crates-io]
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
target/
|
|
@ -0,0 +1,6 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "hunter-preview"
|
||||
version = "0.1.0"
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
[package]
|
||||
name = "hunter-media"
|
||||
version = "0.1.0"
|
||||
authors = ["rabite0"]
|
||||
edition = "2018"
|
||||
description = "hunter's preview generator for image/video/audio files"
|
||||
homepage = "https://github.com/rabite0/hunter"
|
||||
repository = "https://github.com/rabite0/hunter"
|
||||
readme = "../README.md"
|
||||
license = "WTFPL"
|
||||
keywords = ["cli", "terminal", "file"]
|
||||
categories = ["command-line-utilities"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
termion = "1.5.1"
|
||||
failure = "0.1.5"
|
||||
image = "0.21.1"
|
||||
gstreamer = { version = "0.11.2", optional = true }
|
||||
gstreamer-app = { version = "0.11.2", optional = true }
|
||||
|
||||
|
||||
[features]
|
||||
default = ["video"]
|
||||
video = ["gstreamer", "gstreamer-app"]
|
|
@ -0,0 +1,587 @@
|
|||
// Based on https://github.com/jD91mZM2/termplay
|
||||
// MIT License
|
||||
|
||||
use image::{Pixel, FilterType, DynamicImage, GenericImageView};
|
||||
|
||||
use termion::color::{Bg, Fg, Rgb};
|
||||
#[cfg(feature = "video")]
|
||||
use termion::input::TermRead;
|
||||
|
||||
|
||||
#[cfg(feature = "video")]
|
||||
use gstreamer::{self, prelude::*};
|
||||
#[cfg(feature = "video")]
|
||||
use gstreamer_app;
|
||||
|
||||
use failure::Error;
|
||||
#[cfg(feature = "video")]
|
||||
use failure::format_err;
|
||||
|
||||
|
||||
use std::io::Write;
|
||||
#[cfg(feature = "video")]
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
pub type MResult<T> = Result<T, Error>;
|
||||
|
||||
fn main() -> MResult<()> {
|
||||
let args = std::env::args().collect::<Vec<String>>();
|
||||
let xsize: usize = args.get(1)
|
||||
.expect("Provide xsize")
|
||||
.parse::<usize>()
|
||||
.unwrap();
|
||||
let ysize = args.get(2)
|
||||
.expect("provide ysize")
|
||||
.parse()
|
||||
.unwrap();
|
||||
let xpos = args.get(3)
|
||||
.expect("provide xpos")
|
||||
.parse()
|
||||
.unwrap();
|
||||
let ypos = args.get(4)
|
||||
.expect("provide ypos")
|
||||
.parse()
|
||||
.unwrap();
|
||||
let preview_type = args.get(5)
|
||||
.expect("Provide preview type")
|
||||
.parse::<String>()
|
||||
.unwrap();
|
||||
let autoplay = args.get(6)
|
||||
.expect("Autoplay?")
|
||||
.parse::<bool>()
|
||||
.unwrap();
|
||||
let mute = args.get(7)
|
||||
.expect("Muted?")
|
||||
.parse::<bool>()
|
||||
.unwrap();
|
||||
let path = args.get(8).expect("Provide path");
|
||||
|
||||
|
||||
let result =
|
||||
match preview_type.as_ref() {
|
||||
#[cfg(feature = "video")]
|
||||
"video" => video_preview(path,
|
||||
xsize,
|
||||
ysize,
|
||||
0,
|
||||
0,
|
||||
autoplay,
|
||||
mute,
|
||||
false),
|
||||
|
||||
"image" => image_preview(path,
|
||||
xsize,
|
||||
ysize),
|
||||
|
||||
#[cfg(feature = "video")]
|
||||
"audio" => audio_preview(path,
|
||||
autoplay,
|
||||
mute),
|
||||
|
||||
#[cfg(feature = "video")]
|
||||
"video-raw" => video_preview(path,
|
||||
xsize,
|
||||
ysize,
|
||||
xpos,
|
||||
ypos,
|
||||
autoplay,
|
||||
mute,
|
||||
true),
|
||||
#[cfg(feature = "video")]
|
||||
_ => { panic!("Available types: video(-raw)/image/audio") }
|
||||
|
||||
#[cfg(not(feature = "video"))]
|
||||
_ => { panic!("Available type: image") }
|
||||
};
|
||||
|
||||
if result.is_err() {
|
||||
println!("{:?}", &result);
|
||||
result
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn image_preview(path: &str,
|
||||
xsize: usize,
|
||||
ysize: usize) -> MResult<()> {
|
||||
let img = image::open(&path)?;
|
||||
|
||||
let renderer = Renderer::new(xsize,
|
||||
ysize,
|
||||
0,
|
||||
0);
|
||||
|
||||
renderer.send_image(&img)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// #[cfg(feature = "video")]
|
||||
fn video_preview(path: &String,
|
||||
xsize: usize,
|
||||
ysize: usize,
|
||||
xpos: usize,
|
||||
ypos: usize,
|
||||
autoplay: bool,
|
||||
mute: bool,
|
||||
raw: bool)
|
||||
-> MResult<()> {
|
||||
|
||||
let (player, appsink) = make_gstreamer()?;
|
||||
|
||||
let uri = format!("file://{}", &path);
|
||||
|
||||
player.set_property("uri", &uri)?;
|
||||
|
||||
|
||||
let renderer = Renderer::new(xsize, ysize, xpos, ypos);
|
||||
let renderer = Arc::new(RwLock::new(renderer));
|
||||
let crenderer = renderer.clone();
|
||||
|
||||
|
||||
|
||||
|
||||
let p = player.clone();
|
||||
|
||||
appsink.set_callbacks(
|
||||
gstreamer_app::AppSinkCallbacks::new()
|
||||
.new_sample({
|
||||
move |sink| {
|
||||
let sample = match sink.pull_sample() {
|
||||
Some(sample) => sample,
|
||||
None => return gstreamer::FlowReturn::Eos,
|
||||
};
|
||||
|
||||
let position = p.query_position::<gstreamer::ClockTime>()
|
||||
.map(|p| p.seconds().unwrap_or(0))
|
||||
.unwrap_or(0);
|
||||
|
||||
let duration = p.query_duration::<gstreamer::ClockTime>()
|
||||
.map(|d| d.seconds().unwrap_or(0))
|
||||
.unwrap_or(0);
|
||||
|
||||
if let Ok(mut renderer) = crenderer.write() {
|
||||
match renderer.send_frame(&*sample,
|
||||
position,
|
||||
duration,
|
||||
raw) {
|
||||
Ok(()) => {
|
||||
if autoplay == false {
|
||||
// Just render first frame to get a static image
|
||||
match p.set_state(gstreamer::State::Paused)
|
||||
.into_result() {
|
||||
Ok(_) => gstreamer::FlowReturn::Eos,
|
||||
Err(_) => gstreamer::FlowReturn::Error
|
||||
}
|
||||
} else {
|
||||
gstreamer::FlowReturn::Ok
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("{:?}", err);
|
||||
gstreamer::FlowReturn::Error
|
||||
}
|
||||
}
|
||||
} else { gstreamer::FlowReturn::Error }
|
||||
|
||||
}
|
||||
})
|
||||
.eos({
|
||||
move |_| {
|
||||
std::process::exit(0);
|
||||
}
|
||||
})
|
||||
.build()
|
||||
);
|
||||
|
||||
if mute == true || autoplay == false {
|
||||
player.set_property("volume", &0.0)?;
|
||||
}
|
||||
player.set_state(gstreamer::State::Playing).into_result()?;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
read_keys(player, Some(renderer))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// #[cfg(feature = "video")]
|
||||
fn read_keys(player: gstreamer::Element,
|
||||
renderer: Option<Arc<RwLock<Renderer>>>) -> MResult<()> {
|
||||
let seek_time = gstreamer::ClockTime::from_seconds(5);
|
||||
|
||||
let stdin = std::io::stdin();
|
||||
let mut stdin = stdin.lock();
|
||||
|
||||
loop {
|
||||
let input = stdin
|
||||
.read_line()?
|
||||
.unwrap_or_else(|| String::from("q"));
|
||||
|
||||
|
||||
match input.as_str() {
|
||||
"q" => std::process::exit(0),
|
||||
">" => {
|
||||
if let Some(mut time) = player
|
||||
.query_position::<gstreamer::ClockTime>() {
|
||||
time += seek_time;
|
||||
|
||||
player.seek_simple(
|
||||
gstreamer::SeekFlags::FLUSH,
|
||||
gstreamer::format::GenericFormattedValue::from_time(time)
|
||||
)?;
|
||||
}
|
||||
},
|
||||
"<" => {
|
||||
if let Some(mut time) = player
|
||||
.query_position::<gstreamer::ClockTime>() {
|
||||
if time >= seek_time {
|
||||
time -= seek_time;
|
||||
} else {
|
||||
time = gstreamer::ClockTime(Some(0));
|
||||
}
|
||||
|
||||
player.seek_simple(
|
||||
gstreamer::SeekFlags::FLUSH,
|
||||
gstreamer::format::GenericFormattedValue::from_time(time)
|
||||
)?;
|
||||
}
|
||||
}
|
||||
"p" => {
|
||||
player.set_state(gstreamer::State::Playing).into_result()?;
|
||||
|
||||
// To actually start playing again
|
||||
if let Some(time) = player
|
||||
.query_position::<gstreamer::ClockTime>() {
|
||||
player.seek_simple(
|
||||
gstreamer::SeekFlags::FLUSH,
|
||||
gstreamer::format::GenericFormattedValue::from_time(time)
|
||||
)?;
|
||||
}
|
||||
}
|
||||
"a" => {
|
||||
player.set_state(gstreamer::State::Paused).into_result()?;
|
||||
}
|
||||
"m" => {
|
||||
player.set_property("volume", &0.0)?;
|
||||
}
|
||||
"u" => {
|
||||
player.set_property("volume", &1.0)?;
|
||||
}
|
||||
"xy" => {
|
||||
if let Some(ref renderer) = renderer {
|
||||
let xsize = stdin.read_line()?;
|
||||
let ysize = stdin.read_line()?;
|
||||
|
||||
let xsize = xsize.unwrap_or(String::from("0")).parse::<usize>()?;
|
||||
let ysize = ysize.unwrap_or(String::from("0")).parse::<usize>()?;
|
||||
|
||||
let mut renderer = renderer
|
||||
.write()
|
||||
.map_err(|_| format_err!("Renderer RwLock failed!"))?;
|
||||
|
||||
renderer.set_size(xsize, ysize)?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #[cfg(feature = "video")]
|
||||
pub fn audio_preview(path: &String,
|
||||
autoplay: bool,
|
||||
mute: bool)
|
||||
-> MResult<()> {
|
||||
let (player, _) = make_gstreamer()?;
|
||||
|
||||
let uri = format!("file://{}", &path);
|
||||
|
||||
player.set_property("uri", &uri)?;
|
||||
let p = player.clone();
|
||||
|
||||
// Since events don't work with audio files...
|
||||
std::thread::spawn(move || -> MResult<()> {
|
||||
let mut last_pos = None;
|
||||
let sleep_duration = std::time::Duration::from_millis(50);
|
||||
let mut stdout = std::io::stdout();
|
||||
loop {
|
||||
std::thread::sleep(sleep_duration);
|
||||
|
||||
let position = p.query_position::<gstreamer::ClockTime>()
|
||||
.map(|p| p.seconds().unwrap_or(0))
|
||||
.unwrap_or(0);
|
||||
|
||||
let duration = p.query_duration::<gstreamer::ClockTime>()
|
||||
.map(|d| d.seconds().unwrap_or(0))
|
||||
.unwrap_or(0);
|
||||
|
||||
// Just redo loop until position changes
|
||||
if last_pos == Some(position) {
|
||||
continue
|
||||
}
|
||||
|
||||
last_pos = Some(position);
|
||||
|
||||
// MediaView needs empty line as separator
|
||||
writeln!(stdout, "")?;
|
||||
// Send position and duration
|
||||
writeln!(stdout, "{}", position)?;
|
||||
writeln!(stdout, "{}", duration)?;
|
||||
stdout.flush()?;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
if mute == true || autoplay == false{
|
||||
player.set_property("volume", &0.0)?;
|
||||
} else {
|
||||
player.set_state(gstreamer::State::Playing).into_result()?;
|
||||
}
|
||||
|
||||
read_keys(player, None)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// #[cfg(feature = "video")]
|
||||
pub fn make_gstreamer() -> MResult<(gstreamer::Element,
|
||||
gstreamer_app::AppSink)> {
|
||||
gstreamer::init()?;
|
||||
|
||||
let player = gstreamer::ElementFactory::make("playbin", None)
|
||||
.ok_or(format_err!("Can't create playbin"))?;
|
||||
|
||||
let videorate = gstreamer::ElementFactory::make("videorate", None)
|
||||
.ok_or(format_err!("Can't create videorate element"))?;
|
||||
|
||||
let pnmenc = gstreamer::ElementFactory::make("pnmenc", None)
|
||||
.ok_or(format_err!("Can't create PNM-encoder"))?;
|
||||
|
||||
let sink = gstreamer::ElementFactory::make("appsink", None)
|
||||
.ok_or(format_err!("Can't create appsink"))?;
|
||||
|
||||
let appsink = sink.clone()
|
||||
.downcast::<gstreamer_app::AppSink>()
|
||||
.unwrap();
|
||||
|
||||
|
||||
videorate.set_property("max-rate", &30)?;
|
||||
|
||||
let elems = &[&videorate, &pnmenc, &sink];
|
||||
|
||||
let bin = gstreamer::Bin::new(None);
|
||||
bin.add_many(elems)?;
|
||||
gstreamer::Element::link_many(elems)?;
|
||||
|
||||
// make input for bin point to first element
|
||||
let sink = elems[0].get_static_pad("sink").unwrap();
|
||||
let ghost = gstreamer::GhostPad::new("sink", &sink)
|
||||
.ok_or(format_err!("Can't create GhostPad"))?;
|
||||
|
||||
ghost.set_active(true)?;
|
||||
bin.add_pad(&ghost)?;
|
||||
|
||||
player.set_property("video-sink", &bin.upcast::<gstreamer::Element>())?;
|
||||
|
||||
Ok((player, appsink))
|
||||
}
|
||||
|
||||
|
||||
struct Renderer {
|
||||
xsize: usize,
|
||||
ysize: usize,
|
||||
#[cfg(feature = "video")]
|
||||
xpos: usize,
|
||||
#[cfg(feature = "video")]
|
||||
ypos: usize,
|
||||
last_frame: Option<DynamicImage>,
|
||||
#[cfg(feature = "video")]
|
||||
position: Option<usize>,
|
||||
#[cfg(feature = "video")]
|
||||
duration: Option<usize>
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
fn new(xsize: usize,
|
||||
ysize: usize,
|
||||
xpos: usize,
|
||||
ypos: usize) -> Renderer {
|
||||
Renderer {
|
||||
xsize,
|
||||
ysize,
|
||||
#[cfg(feature = "video")]
|
||||
xpos,
|
||||
#[cfg(feature = "video")]
|
||||
ypos,
|
||||
#[cfg(feature = "video")]
|
||||
last_frame: None,
|
||||
#[cfg(feature = "video")]
|
||||
position: None,
|
||||
#[cfg(feature = "video")]
|
||||
duration: None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// #[cfg(feature = "video")]
|
||||
fn set_size(&mut self, xsize: usize, ysize: usize) -> MResult<()> {
|
||||
self.xsize = xsize;
|
||||
self.ysize = ysize;
|
||||
|
||||
if let Some(ref frame) = self.last_frame {
|
||||
let pos = self.position.unwrap_or(0);
|
||||
let dur = self.duration.unwrap_or(0);
|
||||
|
||||
// Use send_image, because send_frame takes SampleRef
|
||||
self.send_image(frame)?;
|
||||
|
||||
let stdout = std::io::stdout();
|
||||
let mut stdout = stdout.lock();
|
||||
|
||||
writeln!(stdout, "")?;
|
||||
writeln!(stdout, "{}", pos)?;
|
||||
writeln!(stdout, "{}", dur)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_image(&self, image: &DynamicImage) -> MResult<()> {
|
||||
let rendered_img = self.render_image(image);
|
||||
let stdout = std::io::stdout();
|
||||
let mut stdout = stdout.lock();
|
||||
|
||||
for line in rendered_img {
|
||||
writeln!(stdout, "{}", line)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// #[cfg(feature = "video")]
|
||||
fn send_frame(&mut self,
|
||||
frame: &gstreamer::sample::SampleRef,
|
||||
position: u64,
|
||||
duration: u64,
|
||||
raw: bool)
|
||||
-> MResult<()> {
|
||||
let buffer = frame.get_buffer()
|
||||
.ok_or(format_err!("Couldn't get buffer from frame!"))?;
|
||||
let map = buffer.map_readable()
|
||||
.ok_or(format_err!("Couldn't get buffer from frame!"))?;
|
||||
|
||||
let stdout = std::io::stdout();
|
||||
let mut stdout = stdout.lock();
|
||||
|
||||
|
||||
if !raw {
|
||||
let img = image::load_from_memory_with_format(&map,
|
||||
image::ImageFormat::PNM)?;
|
||||
let rendered_img = self.render_image(&img);
|
||||
|
||||
self.last_frame = Some(img);
|
||||
self.position = Some(position as usize);
|
||||
self.duration = Some(duration as usize);
|
||||
|
||||
for line in rendered_img {
|
||||
writeln!(stdout, "{}", line)?;
|
||||
}
|
||||
} else {
|
||||
stdout.write_all(map.as_slice())?;
|
||||
|
||||
// Add newline after frame data
|
||||
write!(stdout, "\n")?;
|
||||
}
|
||||
|
||||
// Empty line means end of frame
|
||||
writeln!(stdout, "")?;
|
||||
|
||||
// Send position and duration
|
||||
writeln!(stdout, "{}", position)?;
|
||||
writeln!(stdout, "{}", duration)?;
|
||||
|
||||
#[cfg(feature = "video")]
|
||||
match raw {
|
||||
true => {
|
||||
writeln!(stdout, "{}", self.xpos)?;
|
||||
writeln!(stdout, "{}", self.ypos)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn render_image(&self, image: &DynamicImage) -> Vec<String> {
|
||||
let (xsize, ysize) = self.max_size(&image);
|
||||
|
||||
let img = image.resize_exact(xsize as u32,
|
||||
ysize as u32,
|
||||
FilterType::Nearest).to_rgba();
|
||||
|
||||
|
||||
let rows = img.pixels()
|
||||
.collect::<Vec<_>>()
|
||||
.chunks(xsize as usize)
|
||||
.map(|line| line.to_vec())
|
||||
.collect::<Vec<Vec<_>>>();
|
||||
|
||||
rows.chunks(2)
|
||||
.map(|rows| {
|
||||
rows[0]
|
||||
.iter()
|
||||
.zip(rows[1].iter())
|
||||
.map(|(upper, lower)| {
|
||||
let upper_color = upper.to_rgb();
|
||||
let lower_color = lower.to_rgb();
|
||||
|
||||
format!("{}{}▀{}",
|
||||
Fg(Rgb(upper_color[0], upper_color[1], upper_color[2])),
|
||||
Bg(Rgb(lower_color[0], lower_color[1], lower_color[2])),
|
||||
termion::style::Reset
|
||||
)
|
||||
}).collect()
|
||||
}).collect()
|
||||
}
|
||||
|
||||
pub fn max_size(&self, image: &DynamicImage) -> (usize, usize)
|
||||
{
|
||||
let xsize = self.xsize;
|
||||
let ysize = self.ysize;
|
||||
let img_xsize = image.width();
|
||||
let img_ysize = image.height();
|
||||
let img_ratio = img_xsize as f32 / img_ysize as f32;
|
||||
|
||||
let mut new_x = xsize;
|
||||
let mut new_y;
|
||||
|
||||
new_y = if img_ratio < 1 as f32 {
|
||||
(xsize as f32 * img_ratio) as usize
|
||||
} else {
|
||||
(xsize as f32 / img_ratio) as usize
|
||||
};
|
||||
|
||||
// Multiply by two because of half-block
|
||||
if new_y > ysize*2 {
|
||||
new_y = self.ysize * 2;
|
||||
|
||||
new_x = if img_ratio < 1 as f32 {
|
||||
(ysize as f32 / img_ratio) as usize * 2
|
||||
} else {
|
||||
(ysize as f32 * img_ratio) as usize * 2
|
||||
};
|
||||
}
|
||||
|
||||
// To make half-block encoding easier, y should be divisible by 2
|
||||
if new_y as u32 % 2 == 1 {
|
||||
new_y += 1;
|
||||
}
|
||||
|
||||
|
||||
(new_x as usize, new_y as usize)
|
||||
}
|
||||
}
|
|
@ -77,6 +77,7 @@ pub struct Config {
|
|||
pub icons: bool,
|
||||
pub media_autoplay: bool,
|
||||
pub media_mute: bool,
|
||||
pub media_previewer: String
|
||||
}
|
||||
|
||||
|
||||
|
@ -95,7 +96,8 @@ impl Config {
|
|||
cd_cmd: "find -type d | fzf".to_string(),
|
||||
icons: false,
|
||||
media_autoplay: false,
|
||||
media_mute: false
|
||||
media_mute: false,
|
||||
media_previewer: "hunter-media".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -128,6 +130,10 @@ impl Config {
|
|||
Ok(("media_autoplay", "off")) => { config.media_autoplay = false; },
|
||||
Ok(("media_mute", "on")) => { config.media_mute = true; },
|
||||
Ok(("media_mute", "off")) => { config.media_mute = false; },
|
||||
Ok(("media_previewer", cmd)) => {
|
||||
let cmd = cmd.to_string();
|
||||
config.select_cmd = cmd;
|
||||
}
|
||||
_ => { HError::config_error::<Config>(line.to_string()).log(); }
|
||||
}
|
||||
config
|
||||
|
|
|
@ -10,6 +10,7 @@ use std::path::PathBuf;
|
|||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::foldview::LogEntry;
|
||||
use crate::mediaview::MediaError;
|
||||
|
||||
pub type HResult<T> = Result<T, HError>;
|
||||
|
||||
|
@ -101,6 +102,8 @@ pub enum HError {
|
|||
UTF8ParseError(std::str::Utf8Error),
|
||||
#[fail(display = "Failed to parse integer!")]
|
||||
ParseIntError(std::num::ParseIntError),
|
||||
#[fail(display = "{}", _0)]
|
||||
Media(MediaError)
|
||||
}
|
||||
|
||||
impl HError {
|
||||
|
|
|
@ -424,9 +424,9 @@ impl Files {
|
|||
file.path = new_path.into();
|
||||
file.reload_meta()?;
|
||||
},
|
||||
DebouncedEvent::Error(err, path) => {
|
||||
dbg!(err);
|
||||
dbg!(path);
|
||||
DebouncedEvent::Error(err, _path) => {
|
||||
// Never seen this happen. Should reload affected dirs
|
||||
HError::log::<()>(&format!("{}", err))?;
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@ use crate::fail::HResult;
|
|||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::mediaview::MediaError;
|
||||
|
||||
impl std::cmp::PartialEq for ImgView {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.core == other.core &&
|
||||
|
@ -31,18 +33,34 @@ impl ImgView {
|
|||
|
||||
pub fn encode_file(&mut self) -> HResult<()> {
|
||||
let (xsize, ysize) = self.core.coordinates.size_u();
|
||||
let (xpos, ypos) = self.core.coordinates.position_u();
|
||||
let file = &self.file;
|
||||
let media_previewer = self.core.config().media_previewer;
|
||||
|
||||
let output = std::process::Command::new("preview-gen")
|
||||
let output = std::process::Command::new(&media_previewer)
|
||||
.arg(format!("{}", (xsize)))
|
||||
.arg(format!("{}", (ysize+1)))
|
||||
.arg(format!("{}", xpos))
|
||||
.arg(format!("{}", ypos))
|
||||
.arg("image")
|
||||
.arg(format!("true"))
|
||||
.arg(format!("true"))
|
||||
.arg(file.to_string_lossy().to_string())
|
||||
.output()?
|
||||
.output()
|
||||
.map_err(|e| {
|
||||
let msg = format!("Couldn't run {}{}{}! Error: {:?}",
|
||||
crate::term::color_red(),
|
||||
media_previewer,
|
||||
crate::term::normal_color(),
|
||||
&e.kind());
|
||||
|
||||
self.core.show_status(&msg).ok();
|
||||
|
||||
MediaError::NoPreviewer(msg)
|
||||
})?
|
||||
.stdout;
|
||||
|
||||
|
||||
let output = std::str::from_utf8(&output)?;
|
||||
let output = output.lines()
|
||||
.map(|l| l.to_string())
|
||||
|
|
|
@ -59,10 +59,7 @@ mod icon;
|
|||
mod quick_actions;
|
||||
mod trait_ext;
|
||||
mod config_installer;
|
||||
|
||||
#[cfg(feature = "img")]
|
||||
mod imgview;
|
||||
#[cfg(feature = "video")]
|
||||
mod mediaview;
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use lazy_static;
|
||||
use termion::event::Key;
|
||||
use failure::{self, Fail};
|
||||
|
||||
use crate::widget::{Widget, WidgetCore};
|
||||
use crate::coordinates::Coordinates;
|
||||
|
@ -14,6 +15,18 @@ use std::sync::{Arc, Mutex, RwLock,
|
|||
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
|
||||
|
@ -61,8 +74,23 @@ impl MediaType {
|
|||
impl MediaView {
|
||||
pub fn new_from_file(core: WidgetCore,
|
||||
file: &Path,
|
||||
media_type: MediaType) -> MediaView {
|
||||
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 (xsize, ysize) = core.coordinates.size_u();
|
||||
let (xpos, ypos) = core.coordinates.position_u();
|
||||
let (tx_cmd, rx_cmd) = channel();
|
||||
|
||||
let imgview = ImgView {
|
||||
|
@ -71,6 +99,7 @@ impl MediaView {
|
|||
file: file.to_path_buf()
|
||||
};
|
||||
|
||||
// Stuff that gets moved into the closure
|
||||
let imgview = Arc::new(Mutex::new(imgview));
|
||||
let thread_imgview = imgview.clone();
|
||||
|
||||
|
@ -82,7 +111,8 @@ impl MediaView {
|
|||
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 run_preview = Box::new(move | auto,
|
||||
mute,
|
||||
|
@ -93,18 +123,32 @@ impl MediaView {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
let mut previewer = std::process::Command::new("preview-gen")
|
||||
|
||||
let mut previewer = std::process::Command::new(&media_previewer)
|
||||
.arg(format!("{}", (xsize)))
|
||||
// Leave space for position/seek bar
|
||||
.arg(format!("{}", (ysize-1)))
|
||||
.arg(format!("{}", xpos))
|
||||
.arg(format!("{}", ypos))
|
||||
.arg(format!("{}", ctype.to_str()))
|
||||
.arg(format!("{}", auto))
|
||||
.arg(format!("{}", mute))
|
||||
.arg(&path)
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::inherit())
|
||||
.spawn()?;
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.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()?;
|
||||
|
@ -174,7 +218,7 @@ impl MediaView {
|
|||
});
|
||||
|
||||
|
||||
MediaView {
|
||||
Ok(MediaView {
|
||||
core: core.clone(),
|
||||
imgview: imgview,
|
||||
file: file.to_path_buf(),
|
||||
|
@ -186,7 +230,7 @@ impl MediaView {
|
|||
stale: stale,
|
||||
process: process,
|
||||
preview_runner: Some(run_preview)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn start_video(&mut self) -> HResult<()> {
|
||||
|
@ -308,7 +352,7 @@ impl MediaView {
|
|||
// Since GStreamer sucks, just create a new instace
|
||||
let mut view = MediaView::new_from_file(self.core.clone(),
|
||||
&self.file.clone(),
|
||||
self.media_type.clone());
|
||||
self.media_type.clone())?;
|
||||
|
||||
// Insert buffer to prevent flicker
|
||||
let buffer = self.imgview.lock()?.buffer.clone();
|
||||
|
|
|
@ -12,10 +12,7 @@ use crate::widget::{Widget, WidgetCore};
|
|||
use crate::coordinates::Coordinates;
|
||||
use crate::fail::{HResult, HError, ErrorLog};
|
||||
use crate::dirty::Dirtyable;
|
||||
|
||||
#[cfg(feature = "img")]
|
||||
use crate::imgview::ImgView;
|
||||
#[cfg(feature = "video")]
|
||||
use crate::mediaview::MediaView;
|
||||
|
||||
|
||||
|
@ -201,9 +198,7 @@ impl PartialEq for Previewer {
|
|||
enum PreviewWidget {
|
||||
FileList(ListView<Files>),
|
||||
TextView(TextView),
|
||||
#[cfg(feature = "img")]
|
||||
ImgView(ImgView),
|
||||
#[cfg(feature = "video")]
|
||||
MediaView(MediaView)
|
||||
}
|
||||
|
||||
|
@ -370,26 +365,23 @@ impl Previewer {
|
|||
let is_gif = mime.subtype() == "gif";
|
||||
|
||||
match mime_type {
|
||||
#[cfg(feature = "video")]
|
||||
_ if mime_type == "video" || is_gif => {
|
||||
let media_type = crate::mediaview::MediaType::Video;
|
||||
let mediaview = MediaView::new_from_file(core.clone(),
|
||||
&file.path,
|
||||
media_type);
|
||||
media_type)?;
|
||||
return Ok(PreviewWidget::MediaView(mediaview));
|
||||
}
|
||||
#[cfg(feature = "img")]
|
||||
"image" => {
|
||||
let imgview = ImgView::new_from_file(core.clone(),
|
||||
&file.path())?;
|
||||
return Ok(PreviewWidget::ImgView(imgview));
|
||||
}
|
||||
#[cfg(feature = "video")]
|
||||
"audio" => {
|
||||
let media_type = crate::mediaview::MediaType::Audio;
|
||||
let mediaview = MediaView::new_from_file(core.clone(),
|
||||
&file.path,
|
||||
media_type);
|
||||
media_type)?;
|
||||
return Ok(PreviewWidget::MediaView(mediaview));
|
||||
}
|
||||
"text" if mime.subtype() == "plain" => {
|
||||
|
@ -578,9 +570,7 @@ impl Widget for PreviewWidget {
|
|||
match self {
|
||||
PreviewWidget::FileList(widget) => widget.get_core(),
|
||||
PreviewWidget::TextView(widget) => widget.get_core(),
|
||||
#[cfg(feature = "img")]
|
||||
PreviewWidget::ImgView(widget) => widget.get_core(),
|
||||
#[cfg(feature = "video")]
|
||||
PreviewWidget::MediaView(widget) => widget.get_core()
|
||||
}
|
||||
}
|
||||
|
@ -588,9 +578,7 @@ impl Widget for PreviewWidget {
|
|||
match self {
|
||||
PreviewWidget::FileList(widget) => widget.get_core_mut(),
|
||||
PreviewWidget::TextView(widget) => widget.get_core_mut(),
|
||||
#[cfg(feature = "img")]
|
||||
PreviewWidget::ImgView(widget) => widget.get_core_mut(),
|
||||
#[cfg(feature = "video")]
|
||||
PreviewWidget::MediaView(widget) => widget.get_core_mut()
|
||||
}
|
||||
}
|
||||
|
@ -598,9 +586,7 @@ impl Widget for PreviewWidget {
|
|||
match self {
|
||||
PreviewWidget::FileList(widget) => widget.set_coordinates(coordinates),
|
||||
PreviewWidget::TextView(widget) => widget.set_coordinates(coordinates),
|
||||
#[cfg(feature = "img")]
|
||||
PreviewWidget::ImgView(widget) => widget.set_coordinates(coordinates),
|
||||
#[cfg(feature = "video")]
|
||||
PreviewWidget::MediaView(widget) => widget.set_coordinates(coordinates),
|
||||
}
|
||||
}
|
||||
|
@ -608,9 +594,7 @@ impl Widget for PreviewWidget {
|
|||
match self {
|
||||
PreviewWidget::FileList(widget) => widget.refresh(),
|
||||
PreviewWidget::TextView(widget) => widget.refresh(),
|
||||
#[cfg(feature = "img")]
|
||||
PreviewWidget::ImgView(widget) => widget.refresh(),
|
||||
#[cfg(feature = "video")]
|
||||
PreviewWidget::MediaView(widget) => widget.refresh()
|
||||
}
|
||||
}
|
||||
|
@ -618,9 +602,7 @@ impl Widget for PreviewWidget {
|
|||
match self {
|
||||
PreviewWidget::FileList(widget) => widget.get_drawlist(),
|
||||
PreviewWidget::TextView(widget) => widget.get_drawlist(),
|
||||
#[cfg(feature = "img")]
|
||||
PreviewWidget::ImgView(widget) => widget.get_drawlist(),
|
||||
#[cfg(feature = "video")]
|
||||
PreviewWidget::MediaView(widget) => widget.get_drawlist()
|
||||
}
|
||||
}
|
||||
|
@ -629,9 +611,7 @@ impl Widget for PreviewWidget {
|
|||
match self {
|
||||
PreviewWidget::FileList(widget) => widget.on_key(key),
|
||||
PreviewWidget::TextView(widget) => widget.on_key(key),
|
||||
#[cfg(feature = "img")]
|
||||
PreviewWidget::ImgView(widget) => widget.on_key(key),
|
||||
#[cfg(feature = "video")]
|
||||
PreviewWidget::MediaView(widget) => widget.on_key(key)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -192,9 +192,16 @@ impl WidgetCore {
|
|||
}
|
||||
|
||||
pub fn config(&self) -> Config {
|
||||
self.config.read().unwrap().get()
|
||||
.map(|config| config.clone())
|
||||
.unwrap_or(Config::new())
|
||||
self.get_conf()
|
||||
.unwrap_or_else(|_| Config::new())
|
||||
}
|
||||
|
||||
fn get_conf(&self) -> HResult<Config> {
|
||||
let conf = self.config
|
||||
.read()?
|
||||
.get()?
|
||||
.clone();
|
||||
Ok(conf)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue