use std::collections::HashMap;
use std::fs::File;
use std::io::BufReader;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};
use clap::{Arg, Command, value_parser};
use font_kit::loaders::freetype::Font;
use image::{DynamicImage, GenericImageView};
use log::{error, info, warn};
use pyo3::prelude::*;
use pyo3::types::PyList;
use timely::dataflow::channels::pact::Pipeline;
use timely::dataflow::operators::capture::Event;
use timely::dataflow::operators::{
Branch, Capture, Concat, ConnectLoop, Enter, Filter, Inspect, Leave, LoopVariable, Map,
Notificator, Operator, Probe, ResultStream,
};
use timely::dataflow::{InputHandle, ProbeHandle, Scope};
use timely::order::Product;
use timely::worker::Config;
use pathfinder_geometry::rect::RectI;
use rahmen::Vector;
use rahmen::config::Settings;
use rahmen::dataflow::{Configuration, FormatText, ResizeImage};
use rahmen::display::Display;
use rahmen::display_framebuffer::FramebufferDisplay;
#[cfg(feature = "minifb")]
use rahmen::display_minifb::MinifbDisplay;
use rahmen::errors::{RahmenError, RahmenResult};
use rahmen::font::FontRenderer;
use rahmen::provider::{Provider, StatusLineFormatter, load_image_from_path};
use rahmen::provider_list::ListProvider;
static SPLASH: &[u8] = include_bytes!("rahmen.png");
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum RunControl {
Terminate,
Suppressed,
}
fn fatal_err<T>(result: RahmenResult<Option<T>>) -> RunResult<T> {
match result {
Ok(None) => Err(RunControl::Terminate),
Ok(Some(t)) => Ok(t),
Err(e) => {
error!("Encountered error, terminating: {}", e);
Err(RunControl::Terminate)
}
}
}
fn suppress_err<T>(result: RahmenResult<T>) -> RunResult<T> {
result.map_err(|e| {
error!("Encountered error, suppressing: {}", e);
RunControl::Suppressed
})
}
#[derive(Clone, Debug, PartialEq)]
enum Render {
Image(usize, Vector, Arc<DynamicImage>),
Blank(usize, Vector, Vector),
}
type RunResult<T> = Result<T, RunControl>;
#[cfg(unix)]
const SYSTEM_CONFIG_PATH: &str = "/etc/rahmen.toml";
fn main() -> RahmenResult<()> {
env_logger::init();
let matches = Command::new("Rahmen client")
.arg(
Arg::new("display")
.short('d')
.long("display")
.help("Select the display provider")
.value_name("display")
.value_parser([
#[cfg(feature = "minifb")]
"minifb",
"framebuffer",
])
.default_value("framebuffer"),
)
.arg(Arg::new("input").required(true).index(1))
.arg(Arg::new("output").short('o').long("output"))
.arg(
Arg::new("time")
.short('t')
.long("time")
.value_parser(value_parser!(f64)),
)
.arg(
Arg::new("buffer_max_size")
.long("buffer_max_size")
.value_parser(value_parser!(usize))
.default_value("16000000"),
)
.arg(
Arg::new("font")
.long("font")
.default_value("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"),
)
.arg(
Arg::new("font_size")
.long("font_size")
.value_parser(value_parser!(f32)),
)
.arg(Arg::new("config").long("config").short('c'))
.get_matches();
let input = matches
.get_one::<String>("input")
.expect("Input missing")
.as_str();
let mut provider: Box<dyn Provider<_>> = if input.eq("-") {
info!("Reading from stdin");
Box::new(ListProvider::new(BufReader::new(std::io::stdin())))
} else if let Ok(file) = File::open(input) {
info!("Reading from file");
Box::new(ListProvider::new(BufReader::new(file)))
} else {
info!("Reading from pattern {}", input);
Box::new(rahmen::provider_glob::create(input)?)
};
let dirs = xdg::BaseDirectories::new();
let settings: Settings = if let Some(path) = matches
.get_one::<String>("config")
.map(PathBuf::from)
.or_else(|| dirs.find_config_file("rahmen.toml"))
.or_else(|| {
#[cfg(unix)]
if std::fs::metadata(SYSTEM_CONFIG_PATH).is_ok() {
Some(SYSTEM_CONFIG_PATH.into())
} else {
None
}
#[cfg(not(unix))]
None
}) {
config::Config::builder()
.add_source(config::File::from(path))
.build()?
.try_deserialize()?
} else {
warn!("Config file not found, continuing with default settings");
Default::default()
};
if let Some(python_paths) = settings.py_path {
Python::attach(|py| -> PyResult<()> {
let syspath = py.import("sys")?.getattr("path")?.cast_into::<PyList>()?;
for path in &python_paths {
syspath.insert(0, path)?;
}
Ok(())
})
.expect("Failed to configure Python sys.path");
}
let status_line_formatter = StatusLineFormatter::new(
settings.status_line.iter().cloned(),
settings.py_postprocess,
settings.separator.unwrap_or_else(|| ", ".to_string()),
)?;
let buffer_max_size: usize = *matches
.get_one::<usize>("buffer_max_size")
.expect("Missing buffer_max_size");
let font =
Font::from_path(matches.get_one::<String>("font").expect("Missing font"), 0).unwrap();
let font_renderer = FontRenderer::with_font(font);
let duration_millis = (matches
.get_one::<f64>("time")
.copied()
.or(settings.delay)
.unwrap_or(90.)
* 1000f64) as u64;
let delay = Duration::from_millis(duration_millis);
info!("Delay: {:?}", delay);
let font_size_f = matches
.get_one::<f32>("font_size")
.copied()
.or(settings.font_size)
.unwrap_or(30.);
let show_time = settings.display_time.unwrap_or(false);
let time_format = settings.time_format.unwrap_or("%H:%M:%S".into());
let allocator = timely::communication::allocator::Thread::new();
let mut worker = timely::worker::Worker::new(Config::default(), allocator);
let mut input_configuration: InputHandle<_, Configuration> = InputHandle::new();
let mut probe = ProbeHandle::new();
let output = worker.dataflow(|scope| {
let configuration_stream = input_configuration.to_stream(scope);
let img_path_stream = scope.scoped::<Product<_, u32>, _, _>("File loading", |inner| {
let (handle, cycle) = inner.loop_variable(1);
let (ok, err) = configuration_stream
.filter(|c| matches!(c, Configuration::Tick))
.enter(inner)
.concat(&cycle)
.map(move |_| fatal_err(provider.next_image()))
.and_then(move |ref path| {
suppress_err(
load_image_from_path(path, Some(buffer_max_size))
.map(|img| (path.clone(), Arc::new(img))),
)
})
.branch(|_t, d| d.as_ref().err() == Some(&RunControl::Suppressed));
err.map(|_| Configuration::Tick).connect_loop(handle);
ok.leave()
});
let err_stream = img_path_stream.err();
let mut buffer = vec![];
let mut stash: HashMap<Duration, String> = HashMap::new();
let mut current_text = None;
let mut status_line_stream = img_path_stream
.ok()
.flat_map(move |(p, _img)| status_line_formatter.format(&p).ok())
.concat(&configuration_stream.flat_map(|c| match c {
Configuration::Greeting(text) => Some(text),
_ => None,
}))
.inspect(|loc| info!("Status line: {}", loc));
if show_time {
status_line_stream = status_line_stream.unary_notify(
Pipeline,
"Show time",
Some(Duration::from_secs(0)),
move |input, output, not: &mut Notificator<Duration>| {
input.for_each(|cap, data| {
data.swap(&mut buffer);
if let Some(text) = buffer.drain(..).next_back() {
*stash.entry(*cap.time()).or_default() = text;
not.notify_at(cap.retain());
}
});
not.for_each(|cap, cnt, not| {
let request_notification = if let Some(text) = stash.remove(cap.time()) {
current_text = Some(text);
cnt == 2
} else {
true
};
let now = chrono::Local::now();
let delay = std::cmp::max(50, 1000 - now.timestamp_subsec_millis() as u64);
if request_notification && !not.frontier(0).is_empty() {
let mut next_time = *cap.time() + Duration::from_millis(delay);
while !not.frontier(0).less_equal(&next_time) {
next_time += Duration::from_secs(1);
}
not.notify_at(cap.delayed(&next_time));
}
if let Some(text) = ¤t_text {
let time_text = format!("[{}] {}", now.format(&time_format), text);
output.session(&cap).give(time_text);
}
});
},
);
}
let status_line_stream =
status_line_stream.map(|s| s.split('\n').map(Into::into).collect());
let text_img_stream =
status_line_stream.format_text(&configuration_stream, font_renderer, 2);
let adjusted_configuration_stream = {
let mut stash: HashMap<_, Vec<_>> = HashMap::new();
let mut buffer = vec![];
let mut current_font_size = None;
let mut current_font_canvas_vstretch = None;
configuration_stream.unary_notify(
Pipeline,
"Adjust configuration",
None,
move |input, output, not| {
input.for_each(|cap, data| {
data.swap(&mut buffer);
stash.entry(*cap.time()).or_default().append(&mut buffer);
not.notify_at(cap.retain());
});
not.for_each(|cap, _, _not| {
if let Some(updates) = stash.remove(cap.time()) {
output.session(&cap).give_iterator(updates.into_iter().map(
|configuration| {
match configuration {
Configuration::FontSize(font_size) => {
current_font_size = Some(font_size);
Configuration::FontSize(font_size)
}
Configuration::FontCanvasVStretch(font_canvas_vstretch) => {
current_font_canvas_vstretch =
Some(font_canvas_vstretch);
Configuration::FontCanvasVStretch(font_canvas_vstretch)
}
Configuration::ScreenDimensions(width, height) => {
Configuration::ScreenDimensions(
width,
height
- (current_font_size.unwrap_or(0.)
* current_font_canvas_vstretch
.unwrap_or(1.0))
.ceil()
as u32,
)
}
configuration => configuration,
}
},
));
}
});
},
)
};
let img_stream = img_path_stream
.ok()
.map(|(_, img)| img)
.concat(&configuration_stream.flat_map(|c| match c {
Configuration::Splash(img) => Some(img),
_ => None,
}))
.resize_image(&adjusted_configuration_stream, 1);
let mut size_stash: HashMap<usize, _> = HashMap::new();
let mut input_buffer: HashMap<_, Vec<(_, _, _)>> = HashMap::new();
let composed_img_stream = img_stream.concat(&text_img_stream).unary_notify(
Pipeline,
"Infer blanking",
None,
move |input, output, not| {
let mut buffer = vec![];
input.for_each(|time, data| {
data.swap(&mut buffer);
input_buffer
.entry(*time.time())
.or_default()
.append(&mut buffer);
not.notify_at(time.retain());
});
not.for_each(|time, _count, _not| {
if let Some(updates) = input_buffer.remove(time.time()) {
output
.session(&time)
.give_iterator(updates.into_iter().flat_map(|(key, anchor, img)| {
let rect = RectI::new(
anchor,
Vector::new(img.dimensions().0 as _, img.dimensions().1 as _),
);
size_stash
.insert(key, rect)
.into_iter()
.flat_map(move |old_rect| compute_blanking(key, rect, old_rect))
.chain(Some(Render::Image(key, anchor, img)))
}));
}
})
},
);
err_stream
.map(Err)
.concat(&composed_img_stream.map(Ok))
.probe_with(&mut probe)
.capture()
});
let start_time = Instant::now();
let mut dimensions = None;
input_configuration.send(Configuration::FontSize(font_size_f));
input_configuration.send(Configuration::FontCanvasVStretch(1.4));
match image::load_from_memory(SPLASH) {
Ok(image) => {
info!("Sending splash screen");
input_configuration.send(Configuration::Splash(Arc::new(image)));
}
Err(err) => warn!("Failed to load splash screen: {}", err),
}
input_configuration.send(Configuration::Greeting(format!("Rahmen {}", VERSION)));
let mut next_image_at = start_time.elapsed() + Duration::from_secs(1);
let display_fn = |display: &mut dyn Display| {
let now = start_time.elapsed();
if next_image_at < now {
input_configuration.send(Configuration::Tick);
next_image_at = now + delay;
}
if Some(display.dimensions()) != dimensions {
dimensions = Some(display.dimensions());
input_configuration.send(Configuration::ScreenDimensions(
display.dimensions().0,
display.dimensions().1,
));
}
input_configuration.advance_to(now);
while probe.less_than(&now) {
worker.step();
}
let mut has_update = false;
let result = match output.try_iter().all(|result| match result {
Event::Progress(_) => true,
Event::Messages(_, r) => {
let mut terminate = false;
for result in r {
match result {
Ok(Render::Image(key, anchor, ref img)) => {
has_update = true;
if let Err(err) = display.render(key, anchor, img.as_ref()) {
error!("Render failed: {}", err);
terminate = true;
}
}
Ok(Render::Blank(key, anchor, size)) => {
has_update = true;
if let Err(err) = display.blank(key, anchor, size) {
error!("Blank failed: {}", err);
terminate = true;
}
}
Err(RunControl::Terminate) => terminate = true,
_ => {}
}
}
!terminate
}
}) {
true => Ok(()),
false => Err(RahmenError::Terminate),
};
if result.is_ok() && has_update {
display.update()
} else {
result
}
};
match matches
.get_one::<String>("display")
.expect("Display missing")
.as_str()
{
"framebuffer" => {
let path_to_device = matches
.get_one::<String>("output")
.expect("Framebuffer output missing");
let framebuffer = framebuffer::Framebuffer::new(path_to_device).unwrap();
let _ = framebuffer::Framebuffer::set_kd_mode(framebuffer::KdMode::Graphics)
.map_err(|_e| warn!("Failed to set graphics mode."));
ctrlc::set_handler(|| {
let _ = framebuffer::Framebuffer::set_kd_mode(framebuffer::KdMode::Text)
.map_err(|_e| warn!("Failed to set graphics mode."));
std::process::exit(0);
})
.unwrap();
FramebufferDisplay::new(framebuffer).main_loop(display_fn);
let _ = framebuffer::Framebuffer::set_kd_mode(framebuffer::KdMode::Text)
.map_err(|_e| warn!("Failed to set graphics mode."));
}
#[cfg(feature = "minifb")]
"minifb" => MinifbDisplay::new()?.main_loop(display_fn),
_ => panic!("Unknown display"),
};
input_configuration.close();
while worker.step() {}
Ok(())
}
fn compute_blanking(key: usize, rect: RectI, old_rect: RectI) -> Vec<Render> {
if let Some(overlap) = old_rect.intersection(rect) {
let above = RectI::from_points(old_rect.origin(), overlap.upper_right());
let left = RectI::from_points(old_rect.origin(), overlap.lower_left());
let right = RectI::from_points(overlap.upper_right(), old_rect.lower_right());
let below = RectI::from_points(overlap.lower_left(), old_rect.lower_right());
[above, left, right, below]
.iter()
.filter(|r| r.width() > 0 && r.height() > 0)
.map(|r| Render::Blank(key, r.origin(), r.size()))
.collect::<Vec<_>>()
} else {
vec![Render::Blank(key, old_rect.origin(), old_rect.size())]
}
}