Compare commits

...

No commits in common. "main" and "images" have entirely different histories.
main ... images

15 changed files with 0 additions and 6331 deletions

2
.gitignore vendored
View file

@ -1,2 +0,0 @@
/target
/.idea

3352
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,23 +0,0 @@
[package]
name = "mpd_info_screen"
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]
clap = { version = "4.4", features = ["derive"] }
image = "0.24"
ggez = "0.9.3"
freetype = { version = "0.7", optional = true }
wgpu = "0.16"
[build-dependencies]
bindgen = { version = "0.69", optional = true }
[features]
unicode_support = ["dep:freetype", "dep:bindgen"]

21
LICENSE
View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021-2022 Stephen Seo
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

112
README.md
View file

@ -1,112 +0,0 @@
# mpd info screen
[![mpd info screen crates.io version badge](https://img.shields.io/crates/v/mpd_info_screen)](https://crates.io/crates/mpd_info_screen)
[![mpd info screen license badge](https://img.shields.io/github/license/Stephen-Seo/mpd_info_screen)](https://choosealicense.com/licenses/mit/)
[Github Repository](https://github.com/Stephen-Seo/mpd_info_screen)
![mpd info screen preview image](https://git.seodisparate.com/stephenseo/mpd_info_screen/raw/branch/images/images/mpd_info_screen_preview_image.jpg)
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
the `unicode_support` feature enabled to enable fetching fonts from the local
filesystem to display unicode characters properly (if the system is missing a
font, then it will still be displayed incorrectly). Note that your system must
have `fontconfig` and `freetype` installed (most Linux systems should have these
installed already).
cargo build --release --features unicode_support
or through crates.io:
cargo install --features unicode_support mpd_info_screen
# Usage
Displays info on currently playing music from an MPD daemon
Usage: mpd_info_screen [OPTIONS] <HOST> [PORT]
Arguments:
<HOST>
[PORT] [default: 6600]
Options:
-p <PASSWORD>
--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.
Also note that pressing the H key while displaying text will hide the text.
# Issues / TODO
- [x] UTF-8 Non-ascii font support (Use the `unicode_support` feature to enable; only tested in linux)
- [x] Support for album art not embedded but in the same directory
## MPD Version
To get album art from the image embedded with the audio file, the "readpicture"
protocol command is queried from MPD, which was added in version 0.22 of MPD.
It is uncertain when the "albumart" protocol command was added (this command
fetches album art that resides in cover.jpg/cover.png in the same directory as
the audio file). This means that older versions of MPD may not return album art
to display.
# Legal stuff
Uses dependency [ggez](https://github.com/ggez/ggez) which is licensed under the
MIT license.
Uses dependency [image](https://crates.io/crates/image) which is licensed under
MIT license.
Uses dependency [clap](https://crates.io/crates/clap) which is licensed under
Apache-2.0 or MIT licenses.
## Unicode Support Dependencies
Uses dependency
[fontconfig](https://www.freedesktop.org/wiki/Software/fontconfig/) which is
[licensed with this license](https://www.freedesktop.org/software/fontconfig/fontconfig-devel/ln12.html).
Uses dependency [freetype](https://freetype.org) which is
[licensed with this license](https://freetype.org/license.html).
Uses dependency [bindgen](https://crates.io/crates/bindgen) which is licenced
under the BSD-3-Clause.

View file

@ -1,25 +0,0 @@
#[cfg(feature = "unicode_support")]
use std::env;
#[cfg(feature = "unicode_support")]
use std::path::PathBuf;
#[cfg(not(feature = "unicode_support"))]
fn main() {}
#[cfg(feature = "unicode_support")]
fn main() {
println!("cargo:rustc-link-search=/usr/lib");
println!("cargo:rustc-link-lib=fontconfig");
println!("cargo:rerun-if-changed=src/bindgen_wrapper.h");
let bindings = bindgen::Builder::default()
.header("src/bindgen_wrapper.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("Unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("unicode_support_bindings.rs"))
.expect("Couldn't write bindings");
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

View file

@ -1 +0,0 @@
#include <fontconfig/fontconfig.h>

View file

@ -1,69 +0,0 @@
use std::fmt::Display;
use clap::ValueEnum;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum LogState {
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)
where
T: Display,
{
if state == LogState::Error {
log_error(msg);
} else if state == LogState::Warning {
if level != LogLevel::Error {
log_warning(msg);
}
} else if state == LogState::Debug {
if level == LogLevel::Debug || level == LogLevel::Verbose {
log_debug(msg);
}
} else if state == LogState::Verbose {
if level == LogLevel::Verbose {
log_verbose(msg);
}
} else {
unreachable!();
}
}
pub fn log_error<T>(msg: T)
where
T: Display,
{
println!("Error: {msg}");
}
pub fn log_warning<T>(msg: T)
where
T: Display,
{
println!("Warning: {msg}");
}
pub fn log_debug<T>(msg: T)
where
T: Display,
{
println!("Debug: {msg}");
}
pub fn log_verbose<T>(msg: T)
where
T: Display,
{
println!("Verbose: {msg}");
}

File diff suppressed because it is too large Load diff

View file

@ -1,190 +0,0 @@
mod debug_log;
mod display;
mod mpd_handler;
#[cfg(feature = "unicode_support")]
mod unicode_support;
use ggez::conf::{WindowMode, WindowSetup};
use ggez::event::winit_event::{ElementState, KeyboardInput, ModifiersState};
use ggez::event::{self, ControlFlow, EventHandler};
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 clap::Parser;
use debug_log::log;
#[derive(Parser, Debug, Clone)]
#[command(author, version, about, long_about = None)]
pub struct Opt {
host: Ipv4Addr,
#[arg(default_value = "6600")]
port: u16,
#[arg(short = 'p')]
password: Option<String>,
#[arg(long = "disable-show-title", help = "disable title display")]
disable_show_title: bool,
#[arg(long = "disable-show-artist", help = "disable artist display")]
disable_show_artist: bool,
#[arg(long = "disable-show-album", help = "disable album display")]
disable_show_album: bool,
#[arg(long = "disable-show-filename", help = "disable filename display")]
disable_show_filename: bool,
#[arg(long = "pprompt", help = "input password via prompt")]
enable_prompt_password: bool,
#[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,
#[arg(
short = 'l',
long = "log-level",
default_value = "error",
)]
log_level: debug_log::LogLevel,
#[arg(
short,
long,
help = "sets the opacity of the text background (0-255)",
default_value = "190"
)]
text_bg_opacity: u8,
}
fn main() -> Result<(), String> {
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(),
..Default::default()
})
.window_mode(WindowMode {
resizable: true,
..Default::default()
})
.build()
.expect("Failed to create ggez context");
// mount "/" read-only so that fonts can be loaded via absolute paths
ctx.fs.mount(&PathBuf::from("/"), true);
let mut display = display::MPDDisplay::new(&mut ctx, opt.clone());
let mut modifiers_state: ModifiersState = ModifiersState::default();
event_loop.run(move |mut event, _window_target, control_flow| {
if !ctx.continuing {
*control_flow = ControlFlow::Exit;
return;
}
*control_flow = ControlFlow::WaitUntil(Instant::now() + Duration::from_millis(100));
let ctx = &mut ctx;
event::process_event(ctx, &mut event);
match event {
event::winit_event::Event::WindowEvent { event, .. } => match event {
event::winit_event::WindowEvent::CloseRequested => ctx.request_quit(),
event::winit_event::WindowEvent::ModifiersChanged(state) => {
modifiers_state = state;
}
event::winit_event::WindowEvent::KeyboardInput {
device_id: _,
input:
KeyboardInput {
virtual_keycode: Some(keycode),
state,
..
},
is_synthetic: _,
} => {
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, ki, false).ok();
} else {
display.key_up_event(ctx, ki).ok();
}
}
event::winit_event::WindowEvent::Resized(phys_size) => {
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).ok();
}
x => log(
format!("Other window event fired: {x:?}"),
debug_log::LogState::Verbose,
opt.log_level,
),
},
event::winit_event::Event::MainEventsCleared => {
ctx.time.tick();
let mut game_result: Result<(), GameError> = display.update(ctx);
if game_result.is_err() {
println!("Error update: {}", game_result.unwrap_err());
*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.reset_delta();
// sleep to force ~5 fps
thread::sleep(Duration::from_millis(200));
ggez::timer::yield_now();
}
x => log(
format!("Device event fired: {x:?}"),
debug_log::LogState::Verbose,
opt.log_level,
),
}
});
}

View file

@ -1,918 +0,0 @@
use crate::debug_log::{log, LogLevel, LogState};
use std::fmt::Write;
use std::io::{self, Read, Write as IOWrite};
use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream};
use std::str::FromStr;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, RwLock, RwLockReadGuard};
use std::thread;
use std::time::{Duration, Instant};
const SLEEP_DURATION: Duration = Duration::from_millis(100);
const POLL_DURATION: Duration = Duration::from_secs(5);
const BUF_SIZE: usize = 1024 * 4;
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
enum PollState {
None,
Password,
CurrentSong,
Status,
ReadPicture,
ReadPictureInDir,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum MPDPlayState {
Playing,
Paused,
Stopped,
}
#[derive(Debug, Clone)]
pub struct InfoFromShared {
pub filename: String,
pub title: String,
pub artist: String,
pub album: String,
pub length: f64,
pub pos: f64,
pub error_text: String,
pub mpd_play_state: MPDPlayState,
}
#[derive(Clone)]
pub struct MPDHandler {
state: Arc<RwLock<MPDHandlerState>>,
}
type SelfThreadT = Option<Arc<Mutex<thread::JoinHandle<Result<(), String>>>>>;
pub struct MPDHandlerState {
art_data: Vec<u8>,
art_data_size: usize,
art_data_type: String,
current_song_filename: String,
current_song_title: String,
current_song_artist: String,
current_song_album: String,
current_song_length: f64,
current_song_position: f64,
current_binary_size: usize,
poll_state: PollState,
stream: TcpStream,
password: String,
error_text: String,
can_authenticate: bool,
is_authenticated: bool,
can_get_album_art: bool,
can_get_album_art_in_dir: bool,
can_get_status: bool,
is_init: bool,
did_check_overtime: bool,
force_get_status: bool,
force_get_current_song: bool,
song_title_get_time: Instant,
song_pos_get_time: Instant,
song_length_get_time: Instant,
self_thread: SelfThreadT,
dirty_flag: Arc<AtomicBool>,
pub stop_flag: Arc<AtomicBool>,
log_level: LogLevel,
mpd_play_state: MPDPlayState,
}
fn check_next_chars(
buf: &[u8],
idx: usize,
saved: &mut Vec<u8>,
) -> Result<(char, u8), (String, u8)> {
if idx >= buf.len() {
return Err((String::from("idx out of bounds"), 0u8));
}
if buf[idx] & 0b10000000 == 0 {
let result_str = String::from_utf8(vec![buf[idx]]);
if let Ok(mut s) = result_str {
let popped_char = s.pop();
if s.is_empty() {
Ok((popped_char.unwrap(), 1u8))
} else {
Err((String::from("Not one-byte UTF-8 char"), 0u8))
}
} else {
Err((String::from("Not one-byte UTF-8 char"), 0u8))
}
} else if buf[idx] & 0b11100000 == 0b11000000 {
if idx + 1 >= buf.len() {
saved.push(buf[idx]);
return Err((
String::from("Is two-byte UTF-8, but not enough bytes provided"),
1u8,
));
}
let result_str = String::from_utf8(vec![buf[idx], buf[idx + 1]]);
if let Ok(mut s) = result_str {
let popped_char = s.pop();
if s.is_empty() {
Ok((popped_char.unwrap(), 2u8))
} else {
Err((String::from("Not two-byte UTF-8 char"), 0u8))
}
} else {
Err((String::from("Not two-byte UTF-8 char"), 0u8))
}
} else if buf[idx] & 0b11110000 == 0b11100000 {
if idx + 2 >= buf.len() {
for c in buf.iter().skip(idx) {
saved.push(*c);
}
return Err((
String::from("Is three-byte UTF-8, but not enough bytes provided"),
(idx + 3 - buf.len()) as u8,
));
}
let result_str = String::from_utf8(vec![buf[idx], buf[idx + 1], buf[idx + 2]]);
if let Ok(mut s) = result_str {
let popped_char = s.pop();
if s.is_empty() {
Ok((popped_char.unwrap(), 3u8))
} else {
Err((String::from("Not three-byte UTF-8 char"), 0u8))
}
} else {
Err((String::from("Not three-byte UTF-8 char"), 0u8))
}
} else if buf[idx] & 0b11111000 == 0b11110000 {
if idx + 3 >= buf.len() {
for c in buf.iter().skip(idx) {
saved.push(*c);
}
return Err((
String::from("Is four-byte UTF-8, but not enough bytes provided"),
(idx + 4 - buf.len()) as u8,
));
}
let result_str = String::from_utf8(vec![buf[idx], buf[idx + 1], buf[idx + 2]]);
if let Ok(mut s) = result_str {
let popped_char = s.pop();
if s.is_empty() {
Ok((popped_char.unwrap(), 4u8))
} else {
Err((String::from("Not four-byte UTF-8 char"), 0u8))
}
} else {
Err((String::from("Not four-byte UTF-8 char"), 0u8))
}
} else {
Err((String::from("Invalid UTF-8 char"), 0u8))
}
}
fn read_line(
buf: &mut Vec<u8>,
saved: &mut Vec<u8>,
init: bool,
) -> Result<String, (String, String)> {
let count = buf.len();
let mut result = String::new();
if count == 0 {
return Err((
String::from("Empty string passed to read_line"),
String::new(),
));
}
let mut buf_to_read: Vec<u8> = Vec::with_capacity(saved.len() + buf.len());
if !saved.is_empty() {
buf_to_read.append(saved);
}
buf_to_read.append(buf);
let mut prev_three: Vec<char> = Vec::with_capacity(4);
let mut skip_count = 0;
for idx in 0..count {
if skip_count > 0 {
skip_count -= 1;
continue;
}
let next_char_result = check_next_chars(&buf_to_read, idx, saved);
if let Ok((c, s)) = next_char_result {
if !init {
prev_three.push(c);
if prev_three.len() > 3 {
prev_three.remove(0);
}
if ['O', 'K', '\n'] == prev_three.as_slice() && idx + 1 == count {
buf_to_read = buf_to_read.split_off(2);
result = String::from("OK");
buf.append(&mut buf_to_read);
//println!("Warning: OK was reached"); // DEBUG
return Ok(result);
}
}
if c == '\n' {
buf_to_read = buf_to_read.split_off(idx + s as usize);
buf.append(&mut buf_to_read);
return Ok(result);
}
result.push(c);
skip_count = s - 1;
} else if let Err((msg, count)) = next_char_result {
//println!("Error: {}", msg); // DEBUG
for i in 0..count {
saved.push(buf_to_read[idx + i as usize]);
}
buf_to_read = buf_to_read.split_off(idx);
buf.append(&mut buf_to_read);
return Err((msg, result));
} else {
unreachable!();
}
}
*saved = buf_to_read;
Err((String::from("Newline not reached"), result))
}
impl MPDHandler {
pub fn new(
host: Ipv4Addr,
port: u16,
password: String,
log_level: LogLevel,
) -> Result<Self, String> {
let stream = TcpStream::connect_timeout(
&SocketAddr::new(IpAddr::V4(host), port),
Duration::from_secs(5),
)
.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(),
art_data_size: 0,
art_data_type: String::new(),
current_song_filename: String::new(),
current_song_title: String::new(),
current_song_artist: String::new(),
current_song_length: 0.0,
current_song_position: 0.0,
current_binary_size: 0,
poll_state: PollState::None,
stream,
password,
error_text: String::new(),
can_authenticate: true,
is_authenticated: password_is_empty,
can_get_album_art: true,
can_get_album_art_in_dir: true,
can_get_status: true,
is_init: true,
did_check_overtime: false,
force_get_status: false,
force_get_current_song: false,
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)),
log_level,
mpd_play_state: MPDPlayState::Stopped,
current_song_album: String::new(),
})),
};
let s_clone = s.clone();
let thread = Arc::new(Mutex::new(thread::spawn(|| s_clone.handler_loop())));
loop {
if let Ok(mut write_handle) = s.state.try_write() {
write_handle.self_thread = Some(thread);
break;
} else {
thread::sleep(Duration::from_millis(1));
}
}
Ok(s)
}
pub fn get_mpd_handler_shared_state(&self) -> Result<InfoFromShared, ()> {
if let Ok(read_lock) = self.state.try_read() {
return Ok(InfoFromShared {
filename: read_lock.current_song_filename.clone(),
title: read_lock.current_song_title.clone(),
artist: read_lock.current_song_artist.clone(),
album: read_lock.current_song_album.clone(),
length: read_lock.current_song_length,
pos: read_lock.current_song_position
+ read_lock.song_pos_get_time.elapsed().as_secs_f64(),
error_text: read_lock.error_text.clone(),
mpd_play_state: read_lock.mpd_play_state,
});
}
Err(())
}
pub fn get_dirty_flag(&self) -> Result<Arc<AtomicBool>, ()> {
if let Ok(read_lock) = self.state.try_read() {
return Ok(read_lock.dirty_flag.clone());
}
Err(())
}
#[allow(dead_code)]
pub fn is_dirty(&self) -> Result<bool, ()> {
if let Ok(write_lock) = self.state.try_write() {
return Ok(write_lock.dirty_flag.swap(false, Ordering::Relaxed));
}
Err(())
}
#[allow(dead_code)]
pub fn force_get_current_song(&self) {
loop {
if let Ok(mut write_lock) = self.state.try_write() {
write_lock.force_get_current_song = true;
break;
} else {
thread::sleep(Duration::from_millis(10));
}
}
}
pub fn is_authenticated(&self) -> Result<bool, ()> {
let read_handle = self.state.try_read().map_err(|_| ())?;
Ok(read_handle.is_authenticated)
}
pub fn failed_to_authenticate(&self) -> Result<bool, ()> {
let read_handle = self.state.try_read().map_err(|_| ())?;
Ok(!read_handle.can_authenticate)
}
#[allow(dead_code)]
pub fn has_image_data(&self) -> Result<bool, ()> {
let read_handle = self.state.try_read().map_err(|_| ())?;
Ok(read_handle.is_art_data_ready())
}
pub fn get_state_read_guard(&self) -> Result<RwLockReadGuard<'_, MPDHandlerState>, ()> {
self.state.try_read().map_err(|_| ())
}
pub fn stop_thread(&self) -> Result<(), ()> {
let read_handle = self.state.try_read().map_err(|_| ())?;
read_handle.stop_flag.store(true, Ordering::Relaxed);
Ok(())
}
pub fn force_try_other_album_art(&self) -> Result<(), ()> {
let mut write_handle = self.state.try_write().map_err(|_| ())?;
write_handle.art_data.clear();
write_handle.art_data_size = 0;
write_handle.can_get_album_art = false;
write_handle.can_get_album_art_in_dir = true;
Ok(())
}
fn handler_loop(self) -> Result<(), String> {
let log_level = self
.state
.read()
.expect("Failed to get log_level")
.log_level;
let mut buf: [u8; BUF_SIZE] = [0; BUF_SIZE];
let mut saved: Vec<u8> = Vec::new();
let mut saved_str: String = String::new();
loop {
if let Ok(write_handle) = self.state.try_write() {
write_handle
.stream
.set_nonblocking(true)
.map_err(|_| String::from("Failed to set non-blocking on TCP stream"))?;
break;
} else {
thread::sleep(POLL_DURATION);
}
}
'main: loop {
if !self.is_reading_picture()
&& self.is_authenticated().unwrap_or(true)
&& !self.failed_to_authenticate().unwrap_or(false)
{
thread::sleep(SLEEP_DURATION);
if let Ok(write_handle) = self.state.try_write() {
if write_handle.self_thread.is_none() {
// main thread failed to store handle to this thread
log(
"MPDHandle thread stopping due to failed handle storage",
LogState::Error,
write_handle.log_level,
);
break 'main;
}
}
}
if let Err(err_string) = self.handler_read_block(&mut buf, &mut saved, &mut saved_str) {
log(
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}"),
LogState::Warning,
log_level,
);
}
if let Ok(read_handle) = self.state.try_read() {
if read_handle.stop_flag.load(Ordering::Relaxed) || !read_handle.can_authenticate {
break 'main;
}
}
io::stdout().flush().unwrap();
}
log(
"MPDHandler thread entering exit loop",
LogState::Debug,
log_level,
);
'exit: loop {
if let Ok(mut write_handle) = self.state.try_write() {
write_handle.self_thread = None;
break 'exit;
}
thread::sleep(SLEEP_DURATION);
}
Ok(())
}
fn handler_read_block(
&self,
buf: &mut [u8; BUF_SIZE],
saved: &mut Vec<u8>,
saved_str: &mut String,
) -> Result<(), String> {
let mut write_handle = self
.state
.try_write()
.map_err(|_| String::from("Failed to get MPDHandler write lock (read_block)"))?;
let mut read_amount: usize = 0;
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}"));
} else {
return Ok(());
}
} else if let Ok(read_amount_result) = read_result {
if read_amount_result == 0 {
return Err(String::from("Got zero bytes from TCP stream"));
}
read_amount = read_amount_result;
}
let mut buf_vec: Vec<u8> = Vec::from(&buf[0..read_amount]);
let mut got_mpd_state: MPDPlayState = MPDPlayState::Playing;
'handle_buf: loop {
if write_handle.current_binary_size > 0 {
if write_handle.current_binary_size <= buf_vec.len() {
let count = write_handle.current_binary_size;
write_handle.art_data.extend_from_slice(&buf_vec[0..count]);
buf_vec = buf_vec.split_off(count + 1);
write_handle.current_binary_size = 0;
write_handle.poll_state = PollState::None;
log(
format!(
"Album art recv progress: {}/{}",
write_handle.art_data.len(),
write_handle.art_data_size
),
LogState::Debug,
write_handle.log_level,
);
if write_handle.art_data.len() == write_handle.art_data_size {
write_handle.dirty_flag.store(true, Ordering::Relaxed);
}
} else {
write_handle.art_data.extend_from_slice(&buf_vec);
write_handle.current_binary_size -= buf_vec.len();
log(
format!(
"Album art recv progress: {}/{}",
write_handle.art_data.len(),
write_handle.art_data_size
),
LogState::Debug,
write_handle.log_level,
);
if write_handle.art_data.len() == write_handle.art_data_size {
write_handle.dirty_flag.store(true, Ordering::Relaxed);
}
break 'handle_buf;
}
}
let read_line_result = read_line(&mut buf_vec, saved, write_handle.is_init);
if let Ok(mut line) = read_line_result {
line = saved_str.clone() + &line;
*saved_str = String::new();
if write_handle.is_init {
if line.starts_with("OK MPD ") {
write_handle.is_init = false;
log(
"Got initial \"OK\" from MPD",
LogState::Debug,
write_handle.log_level,
);
write_handle.poll_state = PollState::None;
break 'handle_buf;
} else {
return Err(String::from("Did not get expected init message from MPD"));
}
} // write_handle.is_init
if line.starts_with("OK") {
log(
format!("Got OK when poll state is {:?}", write_handle.poll_state),
LogState::Debug,
write_handle.log_level,
);
match write_handle.poll_state {
PollState::Password => write_handle.is_authenticated = true,
PollState::ReadPicture => {
if write_handle.art_data.is_empty() {
write_handle.can_get_album_art = false;
write_handle.dirty_flag.store(true, Ordering::Relaxed);
log(
"No embedded album art",
LogState::Warning,
write_handle.log_level,
);
}
}
PollState::ReadPictureInDir => {
if write_handle.art_data.is_empty() {
write_handle.can_get_album_art_in_dir = false;
write_handle.dirty_flag.store(true, Ordering::Relaxed);
log(
"No album art in dir",
LogState::Warning,
write_handle.log_level,
);
}
}
_ => (),
}
write_handle.poll_state = PollState::None;
break 'handle_buf;
} else if line.starts_with("ACK") {
log(&line, LogState::Warning, write_handle.log_level);
match write_handle.poll_state {
PollState::Password => {
write_handle.can_authenticate = false;
write_handle.dirty_flag.store(true, Ordering::Relaxed);
write_handle.error_text = "Failed to authenticate to MPD".into();
write_handle.stop_flag.store(true, Ordering::Relaxed);
}
PollState::CurrentSong | PollState::Status => {
write_handle.can_get_status = false;
write_handle.dirty_flag.store(true, Ordering::Relaxed);
write_handle.error_text = "Failed to get MPD status".into();
if line.contains("don't have permission") {
write_handle.can_authenticate = false;
write_handle.error_text.push_str(" (not authenticated?)");
}
}
PollState::ReadPicture => {
write_handle.can_get_album_art = false;
write_handle.dirty_flag.store(true, Ordering::Relaxed);
log(
"Failed to get readpicture",
LogState::Warning,
write_handle.log_level,
);
// Not setting error_text here since
// ReadPictureInDir is tried next
}
PollState::ReadPictureInDir => {
write_handle.can_get_album_art_in_dir = false;
write_handle.dirty_flag.store(true, Ordering::Relaxed);
log(
"Failed to get albumart",
LogState::Warning,
write_handle.log_level,
);
write_handle.error_text = "Failed to get album art from MPD".into();
}
_ => (),
}
write_handle.poll_state = PollState::None;
} else if line.starts_with("state: ") {
let remaining = line.split_off(7);
let remaining = remaining.trim();
if remaining == "stop" {
write_handle.current_song_filename.clear();
write_handle.art_data.clear();
write_handle.art_data_size = 0;
write_handle.art_data_type.clear();
write_handle.can_get_album_art = true;
write_handle.can_get_album_art_in_dir = true;
write_handle.current_song_title.clear();
write_handle.current_song_artist.clear();
write_handle.current_song_album.clear();
write_handle.current_song_length = 0.0;
write_handle.current_song_position = 0.0;
write_handle.did_check_overtime = false;
write_handle.force_get_status = true;
}
if remaining == "stop" || remaining == "pause" {
got_mpd_state = if remaining == "stop" {
MPDPlayState::Stopped
} else {
MPDPlayState::Paused
};
write_handle.error_text.clear();
write!(&mut write_handle.error_text, "MPD has {got_mpd_state:?}").ok();
log(
format!("MPD is {got_mpd_state:?}"),
LogState::Warning,
write_handle.log_level,
);
break 'handle_buf;
}
} else if line.starts_with("file: ") {
let song_file = line.split_off(6);
if song_file != write_handle.current_song_filename {
write_handle.current_song_filename = song_file;
write_handle.art_data.clear();
write_handle.art_data_size = 0;
write_handle.art_data_type.clear();
write_handle.can_get_album_art = true;
write_handle.can_get_album_art_in_dir = true;
write_handle.current_song_title.clear();
write_handle.current_song_artist.clear();
write_handle.current_song_album.clear();
write_handle.current_song_length = 0.0;
write_handle.current_song_position = 0.0;
write_handle.did_check_overtime = false;
write_handle.force_get_status = true;
write_handle.error_text.clear();
}
write_handle.dirty_flag.store(true, Ordering::Relaxed);
write_handle.song_title_get_time = Instant::now();
} else if line.starts_with("elapsed: ") {
let parse_pos_result = f64::from_str(&line.split_off(9));
if let Ok(value) = parse_pos_result {
write_handle.current_song_position = value;
write_handle.dirty_flag.store(true, Ordering::Relaxed);
write_handle.song_pos_get_time = Instant::now();
} else {
log(
"Failed to parse current song position",
LogState::Warning,
write_handle.log_level,
);
}
} else if line.starts_with("duration: ") {
let parse_pos_result = f64::from_str(&line.split_off(10));
if let Ok(value) = parse_pos_result {
write_handle.current_song_length = value;
write_handle.dirty_flag.store(true, Ordering::Relaxed);
write_handle.song_length_get_time = Instant::now();
} else {
log(
"Failed to parse current song duration",
LogState::Warning,
write_handle.log_level,
);
}
} else if line.starts_with("size: ") {
let parse_artsize_result = usize::from_str(&line.split_off(6));
if let Ok(value) = parse_artsize_result {
write_handle.art_data_size = value;
write_handle.dirty_flag.store(true, Ordering::Relaxed);
} else {
log(
"Failed to parse album art byte size",
LogState::Warning,
write_handle.log_level,
);
}
} else if line.starts_with("binary: ") {
let parse_artbinarysize_result = usize::from_str(&line.split_off(8));
if let Ok(value) = parse_artbinarysize_result {
write_handle.current_binary_size = value;
} else {
log(
"Failed to parse album art chunk byte size",
LogState::Warning,
write_handle.log_level,
);
}
} else if line.starts_with("Title: ") {
write_handle.current_song_title = line.split_off(7);
} else if line.starts_with("Artist: ") {
write_handle.current_song_artist = line.split_off(8);
} else if line.starts_with("Album: ") {
write_handle.current_song_album = line.split_off(7);
} else if line.starts_with("type: ") {
write_handle.art_data_type = line.split_off(6);
} else {
log(
format!("Got unrecognized/ignored line: {line}"),
LogState::Warning,
write_handle.log_level,
);
}
} else if let Err((msg, read_line_in_progress)) = read_line_result {
log(
format!(
"read_line: {}, saved size == {}, in_progress size == {}",
msg,
saved.len(),
read_line_in_progress.len()
),
LogState::Warning,
write_handle.log_level,
);
*saved_str = read_line_in_progress;
break 'handle_buf;
} else {
unreachable!();
}
} // 'handle_buf: loop
if got_mpd_state != write_handle.mpd_play_state {
write_handle.dirty_flag.store(true, Ordering::Relaxed);
if got_mpd_state == MPDPlayState::Playing {
write_handle.error_text.clear();
}
}
write_handle.mpd_play_state = got_mpd_state;
if got_mpd_state != MPDPlayState::Playing {
write_handle.poll_state = PollState::None;
write_handle.song_pos_get_time = Instant::now();
write_handle.current_song_length = 30.0;
write_handle.current_song_position = 0.0;
}
Ok(())
}
fn handler_write_block(&self) -> Result<(), String> {
let mut write_handle = self
.state
.try_write()
.map_err(|_| String::from("Failed to get MPDHandler write lock (write_block)"))?;
if write_handle.poll_state == PollState::None {
if !write_handle.did_check_overtime
&& write_handle.current_song_position
+ write_handle.song_pos_get_time.elapsed().as_secs_f64()
- 0.2
> write_handle.current_song_length
{
write_handle.did_check_overtime = true;
write_handle.force_get_current_song = true;
}
if !write_handle.is_authenticated
&& !write_handle.password.is_empty()
&& write_handle.can_authenticate
{
let p = write_handle.password.clone();
let write_result = write_handle
.stream
.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}"),
LogState::Error,
write_handle.log_level,
);
}
} else if write_handle.can_get_status
&& (write_handle.song_title_get_time.elapsed() > POLL_DURATION
|| write_handle.force_get_current_song)
&& write_handle.mpd_play_state == MPDPlayState::Playing
{
write_handle.force_get_current_song = false;
let write_result = write_handle.stream.write(b"currentsong\n");
if write_result.is_ok() {
write_handle.poll_state = PollState::CurrentSong;
} else if let Err(e) = write_result {
log(
format!("Failed to request song info over stream: {e}"),
LogState::Error,
write_handle.log_level,
);
}
} else if write_handle.can_get_status
&& (write_handle.song_length_get_time.elapsed() > POLL_DURATION
|| write_handle.song_pos_get_time.elapsed() > POLL_DURATION
|| write_handle.force_get_status)
{
write_handle.force_get_status = false;
let write_result = write_handle.stream.write(b"status\n");
if write_result.is_ok() {
write_handle.poll_state = PollState::Status;
} else if let Err(e) = write_result {
log(
format!("Failed to request status over stream: {e}"),
LogState::Error,
write_handle.log_level,
);
}
} else if (write_handle.art_data.is_empty()
|| write_handle.art_data.len() != write_handle.art_data_size)
&& !write_handle.current_song_filename.is_empty()
{
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 \"{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}"),
LogState::Error,
write_handle.log_level,
);
}
} else if write_handle.can_get_album_art_in_dir {
let write_result = write_handle
.stream
.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}"),
LogState::Error,
write_handle.log_level,
);
}
}
}
}
Ok(())
}
fn is_reading_picture(&self) -> bool {
loop {
if let Ok(read_handle) = self.state.try_read() {
return read_handle.poll_state == PollState::ReadPicture
|| read_handle.poll_state == PollState::ReadPictureInDir;
} else {
thread::sleep(Duration::from_millis(5));
}
}
}
}
impl MPDHandlerState {
pub fn get_art_type(&self) -> String {
self.art_data_type.clone()
}
pub fn is_art_data_ready(&self) -> bool {
log(
format!(
"is_art_data_ready(): art_data_size == {}, art_data.len() == {}",
self.art_data_size,
self.art_data.len()
),
LogState::Debug,
self.log_level,
);
self.art_data_size != 0 && self.art_data.len() == self.art_data_size
}
pub fn get_art_data(&self) -> &[u8] {
&self.art_data
}
}

View file

@ -1,19 +0,0 @@
mod fontconfig;
mod freetype;
pub use self::freetype::font_has_char;
pub use fontconfig::{get_matching_font_from_char, get_matching_font_from_str};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ascii_verify() {
let fetched_path =
get_matching_font_from_char('a').expect("Should be able to find match for 'a'");
if !font_has_char('a', &fetched_path).expect("Should be able to check font for 'a'") {
panic!("fetched font does not have 'a'");
}
}
}

View file

@ -1,350 +0,0 @@
use std::{path::PathBuf, str::FromStr};
mod ffi {
use std::{ffi::CStr, os::raw::c_int};
mod bindgen {
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(dead_code)]
#![allow(deref_nullptr)]
#![allow(clippy::redundant_static_lifetimes)]
include!(concat!(env!("OUT_DIR"), "/unicode_support_bindings.rs"));
}
pub struct FcConfigWr {
config: *mut bindgen::FcConfig,
}
impl Drop for FcConfigWr {
fn drop(&mut self) {
if !self.config.is_null() {
unsafe {
bindgen::FcConfigDestroy(self.config);
}
}
}
}
impl FcConfigWr {
pub fn new() -> Result<Self, String> {
let config = unsafe { bindgen::FcInitLoadConfigAndFonts() };
if config.is_null() {
Err(String::from("Failed to create FcConfig"))
} else {
Ok(Self { config })
}
}
#[allow(dead_code)]
pub fn get(&mut self) -> *mut bindgen::FcConfig {
self.config
}
pub fn apply_pattern_to_config(&mut self, pattern: &mut FcPatternWr) -> bool {
unsafe {
bindgen::FcConfigSubstitute(
self.config,
pattern.get(),
bindgen::_FcMatchKind_FcMatchPattern,
) == bindgen::FcTrue as bindgen::FcBool
}
}
pub fn font_match(&mut self, pattern: &mut FcPatternWr) -> Result<FcPatternWr, String> {
unsafe {
let mut result: bindgen::FcResult = 0 as bindgen::FcResult;
let result_pattern = bindgen::FcFontMatch(
self.config,
pattern.get(),
&mut result as *mut bindgen::FcResult,
);
if result != bindgen::_FcResult_FcResultMatch {
if !result_pattern.is_null() {
bindgen::FcPatternDestroy(result_pattern);
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:?})"
));
}
} else if result_pattern.is_null() {
return Err(String::from(
"Failed to FcFontMatch (result_pattern is null)",
));
}
Ok(FcPatternWr {
pattern: result_pattern,
})
}
}
}
pub struct FcCharSetWr {
charset: *mut bindgen::FcCharSet,
}
impl Drop for FcCharSetWr {
fn drop(&mut self) {
if !self.charset.is_null() {
unsafe {
bindgen::FcCharSetDestroy(self.charset);
}
}
}
}
impl FcCharSetWr {
#[allow(dead_code)]
pub fn new_with_str(s: &str) -> Result<Self, String> {
let charset;
unsafe {
let charset_ptr = bindgen::FcCharSetCreate();
if charset_ptr.is_null() {
return Err(String::from("Failed to create FcCharSet with str"));
}
charset = FcCharSetWr {
charset: charset_ptr,
};
for c in s.chars() {
if bindgen::FcCharSetAddChar(charset.charset, c as u32)
== bindgen::FcFalse as bindgen::FcBool
{
return Err(String::from("Failed to add chars from str into FcCharSet"));
}
}
}
Ok(charset)
}
pub fn new_with_char(c: char) -> Result<Self, String> {
let charset;
unsafe {
let charset_ptr = bindgen::FcCharSetCreate();
if charset_ptr.is_null() {
return Err(String::from("Failed to create FcCharSet with char"));
}
charset = FcCharSetWr {
charset: charset_ptr,
};
if bindgen::FcCharSetAddChar(charset.charset, c as u32)
== bindgen::FcFalse as bindgen::FcBool
{
return Err(String::from("Failed to add char to FcCharSet"));
}
}
Ok(charset)
}
pub fn get(&mut self) -> *mut bindgen::FcCharSet {
self.charset
}
}
pub struct FcPatternWr {
pattern: *mut bindgen::FcPattern,
}
impl Drop for FcPatternWr {
fn drop(&mut self) {
if !self.pattern.is_null() {
unsafe {
bindgen::FcPatternDestroy(self.pattern);
}
}
}
}
impl FcPatternWr {
pub fn new_with_charset(c: &mut FcCharSetWr) -> Result<Self, String> {
let pattern;
unsafe {
let pattern_ptr = bindgen::FcPatternCreate();
if pattern_ptr.is_null() {
return Err(String::from("Failed to FcPatternCreate"));
}
pattern = Self {
pattern: pattern_ptr,
};
bindgen::FcDefaultSubstitute(pattern.pattern);
let value = bindgen::FcValue {
type_: bindgen::_FcType_FcTypeCharSet,
u: bindgen::_FcValue__bindgen_ty_1 { c: c.get() },
};
if bindgen::FcPatternAdd(
pattern.pattern,
bindgen::FC_CHARSET as *const _ as *const i8,
value,
bindgen::FcTrue as bindgen::FcBool,
) == bindgen::FcFalse as bindgen::FcBool
{
return Err(String::from("Failed to add FcCharSet to new Pattern"));
}
}
Ok(pattern)
}
pub fn get(&mut self) -> *mut bindgen::FcPattern {
self.pattern
}
pub fn get_count(&self) -> c_int {
unsafe { bindgen::FcPatternObjectCount(self.pattern) }
}
pub fn filter_to_filenames(&self) -> Result<Self, String> {
let pattern;
unsafe {
let mut file_object_set_filter = FcObjectSetWr::new_file_object_set()?;
let pattern_ptr =
bindgen::FcPatternFilter(self.pattern, file_object_set_filter.get());
if pattern_ptr.is_null() {
return Err(String::from("Failed to FcPatternFilter"));
}
pattern = Self {
pattern: pattern_ptr,
};
}
Ok(pattern)
}
pub fn get_filename_contents(&self) -> Result<Vec<String>, String> {
let mut vec: Vec<String> = Vec::new();
let count = self.get_count();
unsafe {
let mut value = bindgen::FcValue {
type_: 0,
u: bindgen::_FcValue__bindgen_ty_1 { i: 0 },
};
for i in 0..count {
if bindgen::FcPatternGet(
self.pattern,
bindgen::FC_FILE as *const _ as *const i8,
i,
&mut value as *mut bindgen::FcValue,
) == bindgen::_FcResult_FcResultMatch
&& value.type_ == bindgen::_FcType_FcTypeString
{
let cs = CStr::from_ptr(value.u.s as *const i8);
vec.push(
cs.to_str()
.map_err(|_| String::from("Failed to convert CStr to String"))?
.to_owned(),
);
}
}
}
Ok(vec)
}
}
struct FcObjectSetWr {
object_set: *mut bindgen::FcObjectSet,
}
impl Drop for FcObjectSetWr {
fn drop(&mut self) {
unsafe {
if !self.object_set.is_null() {
bindgen::FcObjectSetDestroy(self.object_set);
}
}
}
}
impl FcObjectSetWr {
pub fn new_file_object_set() -> Result<Self, String> {
let object_set;
unsafe {
let object_set_ptr = bindgen::FcObjectSetCreate();
if object_set_ptr.is_null() {
return Err(String::from("Failed to FcObjectSetCreate"));
}
object_set = Self {
object_set: object_set_ptr,
};
if bindgen::FcObjectSetAdd(
object_set.object_set,
bindgen::FC_FILE as *const _ as *const i8,
) == bindgen::FcFalse as bindgen::FcBool
{
return Err(String::from(
"Failed to add \"FC_FILE\" with FcObjectSetAdd",
));
}
}
Ok(object_set)
}
pub fn get(&mut self) -> *mut bindgen::FcObjectSet {
self.object_set
}
}
}
#[allow(dead_code)]
pub fn get_matching_font_from_str(s: &str) -> Result<PathBuf, String> {
let mut config = ffi::FcConfigWr::new()?;
let mut charset = ffi::FcCharSetWr::new_with_str(s)?;
let mut search_pattern = ffi::FcPatternWr::new_with_charset(&mut charset)?;
if !config.apply_pattern_to_config(&mut search_pattern) {
return Err(String::from("Failed to apply_pattern_to_config"));
}
let result_pattern = config.font_match(&mut search_pattern)?;
let filtered_pattern = result_pattern.filter_to_filenames()?;
let result_vec = filtered_pattern.get_filename_contents()?;
if result_vec.is_empty() {
Err(String::from(
"Empty result_vec for get_matching_font_from_str",
))
} else {
PathBuf::from_str(&result_vec[0]).map_err(|e| e.to_string())
}
}
pub fn get_matching_font_from_char(c: char) -> Result<PathBuf, String> {
let mut config = ffi::FcConfigWr::new()?;
let mut charset = ffi::FcCharSetWr::new_with_char(c)?;
let mut search_pattern = ffi::FcPatternWr::new_with_charset(&mut charset)?;
if !config.apply_pattern_to_config(&mut search_pattern) {
return Err(String::from("Failed to apply_pattern_to_config"));
}
let result_pattern = config.font_match(&mut search_pattern)?;
let filtered_pattern = result_pattern.filter_to_filenames()?;
let result_vec = filtered_pattern.get_filename_contents()?;
if result_vec.is_empty() {
Err(String::from(
"Empty result_vec for get_matching_font_from_char",
))
} else {
PathBuf::from_str(&result_vec[0]).map_err(|e| e.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_ascii_fetching() {
let fetched_path =
get_matching_font_from_char('a').expect("Should be able to find match for 'a'");
println!("{:?}", fetched_path);
}
}

View file

@ -1,160 +0,0 @@
use std::path::Path;
mod ffi {
use freetype::freetype::{
FT_Done_Face, FT_Done_FreeType, FT_Face, FT_FaceRec_, FT_Get_Char_Index, FT_Init_FreeType,
FT_Library, FT_ModuleRec_, FT_Open_Args, FT_Open_Face, FT_Parameter_, FT_StreamRec_,
FT_OPEN_PATHNAME,
};
use std::ffi::CString;
use std::path::Path;
pub struct FTLibrary {
library: FT_Library,
faces: Vec<FT_Face>,
}
impl Drop for FTLibrary {
fn drop(&mut self) {
for face in &self.faces {
unsafe {
FT_Done_Face(*face);
}
}
if !self.library.is_null() {
unsafe {
FT_Done_FreeType(self.library);
}
}
}
}
impl FTLibrary {
pub fn new() -> Option<FTLibrary> {
unsafe {
let mut library_ptr: FT_Library = 0 as FT_Library;
if FT_Init_FreeType(&mut library_ptr) == 0 {
Some(FTLibrary {
library: library_ptr,
faces: Vec::new(),
})
} else {
None
}
}
}
pub fn get(&self) -> FT_Library {
self.library
}
pub fn init_faces(&mut self, args: &mut FTOpenArgs) -> Result<(), String> {
unsafe {
let mut face: FT_Face = 0 as FT_Face;
// first get number of faces
let mut result = FT_Open_Face(
self.get(),
args.get_ptr(),
-1,
&mut face as *mut *mut FT_FaceRec_,
);
if result != 0 {
FT_Done_Face(face);
return Err(String::from("Failed to get number of faces"));
}
let count = (*face).num_faces;
for i in 0..count {
result = FT_Open_Face(
self.get(),
args.get_ptr(),
i,
&mut face as *mut *mut FT_FaceRec_,
);
if result != 0 {
FT_Done_Face(face);
return Err(String::from("Failed to fetch face"));
}
self.faces.push(face);
}
}
Ok(())
}
#[allow(dead_code)]
pub fn drop_faces(&mut self) {
for face in &self.faces {
unsafe {
FT_Done_Face(*face);
}
}
self.faces.clear();
}
pub fn has_char(&self, c: char) -> bool {
let char_value: u64 = c as u64;
for face in &self.faces {
unsafe {
let result = FT_Get_Char_Index(*face, char_value);
if result != 0 {
return true;
}
}
}
false
}
}
pub struct FTOpenArgs {
args: FT_Open_Args,
// "args" has a pointer to the CString in "pathname", so it must be kept
#[allow(dead_code)]
pathname: Option<CString>,
}
impl FTOpenArgs {
pub fn new_with_path(path: &Path) -> Self {
unsafe {
let cstring: CString = CString::from_vec_unchecked(
path.as_os_str().to_str().unwrap().as_bytes().to_vec(),
);
let args = FT_Open_Args {
flags: FT_OPEN_PATHNAME,
memory_base: std::ptr::null::<u8>(),
memory_size: 0,
pathname: cstring.as_ptr() as *mut i8,
stream: std::ptr::null_mut::<FT_StreamRec_>(),
driver: std::ptr::null_mut::<FT_ModuleRec_>(),
num_params: 0,
params: std::ptr::null_mut::<FT_Parameter_>(),
};
FTOpenArgs {
args,
pathname: Some(cstring),
}
}
}
#[allow(dead_code)]
pub fn get_args(&self) -> FT_Open_Args {
self.args
}
pub fn get_ptr(&mut self) -> *mut FT_Open_Args {
&mut self.args as *mut FT_Open_Args
}
}
}
pub fn font_has_char(c: char, font_path: &Path) -> Result<bool, String> {
let mut library =
ffi::FTLibrary::new().ok_or_else(|| String::from("Failed to get FTLibrary"))?;
let mut args = ffi::FTOpenArgs::new_with_path(font_path);
library.init_faces(&mut args)?;
Ok(library.has_char(c))
}