Compare commits

...

38 commits

Author SHA1 Message Date
Stephen Seo d90c86c894 cargo update, bump version 2024-02-28 11:53:21 +09:00
Stephen Seo f42fadd403 Update README.md
Previously, the README referenced dependency structopt, which was
replaced with dependency clap.
2023-11-28 21:42:12 +09:00
Stephen Seo f2f58047a5 Version 0.4.8 2023-11-28 15:02:27 +09:00
Stephen Seo d457098f2a Fix use of deprecated object from bindgen 2023-11-28 15:02:09 +09:00
Stephen Seo d0beff5a98 Update build dependency bindgen 2023-11-28 15:00:26 +09:00
Stephen Seo 7a860e323a Update image and wgpu dependencies 2023-11-28 12:29:35 +09:00
Stephen Seo ef234f0ec0 Replace dependency structopt with clap 2023-11-28 12:20:49 +09:00
Stephen Seo 391949cde6 Version 0.4.7 2023-11-26 15:49:48 +09:00
Stephen Seo f66880f13d Update ggez dependency 2023-11-26 15:42:04 +09:00
Stephen Seo 83bb20c246 Remove no longer needed comment in Cargo.toml 2023-06-27 18:59:12 +09:00
Stephen Seo dd868969cc Version 0.4.6 with new version of ggez 2023-06-27 10:41:04 +09:00
Stephen Seo 1dc28d7b07 Update README.md 2023-02-27 15:55:31 +09:00
Stephen Seo f7ffa62e02 Update README.md 2023-02-27 15:49:05 +09:00
Stephen Seo bcea959381 Merge branch 'devel' 2023-02-27 15:39:48 +09:00
Stephen Seo aa6fb750e7 Refactor "no-password-fix"
Refactors the fix for the use case of when MPD requires no password.
2023-02-04 21:32:31 +09:00
Stephen Seo a1706913e6 Fix timer bg not updating
This commit fixes the bg mesh (slightly-transparent-black-bg for text)
not updating when the timer-text changes size.
2023-02-04 20:41:47 +09:00
Stephen Seo e28d20a5da
Update README.md 2023-02-04 19:59:04 +09:00
Stephen Seo 4653399fe9 Backport Fix: display not working when no password
Previous implementation only worked if MPD was accessed with a password.
This commit fixes no-password access of MPD.

Backported from `devel` branch.
2023-02-04 19:56:09 +09:00
Stephen Seo e0be69df81 Impl passing password via file 2023-01-31 16:19:55 +09:00
Stephen Seo 8643542b7a
Update README.md 2023-01-31 16:05:48 +09:00
Stephen Seo e9e57c9dff Update README.md 2023-01-31 16:02:02 +09:00
Stephen Seo d1590bee0a Update README.md 2023-01-31 15:59:19 +09:00
Stephen Seo 56f6784892 Fix where display doesn't work when no password
Previous implementation only worked if MPD was accessed with a password.
This commit fixes no-password access of MPD.
2023-01-31 15:57:56 +09:00
Stephen Seo a223d8b530
Update README.md 2023-01-29 16:57:54 +09:00
Stephen Seo fad82f6448
Update README.md 2023-01-29 16:55:20 +09:00
Stephen Seo 934aa1a610 Version 0.4.1 2023-01-29 16:49:04 +09:00
Stephen Seo 6d400cd7c7 Workaround for album art not drawing bug
ggez (at the time of this commit) latest release has a bug that results
in the album art not drawing. Workaround is to build against a ggez
commit in its "devel" branch.
2023-01-29 16:45:40 +09:00
Stephen Seo c902f0fcf3 Fix clippy warnings 2023-01-29 14:50:26 +09:00
Stephen Seo e025a48735 Version 0.4.0
Testing shows that even with some of the expect()/unwrap() additions,
the program is still stable enough to use. Will merge into the main
branch.
2023-01-08 17:43:19 +09:00
Stephen Seo 74a84d9f7a Change rust project edition to "2021"
Also update Cargo.lock.
2022-12-11 14:36:06 +09:00
Stephen Seo f2f93f5393 Fix clippy warning 2022-12-07 12:22:06 +09:00
Stephen Seo 1a623f451d Fix not-staying-in-fullscreen bug 2022-12-06 22:26:02 +09:00
Stephen Seo ef07d7936a Fix "brighter" displayed album art bug 2022-12-06 22:19:48 +09:00
Stephen Seo 7f6a24545c Update to latest ggez (0.8.1)
A lot of ".expect(...)" or ".unwrap(...)" lines were added, so these
need to be refactored out. For now, just got it to work with the latest
ggez.

Currently noticed bugs:

 - Album art is brighter than original.

 - Fullscreen doesn't work.
2022-12-06 22:03:46 +09:00
Stephen Seo 34d3e47863 Update README.md 2022-11-01 20:44:23 +09:00
Stephen Seo a9441536c4 Refactor handling of Result conversion 2022-11-01 17:36:16 +09:00
Stephen Seo a1fe8c3120 Impl better handling of unknown image type 2022-10-30 19:41:50 +09:00
Stephen Seo ce45a40df5 Fix clippy warnings 2022-10-30 14:05:04 +09:00
9 changed files with 2052 additions and 1774 deletions

3228
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,21 +1,23 @@
[package]
name = "mpd_info_screen"
version = "0.3.7"
edition = "2018"
version = "0.4.9"
edition = "2021"
description = "Displays info on currently playing music from an MPD daemon"
license = "MIT"
repository = "https://github.com/Stephen-Seo/mpd_info_screen"
resolver = "2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
structopt = "0.3"
clap = { version = "4.4", features = ["derive"] }
image = "0.24"
ggez = "0.7"
ggez = "0.9.3"
freetype = { version = "0.7", optional = true }
wgpu = "0.16"
[build-dependencies]
bindgen = { version = "0.53", optional = true }
bindgen = { version = "0.69", optional = true }
[features]
unicode_support = ["dep:freetype", "dep:bindgen"]

View file

@ -12,6 +12,11 @@ A Rust program that displays info about the currently running MPD server.
The window shows albumart (may be embedded in the audio file, or is a "cover.jpg" in the same directory as the song file), a "time-remaining"
counter, and the filename currently being played
## Known Bugs ❗❗
Currently there are no known bugs. Please report any bugs you find to the
[issue tracker](https://github.com/Stephen-Seo/mpd_info_screen/issues).
## Unicode Support
By default, unicode characters will not display properly. Build the project with
@ -23,32 +28,46 @@ installed already).
cargo build --release --features unicode_support
or through crates.io:
cargo install --features unicode_support mpd_info_screen
# Usage
mpd_info_screen 0.3.7
Displays info on currently playing music from an MPD daemon
USAGE:
mpd_info_screen [FLAGS] [OPTIONS] <host> [port]
Usage: mpd_info_screen [OPTIONS] <HOST> [PORT]
FLAGS:
--disable-show-album disable album display
--disable-show-artist disable artist display
--disable-show-filename disable filename display
--disable-show-title disable title display
--no-scale-fill don't scale-fill the album art to the window
--pprompt input password via prompt
-h, --help Prints help information
-V, --version Prints version information
Arguments:
<HOST>
[PORT] [default: 6600]
OPTIONS:
-l, --log-level <log-level> [default: Error] [possible values: Error, Warning, Debug, Verbose]
-p <password>
-t, --text-bg-opacity <text-bg-opacity> sets the opacity of the text background (0-255) [default: 190]
Options:
-p <PASSWORD>
ARGS:
<host>
<port> [default: 6600]
--disable-show-title
disable title display
--disable-show-artist
disable artist display
--disable-show-album
disable album display
--disable-show-filename
disable filename display
--pprompt
input password via prompt
--pfile <PASSWORD_FILE>
read password from file
--no-scale-fill
don't scale-fill the album art to the window
-l, --log-level <LOG_LEVEL>
[default: error] [possible values: error, warning, debug, verbose]
-t, --text-bg-opacity <TEXT_BG_OPACITY>
sets the opacity of the text background (0-255) [default: 190]
-h, --help
Print help
-V, --version
Print version
Note that presing the Escape key when the window is focused closes the program.
@ -77,8 +96,8 @@ MIT license.
Uses dependency [image](https://crates.io/crates/image) which is licensed under
MIT license.
Uses dependency [structopt](https://crates.io/crates/structopt) which is
licensed under Apache-2.0 or MIT licenses.
Uses dependency [clap](https://crates.io/crates/clap) which is licensed under
Apache-2.0 or MIT licenses.
## Unicode Support Dependencies

View file

@ -14,7 +14,7 @@ fn main() {
let bindings = bindgen::Builder::default()
.header("src/bindgen_wrapper.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("Unable to generate bindings");

View file

@ -1,7 +1,7 @@
use std::fmt::Display;
use structopt::clap::arg_enum;
use clap::ValueEnum;
#[derive(Copy, Clone, Debug, PartialEq)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum LogState {
Error,
Warning,
@ -9,14 +9,12 @@ pub enum LogState {
Verbose,
}
arg_enum! {
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum LogLevel {
Error,
Warning,
Debug,
Verbose,
}
#[derive(ValueEnum, Copy, Clone, Debug, PartialEq, Eq)]
pub enum LogLevel {
Error,
Warning,
Debug,
Verbose,
}
pub fn log<T>(msg: T, state: LogState, level: LogLevel)
@ -46,26 +44,26 @@ pub fn log_error<T>(msg: T)
where
T: Display,
{
println!("Error: {}", msg);
println!("Error: {msg}");
}
pub fn log_warning<T>(msg: T)
where
T: Display,
{
println!("Warning: {}", msg);
println!("Warning: {msg}");
}
pub fn log_debug<T>(msg: T)
where
T: Display,
{
println!("Debug: {}", msg);
println!("Debug: {msg}");
}
pub fn log_verbose<T>(msg: T)
where
T: Display,
{
println!("Verbose: {}", msg);
println!("Verbose: {msg}");
}

View file

@ -1,13 +1,16 @@
use crate::debug_log::{self, log};
use crate::mpd_handler::{InfoFromShared, MPDHandler, MPDHandlerState, MPDPlayState};
use crate::Opt;
use ggez::event::{self, EventHandler};
use ggez::event::EventHandler;
use ggez::graphics::{
self, Color, DrawMode, DrawParam, Drawable, Font, Image, Mesh, MeshBuilder, PxScale, Rect,
Text, TextFragment, Transform,
self, Color, DrawMode, DrawParam, Drawable, Image, Mesh, MeshBuilder, PxScale, Rect, Text,
TextFragment, Transform,
};
use ggez::{timer, Context, GameError, GameResult};
use ggez::input::keyboard::{self, KeyInput};
use ggez::mint::Vector2;
use ggez::{Context, GameError, GameResult};
use image::io::Reader as ImageReader;
use image::DynamicImage;
use std::io::Cursor;
use std::path::PathBuf;
use std::sync::atomic::AtomicBool;
@ -55,7 +58,7 @@ fn seconds_to_time(seconds: f64) -> String {
#[allow(clippy::ptr_arg)]
fn string_to_text(
string: String,
_loaded_fonts: &mut Vec<(PathBuf, Font)>,
_loaded_fonts: &mut Vec<(PathBuf, String)>,
_ctx: &mut Context,
) -> Text {
Text::new(TextFragment::from(string))
@ -64,7 +67,7 @@ fn string_to_text(
#[cfg(feature = "unicode_support")]
fn string_to_text(
string: String,
loaded_fonts: &mut Vec<(PathBuf, Font)>,
loaded_fonts: &mut Vec<(PathBuf, String)>,
ctx: &mut Context,
) -> Text {
use super::unicode_support;
@ -79,7 +82,7 @@ fn string_to_text(
}
let find_font =
|c: char, loaded_fonts: &mut Vec<(PathBuf, Font)>, ctx: &mut Context| -> Option<usize> {
|c: char, loaded_fonts: &mut Vec<(PathBuf, String)>, ctx: &mut Context| -> Option<usize> {
for (idx, (path, _)) in loaded_fonts.iter().enumerate() {
let result = unicode_support::font_has_char(c, path);
if result.is_ok() && result.unwrap() {
@ -89,9 +92,16 @@ fn string_to_text(
let find_result = unicode_support::get_matching_font_from_char(c);
if let Ok(path) = find_result {
let new_font = Font::new(ctx, &path);
let new_font = ggez::graphics::FontData::from_path(ctx, &path);
if let Ok(font) = new_font {
loaded_fonts.push((path, font));
let font_name: String = path
.file_name()
.expect("Should be valid filename at end of Font path.")
.to_str()
.expect("Font filename should be valid unicode.")
.to_owned();
ctx.gfx.add_font(&font_name, font);
loaded_fonts.push((path, font_name));
return Some(loaded_fonts.len() - 1);
} else {
log(
@ -102,7 +112,7 @@ fn string_to_text(
}
} else {
log(
format!("Failed to find font for {}", c),
format!("Failed to find font for {c}"),
debug_log::LogState::Error,
debug_log::LogLevel::Error,
);
@ -132,13 +142,13 @@ fn string_to_text(
text.add(current_fragment);
current_fragment = Default::default();
}
let (_, font) = loaded_fonts[idx];
current_fragment.font = Some(font);
let (_, font) = &loaded_fonts[idx];
current_fragment.font = Some(font.clone());
}
current_fragment.text.push(c);
} else if let Some(idx) = idx_opt {
let font = loaded_fonts[idx].1;
if let Some(current_font) = current_fragment.font {
let font = &loaded_fonts[idx].1;
if let Some(current_font) = current_fragment.font.as_ref() {
if current_font == font {
current_fragment.text.push(c);
} else {
@ -147,17 +157,17 @@ fn string_to_text(
current_fragment = Default::default();
}
current_fragment.text.push(c);
current_fragment.font = Some(font);
current_fragment.font = Some(font.clone());
}
} else if current_fragment.text.is_empty() {
current_fragment.text.push(c);
current_fragment.font = Some(font);
current_fragment.font = Some(font.clone());
} else {
text.add(current_fragment);
current_fragment = Default::default();
current_fragment.text.push(c);
current_fragment.font = Some(font);
current_fragment.font = Some(font.clone());
}
} else {
if !current_fragment.text.is_empty() && current_fragment.font.is_some() {
@ -203,16 +213,22 @@ pub struct MPDDisplay {
album_string_cache: String,
album_transform: Transform,
timer_text: Text,
timer_text_len: usize,
timer_transform: Transform,
timer_x: f32,
timer_y: f32,
timer: f64,
length: f64,
cached_filename_y: f32,
cached_album_y: f32,
cached_artist_y: f32,
cached_title_y: f32,
cached_timer_y: f32,
text_bg_mesh: Option<Mesh>,
hide_text: bool,
tried_album_art_in_dir: bool,
mpd_play_state: MPDPlayState,
loaded_fonts: Vec<(PathBuf, Font)>,
loaded_fonts: Vec<(PathBuf, String)>,
}
impl MPDDisplay {
@ -224,7 +240,7 @@ impl MPDDisplay {
is_initialized: false,
is_authenticated: false,
notice_text: Text::default(),
poll_instant: Instant::now() - POLL_TIME,
poll_instant: Instant::now().checked_sub(POLL_TIME).unwrap(),
shared: None,
password_entered: false,
dirty_flag: None,
@ -237,11 +253,17 @@ impl MPDDisplay {
title_text: Text::default(),
title_transform: Transform::default(),
timer_text: Text::new("0"),
timer_text_len: 0,
timer_transform: Transform::default(),
timer_x: INIT_FONT_SIZE_X,
timer_y: INIT_FONT_SIZE_Y,
timer: 0.0,
length: 0.0,
cached_filename_y: 0.0f32,
cached_album_y: 0.0f32,
cached_artist_y: 0.0f32,
cached_title_y: 0.0f32,
cached_timer_y: 0.0f32,
text_bg_mesh: None,
hide_text: false,
tried_album_art_in_dir: false,
@ -291,24 +313,24 @@ impl MPDDisplay {
fn get_album_art_transform(&mut self, ctx: &mut Context, fill_scaled: bool) {
if fill_scaled {
if let Some(image) = &self.album_art {
let screen_coords: Rect = graphics::screen_coordinates(ctx);
let art_rect: Rect = image.dimensions();
let drawable_size = ctx.gfx.drawable_size();
let art_rect: Rect = image.dimensions(ctx).expect("Image should have dimensions");
// try to fit to width first
let mut x_scale = screen_coords.w / art_rect.w;
let mut x_scale = drawable_size.0 / art_rect.w;
let mut y_scale = x_scale;
let mut new_width = art_rect.w * x_scale;
let mut new_height = art_rect.h * y_scale;
if new_height > screen_coords.h.abs() {
if new_height > drawable_size.1.abs() {
// fit to height instead
y_scale = screen_coords.h.abs() / art_rect.h;
y_scale = drawable_size.1.abs() / art_rect.h;
x_scale = y_scale;
new_width = art_rect.w * x_scale;
new_height = art_rect.h * y_scale;
}
let offset_x: f32 = (screen_coords.w.abs() - new_width) / 2.0f32;
let offset_y: f32 = (screen_coords.h.abs() - new_height) / 2.0f32;
let offset_x: f32 = (drawable_size.0.abs() - new_width) / 2.0f32;
let offset_y: f32 = (drawable_size.1.abs() - new_height) / 2.0f32;
self.album_art_draw_transform = Some(Transform::Values {
dest: [offset_x, offset_y].into(),
@ -320,10 +342,10 @@ impl MPDDisplay {
self.album_art_draw_transform = None;
}
} else if let Some(image) = &self.album_art {
let screen_coords: Rect = graphics::screen_coordinates(ctx);
let art_rect: Rect = image.dimensions();
let offset_x: f32 = (screen_coords.w.abs() - art_rect.w.abs()) / 2.0f32;
let offset_y: f32 = (screen_coords.h.abs() - art_rect.h.abs()) / 2.0f32;
let drawable_size = ctx.gfx.drawable_size();
let art_rect: Rect = image.dimensions(ctx).expect("Image should have dimensions");
let offset_x: f32 = (drawable_size.0.abs() - art_rect.w.abs()) / 2.0f32;
let offset_y: f32 = (drawable_size.1.abs() - art_rect.h.abs()) / 2.0f32;
self.album_art_draw_transform = Some(Transform::Values {
dest: [offset_x, offset_y].into(),
rotation: 0.0f32,
@ -398,20 +420,23 @@ impl MPDDisplay {
}
let img_result = if is_unknown_format {
let mut reader = ImageReader::new(Cursor::new(&image_ref));
reader = reader
let reader = ImageReader::new(Cursor::new(image_ref));
let guessed_reader = reader
.with_guessed_format()
.map_err(|e| format!("Error: Failed to guess format of album art image: {}", e))?;
reader.decode().map_err(|e| {
format!(
"Error: Failed to decode album art image (guessed format): {}",
e
)
})
.map_err(|e| format!("Error: Failed to guess format of album art image: {e}"));
if let Ok(reader) = guessed_reader {
reader.decode().map_err(|e| {
format!("Error: Failed to decode album art image (guessed format): {e}")
})
} else {
// Convert Ok(_) to Ok(DynamicImage) which will never be used
// since the if statement covers it.
guessed_reader.map(|_| -> DynamicImage { unreachable!() })
}
} else {
ImageReader::with_format(Cursor::new(&image_ref), image_format)
ImageReader::with_format(Cursor::new(image_ref), image_format)
.decode()
.map_err(|e| format!("Error: Failed to decode album art image: {}", e))
.map_err(|e| format!("Error: Failed to decode album art image: {e}"))
};
if img_result.is_err() && !self.tried_album_art_in_dir {
return try_second_art_fetch_method(
@ -423,13 +448,13 @@ impl MPDDisplay {
}
let img = img_result?;
let rgba8 = img.to_rgba8();
let ggez_img = Image::from_rgba8(
let ggez_img = Image::from_pixels(
ctx,
rgba8.width() as u16,
rgba8.height() as u16,
rgba8.as_raw(),
)
.map_err(|e| format!("Error: Failed to load album art image in ggez Image: {}", e))?;
wgpu::TextureFormat::Rgba8UnormSrgb,
rgba8.width(),
rgba8.height(),
);
self.album_art = Some(ggez_img);
@ -437,20 +462,14 @@ impl MPDDisplay {
}
fn refresh_text_transforms(&mut self, ctx: &mut Context) -> GameResult<()> {
let screen_coords: Rect = graphics::screen_coordinates(ctx);
let drawable_size = ctx.gfx.drawable_size();
let text_height_limit = TEXT_HEIGHT_SCALE * screen_coords.h.abs();
let album_height_limit = ALBUM_HEIGHT_SCALE * screen_coords.h.abs();
let artist_height_limit = ARTIST_HEIGHT_SCALE * screen_coords.h.abs();
let timer_height = TIMER_HEIGHT_SCALE * screen_coords.h.abs();
let text_height_limit = TEXT_HEIGHT_SCALE * drawable_size.1.abs();
let album_height_limit = ALBUM_HEIGHT_SCALE * drawable_size.1.abs();
let artist_height_limit = ARTIST_HEIGHT_SCALE * drawable_size.1.abs();
let timer_height = TIMER_HEIGHT_SCALE * drawable_size.1.abs();
let mut offset_y: f32 = screen_coords.h;
let mut filename_y: f32 = 0.0;
let mut album_y: f32 = 0.0;
let mut artist_y: f32 = 0.0;
let mut title_y: f32 = 0.0;
let mut timer_y: f32 = 0.0;
let mut offset_y: f32 = drawable_size.1;
let set_transform = |text: &mut Text,
transform: &mut Transform,
@ -463,8 +482,7 @@ impl MPDDisplay {
timer_y: &mut f32| {
let mut current_x = INIT_FONT_SIZE_X;
let mut current_y = INIT_FONT_SIZE_Y;
let mut width: f32;
let mut height: f32 = 0.0;
let mut width_height: Vector2<f32> = Vector2 { x: 0.0, y: 0.0 };
let mut iteration_count: u8 = 0;
loop {
iteration_count += 1;
@ -478,12 +496,13 @@ impl MPDDisplay {
y: current_y,
});
}
width = text.width(ctx);
height = text.height(ctx);
width_height = text
.measure(ctx)
.expect("Should be able to get width/height of text.");
if is_string {
if screen_coords.w < width
|| height
if drawable_size.0 < width_height.x
|| width_height.y
>= (if is_artist {
artist_height_limit
} else if is_album {
@ -495,7 +514,7 @@ impl MPDDisplay {
current_x *= DECREASE_AMT;
current_y *= DECREASE_AMT;
continue;
} else if screen_coords.w * MIN_WIDTH_RATIO > width {
} else if drawable_size.0 * MIN_WIDTH_RATIO > width_height.x {
current_x *= INCREASE_AMT;
current_y *= INCREASE_AMT;
continue;
@ -503,7 +522,7 @@ impl MPDDisplay {
break;
}
} else {
let diff_scale_y = current_y / height * timer_height;
let diff_scale_y = current_y / width_height.y * timer_height;
let current_x = current_x * diff_scale_y / current_y;
for fragment in text.fragments_mut() {
fragment.scale = Some(PxScale {
@ -514,20 +533,23 @@ impl MPDDisplay {
*timer_x = current_x;
*timer_y = diff_scale_y;
// width = text.width(ctx); // not really used after this
height = text.height(ctx);
width_height.y = text
.measure(ctx)
.expect("Should be able to get width/height of text.")
.y;
break;
}
}
*y = *offset_y - height;
*y = *offset_y - width_height.y;
*transform = Transform::Values {
dest: [TEXT_X_OFFSET, *offset_y - height].into(),
dest: [TEXT_X_OFFSET, *offset_y - width_height.y].into(),
rotation: 0.0,
scale: [1.0, 1.0].into(),
offset: [0.0, 0.0].into(),
};
*offset_y -= height + TEXT_OFFSET_Y_SPACING;
*offset_y -= width_height.y + TEXT_OFFSET_Y_SPACING;
};
if !self.filename_text.contents().is_empty() && !self.opts.disable_show_filename {
@ -535,7 +557,7 @@ impl MPDDisplay {
&mut self.filename_text,
&mut self.filename_transform,
&mut offset_y,
&mut filename_y,
&mut self.cached_filename_y,
true,
false,
false,
@ -555,7 +577,7 @@ impl MPDDisplay {
&mut self.album_text,
&mut self.album_transform,
&mut offset_y,
&mut album_y,
&mut self.cached_album_y,
true,
false,
true,
@ -569,7 +591,7 @@ impl MPDDisplay {
&mut self.artist_text,
&mut self.artist_transform,
&mut offset_y,
&mut artist_y,
&mut self.cached_artist_y,
true,
true,
false,
@ -589,7 +611,7 @@ impl MPDDisplay {
&mut self.title_text,
&mut self.title_transform,
&mut offset_y,
&mut title_y,
&mut self.cached_title_y,
true,
false,
false,
@ -608,7 +630,7 @@ impl MPDDisplay {
&mut self.timer_text,
&mut self.timer_transform,
&mut offset_y,
&mut timer_y,
&mut self.cached_timer_y,
false,
false,
false,
@ -616,11 +638,32 @@ impl MPDDisplay {
&mut self.timer_y,
);
let filename_dimensions = self.filename_text.dimensions(ctx);
let album_dimensions = self.album_text.dimensions(ctx);
let artist_dimensions = self.artist_text.dimensions(ctx);
let title_dimensions = self.title_text.dimensions(ctx);
let timer_dimensions = self.timer_text.dimensions(ctx);
self.update_bg_mesh(ctx)?;
Ok(())
}
fn update_bg_mesh(&mut self, ctx: &mut Context) -> GameResult<()> {
let filename_dimensions = self
.filename_text
.dimensions(ctx)
.expect("Should be able to get dimensions of Text.");
let album_dimensions = self
.album_text
.dimensions(ctx)
.expect("Should be able to get dimensions of Text.");
let artist_dimensions = self
.artist_text
.dimensions(ctx)
.expect("Should be able to get dimensions of Text.");
let title_dimensions = self
.title_text
.dimensions(ctx)
.expect("Should be able to get dimensions of Text.");
let timer_dimensions = self
.timer_text
.dimensions(ctx)
.expect("Should be able to get dimensions of Text.");
let mut mesh_builder: MeshBuilder = MeshBuilder::new();
if !self.opts.disable_show_filename {
@ -628,7 +671,7 @@ impl MPDDisplay {
DrawMode::fill(),
Rect {
x: TEXT_X_OFFSET,
y: filename_y,
y: self.cached_filename_y,
w: filename_dimensions.w,
h: filename_dimensions.h,
},
@ -640,7 +683,7 @@ impl MPDDisplay {
DrawMode::fill(),
Rect {
x: TEXT_X_OFFSET,
y: album_y,
y: self.cached_album_y,
w: album_dimensions.w,
h: album_dimensions.h,
},
@ -652,7 +695,7 @@ impl MPDDisplay {
DrawMode::fill(),
Rect {
x: TEXT_X_OFFSET,
y: artist_y,
y: self.cached_artist_y,
w: artist_dimensions.w,
h: artist_dimensions.h,
},
@ -664,25 +707,28 @@ impl MPDDisplay {
DrawMode::fill(),
Rect {
x: TEXT_X_OFFSET,
y: title_y,
y: self.cached_title_y,
w: title_dimensions.w,
h: title_dimensions.h,
},
Color::from_rgba(0, 0, 0, self.opts.text_bg_opacity),
)?;
}
let mesh: Mesh = mesh_builder
.rectangle(
DrawMode::fill(),
Rect {
x: TEXT_X_OFFSET,
y: timer_y,
w: timer_dimensions.w,
h: timer_dimensions.h,
},
Color::from_rgba(0, 0, 0, self.opts.text_bg_opacity),
)?
.build(ctx)?;
let mesh: Mesh = Mesh::from_data(
ctx,
mesh_builder
.rectangle(
DrawMode::fill(),
Rect {
x: TEXT_X_OFFSET,
y: self.cached_timer_y,
w: timer_dimensions.w,
h: timer_dimensions.h,
},
Color::from_rgba(0, 0, 0, self.opts.text_bg_opacity),
)?
.build(),
);
self.text_bg_mesh = Some(mesh);
@ -695,8 +741,7 @@ impl EventHandler for MPDDisplay {
if !self.is_valid {
if let Err(mpd_handler_error) = &self.mpd_handler {
return Err(GameError::EventLoopError(format!(
"Failed to initialize MPDHandler: {}",
mpd_handler_error
"Failed to initialize MPDHandler: {mpd_handler_error}"
)));
} else {
return Err(GameError::EventLoopError(
@ -882,102 +927,104 @@ impl EventHandler for MPDDisplay {
}
}
let delta = timer::delta(ctx);
let delta = ctx.time.delta();
self.timer += delta.as_secs_f64();
let timer_diff = seconds_to_time(self.length - self.timer);
let timer_diff_len = timer_diff.len();
self.timer_text = Text::new(timer_diff);
self.timer_text.set_font(
Font::default(),
PxScale {
x: self.timer_x,
y: self.timer_y,
},
);
self.timer_text.set_scale(PxScale {
x: self.timer_x,
y: self.timer_y,
});
if timer_diff_len != self.timer_text_len {
self.timer_text_len = timer_diff_len;
self.update_bg_mesh(ctx)?;
}
Ok(())
}
fn draw(&mut self, ctx: &mut ggez::Context) -> Result<(), GameError> {
graphics::clear(ctx, Color::BLACK);
let mut canvas = graphics::Canvas::from_frame(ctx, Color::BLACK);
if self.mpd_play_state != MPDPlayState::Stopped
&& self.album_art.is_some()
&& self.album_art_draw_transform.is_some()
{
self.album_art.as_ref().unwrap().draw(
ctx,
canvas.draw(
self.album_art.as_ref().unwrap(),
DrawParam {
trans: self.album_art_draw_transform.unwrap(),
transform: self.album_art_draw_transform.unwrap(),
..Default::default()
},
)?;
);
}
if !self.hide_text {
self.notice_text.draw(ctx, DrawParam::default())?;
canvas.draw(&self.notice_text, DrawParam::default());
if self.mpd_play_state != MPDPlayState::Stopped && self.is_valid && self.is_initialized
{
if let Some(mesh) = &self.text_bg_mesh {
mesh.draw(ctx, DrawParam::default())?;
canvas.draw(mesh, DrawParam::default());
}
if !self.opts.disable_show_filename {
self.filename_text.draw(
ctx,
canvas.draw(
&self.filename_text,
DrawParam {
trans: self.filename_transform,
transform: self.filename_transform,
..Default::default()
},
)?;
);
}
if !self.opts.disable_show_album {
self.album_text.draw(
ctx,
canvas.draw(
&self.album_text,
DrawParam {
trans: self.album_transform,
transform: self.album_transform,
..Default::default()
},
)?;
);
}
if !self.opts.disable_show_artist {
self.artist_text.draw(
ctx,
canvas.draw(
&self.artist_text,
DrawParam {
trans: self.artist_transform,
transform: self.artist_transform,
..Default::default()
},
)?;
);
}
if !self.opts.disable_show_title {
self.title_text.draw(
ctx,
canvas.draw(
&self.title_text,
DrawParam {
trans: self.title_transform,
transform: self.title_transform,
..Default::default()
},
)?;
);
}
if self.mpd_play_state == MPDPlayState::Playing {
self.timer_text.draw(
ctx,
canvas.draw(
&self.timer_text,
DrawParam {
trans: self.timer_transform,
transform: self.timer_transform,
..Default::default()
},
)?;
);
}
}
}
graphics::present(ctx)
canvas.finish(ctx)
}
fn text_input_event(&mut self, _ctx: &mut Context, character: char) {
fn text_input_event(&mut self, _ctx: &mut Context, character: char) -> Result<(), GameError> {
if !self.is_initialized && self.opts.enable_prompt_password && !character.is_control() {
if self.opts.password.is_none() {
let s = String::from(character);
@ -988,17 +1035,18 @@ impl EventHandler for MPDDisplay {
self.notice_text.add('*');
}
}
Ok(())
}
fn key_down_event(
&mut self,
_ctx: &mut Context,
keycode: event::KeyCode,
_keymods: event::KeyMods,
input: KeyInput,
_repeat: bool,
) {
) -> Result<(), GameError> {
if !self.is_initialized && self.opts.enable_prompt_password {
if keycode == event::KeyCode::Back {
if input.keycode == Some(keyboard::KeyCode::Back) {
let s: String = self.notice_text.contents();
if s.ends_with('*') {
@ -1008,28 +1056,34 @@ impl EventHandler for MPDDisplay {
if let Some(input_p) = &mut self.opts.password {
input_p.pop();
}
} else if keycode == event::KeyCode::Return {
} else if input.keycode == Some(keyboard::KeyCode::Return) {
self.password_entered = true;
}
} else if keycode == event::KeyCode::H {
} else if input.keycode == Some(keyboard::KeyCode::H) {
self.hide_text = true;
}
Ok(())
}
fn key_up_event(
&mut self,
_ctx: &mut Context,
keycode: event::KeyCode,
_keymods: event::KeyMods,
) {
if keycode == event::KeyCode::H {
fn key_up_event(&mut self, _ctx: &mut Context, input: KeyInput) -> Result<(), GameError> {
if input.keycode == Some(keyboard::KeyCode::H) {
self.hide_text = false;
}
Ok(())
}
fn resize_event(&mut self, ctx: &mut Context, _width: f32, _height: f32) {
fn resize_event(
&mut self,
ctx: &mut Context,
_width: f32,
_height: f32,
) -> Result<(), GameError> {
self.get_album_art_transform(ctx, !self.opts.do_not_fill_scale_album_art);
self.refresh_text_transforms(ctx)
.expect("Failed to set text transforms");
Ok(())
}
}

View file

@ -7,49 +7,50 @@ mod unicode_support;
use ggez::conf::{WindowMode, WindowSetup};
use ggez::event::winit_event::{ElementState, KeyboardInput, ModifiersState};
use ggez::event::{self, ControlFlow, EventHandler};
use ggez::filesystem::mount;
use ggez::graphics::{self, Rect};
use ggez::input::keyboard::{self, KeyInput};
use ggez::{ContextBuilder, GameError};
use std::fs::File;
use std::io::Read;
use std::net::Ipv4Addr;
use std::path::PathBuf;
use std::thread;
use std::time::{Duration, Instant};
use structopt::StructOpt;
use clap::Parser;
use debug_log::log;
#[derive(StructOpt, Debug, Clone)]
#[structopt(name = "mpd_info_screen")]
#[derive(Parser, Debug, Clone)]
#[command(author, version, about, long_about = None)]
pub struct Opt {
host: Ipv4Addr,
#[structopt(default_value = "6600")]
#[arg(default_value = "6600")]
port: u16,
#[structopt(short = "p")]
#[arg(short = 'p')]
password: Option<String>,
#[structopt(long = "disable-show-title", help = "disable title display")]
#[arg(long = "disable-show-title", help = "disable title display")]
disable_show_title: bool,
#[structopt(long = "disable-show-artist", help = "disable artist display")]
#[arg(long = "disable-show-artist", help = "disable artist display")]
disable_show_artist: bool,
#[structopt(long = "disable-show-album", help = "disable album display")]
#[arg(long = "disable-show-album", help = "disable album display")]
disable_show_album: bool,
#[structopt(long = "disable-show-filename", help = "disable filename display")]
#[arg(long = "disable-show-filename", help = "disable filename display")]
disable_show_filename: bool,
#[structopt(long = "pprompt", help = "input password via prompt")]
#[arg(long = "pprompt", help = "input password via prompt")]
enable_prompt_password: bool,
#[structopt(
#[arg(long = "pfile", help = "read password from file")]
password_file: Option<PathBuf>,
#[arg(
long = "no-scale-fill",
help = "don't scale-fill the album art to the window"
)]
do_not_fill_scale_album_art: bool,
#[structopt(
short = "l",
#[arg(
short = 'l',
long = "log-level",
possible_values = &debug_log::LogLevel::variants(),
default_value = "Error",
case_insensitive = true,
default_value = "error",
)]
log_level: debug_log::LogLevel,
#[structopt(
#[arg(
short,
long,
help = "sets the opacity of the text background (0-255)",
@ -59,9 +60,26 @@ pub struct Opt {
}
fn main() -> Result<(), String> {
let opt = Opt::from_args();
let mut opt = Opt::parse();
println!("Got host addr == {}, port == {}", opt.host, opt.port);
// Read password from file if exists, error otherwise.
if let Some(psswd_file_path) = opt.password_file.as_ref() {
let mut file = File::open(psswd_file_path).expect("pfile/password_file should exist");
let mut content: String = String::new();
file.read_to_string(&mut content)
.expect("Should be able to read from pfile/password_file");
if content.ends_with("\r\n") {
content.truncate(content.len() - 2);
} else if content.ends_with('\n') {
content.truncate(content.len() - 1);
}
opt.password = Some(content);
}
let (mut ctx, event_loop) = ContextBuilder::new("mpd_info_screen", "Stephen Seo")
.window_setup(WindowSetup {
title: "mpd info screen".into(),
@ -75,7 +93,7 @@ fn main() -> Result<(), String> {
.expect("Failed to create ggez context");
// mount "/" read-only so that fonts can be loaded via absolute paths
mount(&mut ctx, &PathBuf::from("/"), true);
ctx.fs.mount(&PathBuf::from("/"), true);
let mut display = display::MPDDisplay::new(&mut ctx, opt.clone());
@ -94,7 +112,7 @@ fn main() -> Result<(), String> {
event::process_event(ctx, &mut event);
match event {
event::winit_event::Event::WindowEvent { event, .. } => match event {
event::winit_event::WindowEvent::CloseRequested => event::quit(ctx),
event::winit_event::WindowEvent::CloseRequested => ctx.request_quit(),
event::winit_event::WindowEvent::ModifiersChanged(state) => {
modifiers_state = state;
}
@ -108,40 +126,37 @@ fn main() -> Result<(), String> {
},
is_synthetic: _,
} => {
if keycode == event::KeyCode::Escape {
if keycode == keyboard::KeyCode::Escape {
*control_flow = ControlFlow::Exit;
return;
}
let ki = KeyInput {
scancode: 0,
keycode: Some(keycode),
mods: From::from(modifiers_state),
};
if state == ElementState::Pressed {
display.key_down_event(ctx, keycode, modifiers_state.into(), false);
display.key_down_event(ctx, ki, false).ok();
} else {
display.key_up_event(ctx, keycode, modifiers_state.into());
display.key_up_event(ctx, ki).ok();
}
}
event::winit_event::WindowEvent::Resized(phys_size) => {
graphics::set_screen_coordinates(
ctx,
Rect {
x: 0.0,
y: 0.0,
w: phys_size.width as f32,
h: phys_size.height as f32,
},
)
.expect("Failed to handle resizing window");
display.resize_event(ctx, phys_size.width as f32, phys_size.height as f32);
display
.resize_event(ctx, phys_size.width as f32, phys_size.height as f32)
.ok();
}
event::winit_event::WindowEvent::ReceivedCharacter(ch) => {
display.text_input_event(ctx, ch);
display.text_input_event(ctx, ch).ok();
}
x => log(
format!("Other window event fired: {:?}", x),
format!("Other window event fired: {x:?}"),
debug_log::LogState::Verbose,
opt.log_level,
),
},
event::winit_event::Event::MainEventsCleared => {
ctx.timer_context.tick();
ctx.time.tick();
let mut game_result: Result<(), GameError> = display.update(ctx);
if game_result.is_err() {
@ -149,21 +164,24 @@ fn main() -> Result<(), String> {
*control_flow = ControlFlow::Exit;
return;
}
ctx.gfx.begin_frame().unwrap();
game_result = display.draw(ctx);
if game_result.is_err() {
println!("Error draw: {}", game_result.unwrap_err());
*control_flow = ControlFlow::Exit;
return;
}
ctx.gfx.end_frame().unwrap();
ctx.mouse_context.reset_delta();
ctx.mouse.reset_delta();
// sleep to force ~5 fps
thread::sleep(Duration::from_millis(200));
ggez::timer::yield_now();
}
x => log(
format!("Device event fired: {:?}", x),
format!("Device event fired: {x:?}"),
debug_log::LogState::Verbose,
opt.log_level,
),

View file

@ -250,6 +250,8 @@ impl MPDHandler {
)
.map_err(|_| String::from("Failed to get TCP connection (is MPD running?)"))?;
let password_is_empty = password.is_empty();
let s = MPDHandler {
state: Arc::new(RwLock::new(MPDHandlerState {
art_data: Vec::new(),
@ -266,7 +268,7 @@ impl MPDHandler {
password,
error_text: String::new(),
can_authenticate: true,
is_authenticated: false,
is_authenticated: password_is_empty,
can_get_album_art: true,
can_get_album_art_in_dir: true,
can_get_status: true,
@ -274,9 +276,9 @@ impl MPDHandler {
did_check_overtime: false,
force_get_status: false,
force_get_current_song: false,
song_title_get_time: Instant::now() - Duration::from_secs(10),
song_pos_get_time: Instant::now() - Duration::from_secs(10),
song_length_get_time: Instant::now() - Duration::from_secs(10),
song_title_get_time: Instant::now().checked_sub(Duration::from_secs(10)).unwrap(),
song_pos_get_time: Instant::now().checked_sub(Duration::from_secs(10)).unwrap(),
song_length_get_time: Instant::now().checked_sub(Duration::from_secs(10)).unwrap(),
self_thread: None,
dirty_flag: Arc::new(AtomicBool::new(true)),
stop_flag: Arc::new(AtomicBool::new(false)),
@ -426,13 +428,13 @@ impl MPDHandler {
if let Err(err_string) = self.handler_read_block(&mut buf, &mut saved, &mut saved_str) {
log(
format!("read_block error: {}", err_string),
format!("read_block error: {err_string}"),
LogState::Warning,
log_level,
);
} else if let Err(err_string) = self.handler_write_block() {
log(
format!("write_block error: {}", err_string),
format!("write_block error: {err_string}"),
LogState::Warning,
log_level,
);
@ -477,7 +479,7 @@ impl MPDHandler {
let read_result = write_handle.stream.read(buf);
if let Err(io_err) = read_result {
if io_err.kind() != io::ErrorKind::WouldBlock {
return Err(format!("TCP stream error: {}", io_err));
return Err(format!("TCP stream error: {io_err}"));
} else {
return Ok(());
}
@ -649,9 +651,9 @@ impl MPDHandler {
MPDPlayState::Paused
};
write_handle.error_text.clear();
write!(&mut write_handle.error_text, "MPD has {:?}", got_mpd_state).ok();
write!(&mut write_handle.error_text, "MPD has {got_mpd_state:?}").ok();
log(
format!("MPD is {:?}", got_mpd_state),
format!("MPD is {got_mpd_state:?}"),
LogState::Warning,
write_handle.log_level,
);
@ -736,7 +738,7 @@ impl MPDHandler {
write_handle.art_data_type = line.split_off(6);
} else {
log(
format!("Got unrecognized/ignored line: {}", line),
format!("Got unrecognized/ignored line: {line}"),
LogState::Warning,
write_handle.log_level,
);
@ -799,12 +801,12 @@ impl MPDHandler {
let p = write_handle.password.clone();
let write_result = write_handle
.stream
.write(format!("password {}\n", p).as_bytes());
.write(format!("password {p}\n").as_bytes());
if write_result.is_ok() {
write_handle.poll_state = PollState::Password;
} else if let Err(e) = write_result {
log(
format!("Failed to send password for authentication: {}", e),
format!("Failed to send password for authentication: {e}"),
LogState::Error,
write_handle.log_level,
);
@ -820,7 +822,7 @@ impl MPDHandler {
write_handle.poll_state = PollState::CurrentSong;
} else if let Err(e) = write_result {
log(
format!("Failed to request song info over stream: {}", e),
format!("Failed to request song info over stream: {e}"),
LogState::Error,
write_handle.log_level,
);
@ -836,7 +838,7 @@ impl MPDHandler {
write_handle.poll_state = PollState::Status;
} else if let Err(e) = write_result {
log(
format!("Failed to request status over stream: {}", e),
format!("Failed to request status over stream: {e}"),
LogState::Error,
write_handle.log_level,
);
@ -848,14 +850,14 @@ impl MPDHandler {
let title = write_handle.current_song_filename.clone();
let art_data_length = write_handle.art_data.len();
if write_handle.can_get_album_art {
let write_result = write_handle.stream.write(
format!("readpicture \"{}\" {}\n", title, art_data_length).as_bytes(),
);
let write_result = write_handle
.stream
.write(format!("readpicture \"{title}\" {art_data_length}\n").as_bytes());
if write_result.is_ok() {
write_handle.poll_state = PollState::ReadPicture;
} else if let Err(e) = write_result {
log(
format!("Failed to request album art: {}", e),
format!("Failed to request album art: {e}"),
LogState::Error,
write_handle.log_level,
);
@ -863,12 +865,12 @@ impl MPDHandler {
} else if write_handle.can_get_album_art_in_dir {
let write_result = write_handle
.stream
.write(format!("albumart \"{}\" {}\n", title, art_data_length).as_bytes());
.write(format!("albumart \"{title}\" {art_data_length}\n").as_bytes());
if write_result.is_ok() {
write_handle.poll_state = PollState::ReadPictureInDir;
} else if let Err(e) = write_result {
log(
format!("Failed to request album art in dir: {}", e),
format!("Failed to request album art in dir: {e}"),
LogState::Error,
write_handle.log_level,
);

View file

@ -67,8 +67,7 @@ mod ffi {
return Err(String::from("Failed to FcFontMatch (FcResult is not FcResultMatch; result_pattern is not null)"));
} else {
return Err(format!(
"Failed to FcFontMatch (FcResult is not FcResultMatch; {:?})",
result
"Failed to FcFontMatch (FcResult is not FcResultMatch; {result:?})"
));
}
} else if result_pattern.is_null() {