work on advanced settings, #558
This commit is contained in:
parent
5b68fc4fbc
commit
fb2fee92af
9
Cargo.lock
generated
9
Cargo.lock
generated
@ -1231,7 +1231,7 @@ checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
|
||||
|
||||
[[package]]
|
||||
name = "ffplayout"
|
||||
version = "0.20.5"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
@ -1253,7 +1253,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ffplayout-api"
|
||||
version = "0.20.5"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"actix-files",
|
||||
"actix-multipart",
|
||||
@ -1292,13 +1292,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ffplayout-lib"
|
||||
version = "0.20.5"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"crossbeam-channel",
|
||||
"derive_more",
|
||||
"ffprobe",
|
||||
"file-rotate",
|
||||
"lazy_static",
|
||||
"lettre",
|
||||
"lexical-sort",
|
||||
"log",
|
||||
@ -3462,7 +3463,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tests"
|
||||
version = "0.20.5"
|
||||
version = "0.21.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"crossbeam-channel",
|
||||
|
@ -4,7 +4,7 @@ default-members = ["ffplayout-api", "ffplayout-engine", "tests"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.20.5"
|
||||
version = "0.21.0"
|
||||
license = "GPL-3.0"
|
||||
repository = "https://github.com/ffplayout/ffplayout"
|
||||
authors = ["Jonathan Baecker <jonbae77@gmail.com>"]
|
||||
|
29
assets/advanced.yml
Normal file
29
assets/advanced.yml
Normal file
@ -0,0 +1,29 @@
|
||||
help: Changing these settings is for advanced users only! There will be no support or guarantee that it will be stable after changing them.
|
||||
decoder:
|
||||
input_param:
|
||||
# output_param get also applied to ingest instance.
|
||||
output_param:
|
||||
filters:
|
||||
deinterlace: # yadif=0:-1:0
|
||||
pad_scale_w: # scale={}:-1,
|
||||
pad_scale_h: # scale=-1:{},
|
||||
pad_video: # '{}pad=max(iw\\,ih*({0}/{1})):ow/({0}/{1}):(ow-iw)/2:(oh-ih)/2'
|
||||
fps: # fps={}
|
||||
scale: # scale={}:{}
|
||||
set_dar: # setdar=dar={}
|
||||
fade_in: # '{}fade=in:st=0:d=0.5'
|
||||
fade_out: # '{}fade=out:st={}:d=1.0'
|
||||
overlay_logo_scale: # ',scale={}'
|
||||
overlay_logo: # null[v];movie={}:loop=0,setpts=N/(FRAME_RATE*TB),format=rgba,colorchannelmixer=aa={}{}[l];[v][l]{}:shortest=1
|
||||
overlay_logo_fade_in: # ',fade=in:st=0:d=1.0:alpha=1'
|
||||
overlay_logo_fade_out: # ',fade=out:st={}:d=1.0:alpha=1'
|
||||
tpad: # tpad=stop_mode=add:stop_duration={}
|
||||
drawtext_from_file: # drawtext=text='{}':{}{}
|
||||
drawtext_from_zmq: # zmq=b=tcp\\\\://'{}',drawtext@dyntext={}
|
||||
apad: # apad=whole_dur={}
|
||||
volume: # volume={}
|
||||
split: # split={}{}
|
||||
encoder:
|
||||
input_param:
|
||||
ingest:
|
||||
input_param:
|
@ -1,26 +0,0 @@
|
||||
help: Changing these settings is for advanced users only! There will be no support or guarantee that it will be stable after changing it.
|
||||
decoder:
|
||||
input_options:
|
||||
output_options:
|
||||
filters:
|
||||
deinterlace: yadif=0:-1:0
|
||||
pad_scale_w: scale={}:-1,
|
||||
pad_scale_h: scale=-1:{},
|
||||
pad_video: '{}pad=max(iw\\,ih*({0}/{1})):ow/({0}/{1}):(ow-iw)/2:(oh-ih)/2'
|
||||
fps: fps={}
|
||||
scale: scale={}:{}
|
||||
set_dar: setdar=dar={}
|
||||
fade_in: {}fade=in:st=0:d=0.5
|
||||
fade_out: {}fade=out:st={}:d=1.0
|
||||
overlay_logo_scale: ',scale={}'
|
||||
overlay_logo: null[v];movie={}:loop=0,setpts=N/(FRAME_RATE*TB),format=rgba,colorchannelmixer=aa={}{}[l];[v][l]{}:shortest=1
|
||||
overlay_logo_fade_in: ',fade=in:st=0:d=1.0:alpha=1'
|
||||
overlay_logo_fade_out: ',fade=out:st={}:d=1.0:alpha=1'
|
||||
tpad: tpad=stop_mode=add:stop_duration={}
|
||||
drawtext_from_file: drawtext=text='{}':{}{}
|
||||
drawtext_from_zmq: zmq=b=tcp\\\\://'{}',drawtext@dyntext={}
|
||||
apad: apad=whole_dur={}
|
||||
volume: volume={}
|
||||
split: split={}{}
|
||||
encoder:
|
||||
input_options:
|
@ -14,7 +14,7 @@ use ffplayout_lib::{
|
||||
controller::ProcessUnit::*, test_tcp_port, Media, PlayoutConfig, ProcessControl,
|
||||
FFMPEG_IGNORE_ERRORS, FFMPEG_UNRECOVERABLE_ERRORS,
|
||||
},
|
||||
vec_strings,
|
||||
vec_strings, ADVANCED_CONFIG,
|
||||
};
|
||||
|
||||
fn server_monitor(
|
||||
@ -61,6 +61,10 @@ pub fn ingest_server(
|
||||
dummy_media.unit = Ingest;
|
||||
dummy_media.add_filter(&config, &None);
|
||||
|
||||
if let Some(ingest_input_cmd) = &ADVANCED_CONFIG.lock().unwrap().ingest.input_cmd {
|
||||
server_cmd.append(&mut ingest_input_cmd.clone());
|
||||
}
|
||||
|
||||
server_cmd.append(&mut stream_input.clone());
|
||||
|
||||
if let Some(mut filter) = dummy_media.filter {
|
||||
|
@ -2,7 +2,7 @@ use std::process::{self, Command, Stdio};
|
||||
|
||||
use simplelog::*;
|
||||
|
||||
use ffplayout_lib::{filter::v_drawtext, utils::PlayoutConfig, vec_strings};
|
||||
use ffplayout_lib::{filter::v_drawtext, utils::PlayoutConfig, vec_strings, ADVANCED_CONFIG};
|
||||
|
||||
/// Desktop Output
|
||||
///
|
||||
@ -10,16 +10,18 @@ use ffplayout_lib::{filter::v_drawtext, utils::PlayoutConfig, vec_strings};
|
||||
pub fn output(config: &PlayoutConfig, log_format: &str) -> process::Child {
|
||||
let mut enc_filter: Vec<String> = vec![];
|
||||
|
||||
let mut enc_cmd = vec_strings![
|
||||
"-hide_banner",
|
||||
"-nostats",
|
||||
"-v",
|
||||
log_format,
|
||||
let mut enc_cmd = vec_strings!["-hide_banner", "-nostats", "-v", log_format];
|
||||
|
||||
if let Some(encoder_input_cmd) = &ADVANCED_CONFIG.lock().unwrap().encoder.input_cmd {
|
||||
enc_cmd.append(&mut encoder_input_cmd.clone());
|
||||
}
|
||||
|
||||
enc_cmd.append(&mut vec_strings![
|
||||
"-i",
|
||||
"pipe:0",
|
||||
"-window_title",
|
||||
"ffplayout"
|
||||
];
|
||||
]);
|
||||
|
||||
if let Some(mut cmd) = config.out.output_cmd.clone() {
|
||||
if !cmd.iter().any(|i| {
|
||||
|
@ -34,7 +34,7 @@ use ffplayout_lib::{
|
||||
controller::ProcessUnit::*, get_delta, sec_to_time, stderr_reader, test_tcp_port, Media,
|
||||
PlayerControl, PlayoutConfig, PlayoutStatus, ProcessControl,
|
||||
},
|
||||
vec_strings,
|
||||
vec_strings, ADVANCED_CONFIG,
|
||||
};
|
||||
|
||||
/// Ingest Server for HLS
|
||||
@ -47,10 +47,15 @@ fn ingest_to_hls_server(
|
||||
|
||||
let mut server_prefix = vec_strings!["-hide_banner", "-nostats", "-v", "level+info"];
|
||||
let stream_input = config.ingest.input_cmd.clone().unwrap();
|
||||
server_prefix.append(&mut stream_input.clone());
|
||||
let mut dummy_media = Media::new(0, "Live Stream", false);
|
||||
dummy_media.unit = Ingest;
|
||||
|
||||
if let Some(ingest_input_cmd) = &ADVANCED_CONFIG.lock().unwrap().ingest.input_cmd {
|
||||
server_prefix.append(&mut ingest_input_cmd.clone());
|
||||
}
|
||||
|
||||
server_prefix.append(&mut stream_input.clone());
|
||||
|
||||
let mut is_running;
|
||||
|
||||
if let Some(url) = stream_input.iter().find(|s| s.contains("://")) {
|
||||
@ -197,6 +202,10 @@ pub fn write_hls(
|
||||
|
||||
let mut enc_prefix = vec_strings!["-hide_banner", "-nostats", "-v", &ff_log_format];
|
||||
|
||||
if let Some(encoder_input_cmd) = &ADVANCED_CONFIG.lock().unwrap().encoder.input_cmd {
|
||||
enc_prefix.append(&mut encoder_input_cmd.clone());
|
||||
}
|
||||
|
||||
let mut read_rate = 1.0;
|
||||
|
||||
if let Some(begin) = &node.begin {
|
||||
|
@ -19,11 +19,14 @@ pub use hls::write_hls;
|
||||
use crate::input::{ingest_server, source_generator};
|
||||
use crate::utils::task_runner;
|
||||
|
||||
use ffplayout_lib::utils::{
|
||||
use ffplayout_lib::vec_strings;
|
||||
use ffplayout_lib::{
|
||||
utils::{
|
||||
sec_to_time, stderr_reader, OutputMode::*, PlayerControl, PlayoutConfig, PlayoutStatus,
|
||||
ProcessControl, ProcessUnit::*,
|
||||
},
|
||||
ADVANCED_CONFIG,
|
||||
};
|
||||
use ffplayout_lib::vec_strings;
|
||||
|
||||
/// Player
|
||||
///
|
||||
@ -130,6 +133,11 @@ pub fn player(
|
||||
}
|
||||
|
||||
let mut dec_cmd = vec_strings!["-hide_banner", "-nostats", "-v", &ff_log_format];
|
||||
|
||||
if let Some(decoder_input_cmd) = &ADVANCED_CONFIG.lock().unwrap().decoder.input_cmd {
|
||||
dec_cmd.append(&mut decoder_input_cmd.clone());
|
||||
}
|
||||
|
||||
dec_cmd.append(&mut cmd);
|
||||
|
||||
if let Some(mut filter) = node.filter {
|
||||
|
@ -14,6 +14,7 @@ crossbeam-channel = "0.5"
|
||||
derive_more = "0.99"
|
||||
ffprobe = "0.3"
|
||||
file-rotate = "0.7"
|
||||
lazy_static = "1.4"
|
||||
lettre = { version = "0.11", features = ["builder", "rustls-tls", "smtp-transport"], default-features = false }
|
||||
lexical-sort = "0.3"
|
||||
log = "0.4"
|
||||
|
@ -11,8 +11,10 @@ mod custom;
|
||||
pub mod v_drawtext;
|
||||
|
||||
use crate::utils::{
|
||||
controller::ProcessUnit::*, fps_calc, is_close, Media, OutputMode::*, PlayoutConfig,
|
||||
controller::ProcessUnit::*, custom_format, fps_calc, is_close, Media, OutputMode::*,
|
||||
PlayoutConfig,
|
||||
};
|
||||
use crate::ADVANCED_CONFIG;
|
||||
|
||||
use super::vec_strings;
|
||||
|
||||
@ -182,7 +184,9 @@ impl Default for Filters {
|
||||
}
|
||||
|
||||
fn deinterlace(field_order: &Option<String>, chain: &mut Filters) {
|
||||
if let Some(order) = field_order {
|
||||
if let Some(deinterlace) = &ADVANCED_CONFIG.lock().unwrap().decoder.filters.deinterlace {
|
||||
chain.add_filter(&deinterlace, 0, Video)
|
||||
} else if let Some(order) = field_order {
|
||||
if order != "progressive" {
|
||||
chain.add_filter("yadif=0:-1:0", 0, Video)
|
||||
}
|
||||
@ -193,27 +197,60 @@ fn pad(aspect: f64, chain: &mut Filters, v_stream: &ffprobe::Stream, config: &Pl
|
||||
if !is_close(aspect, config.processing.aspect, 0.03) {
|
||||
let mut scale = String::new();
|
||||
|
||||
let advanced = ADVANCED_CONFIG.lock().unwrap();
|
||||
|
||||
if let (Some(w), Some(h)) = (v_stream.width, v_stream.height) {
|
||||
if w > config.processing.width && aspect > config.processing.aspect {
|
||||
scale = format!("scale={}:-1,", config.processing.width);
|
||||
match &advanced.decoder.filters.pad_scale_w {
|
||||
Some(pad_scale_w) => {
|
||||
scale = custom_format(pad_scale_w, &[&config.processing.width])
|
||||
}
|
||||
None => scale = format!("scale={}:-1,", config.processing.width),
|
||||
};
|
||||
} else if h > config.processing.height && aspect < config.processing.aspect {
|
||||
scale = format!("scale=-1:{},", config.processing.height);
|
||||
match &advanced.decoder.filters.pad_scale_h {
|
||||
Some(pad_scale_h) => {
|
||||
scale = custom_format(pad_scale_h, &[&config.processing.width])
|
||||
}
|
||||
None => scale = format!("scale=-1:{},", config.processing.height),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(pad_video) = &advanced.decoder.filters.pad_video {
|
||||
chain.add_filter(
|
||||
&custom_format(
|
||||
pad_video,
|
||||
&[
|
||||
&scale,
|
||||
&config.processing.width.to_string(),
|
||||
&config.processing.height.to_string(),
|
||||
],
|
||||
),
|
||||
0,
|
||||
Video,
|
||||
)
|
||||
} else {
|
||||
chain.add_filter(
|
||||
&format!(
|
||||
"{scale}pad=max(iw\\,ih*({0}/{1})):ow/({0}/{1}):(ow-iw)/2:(oh-ih)/2",
|
||||
config.processing.width, config.processing.height
|
||||
"{}pad=max(iw\\,ih*({1}/{2})):ow/({1}/{2}):(ow-iw)/2:(oh-ih)/2",
|
||||
scale, config.processing.width, config.processing.height
|
||||
),
|
||||
0,
|
||||
Video,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fps(fps: f64, chain: &mut Filters, config: &PlayoutConfig) {
|
||||
if fps != config.processing.fps {
|
||||
chain.add_filter(&format!("fps={}", config.processing.fps), 0, Video)
|
||||
let advanced = ADVANCED_CONFIG.lock().unwrap();
|
||||
|
||||
match &advanced.decoder.filters.fps {
|
||||
Some(fps) => chain.add_filter(&custom_format(fps, &[&config.processing.fps]), 0, Video),
|
||||
None => chain.add_filter(&format!("fps={}", config.processing.fps), 0, Video),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,17 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
extern crate log;
|
||||
extern crate simplelog;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
pub mod filter;
|
||||
pub mod macros;
|
||||
pub mod utils;
|
||||
|
||||
use utils::advanced_config::AdvancedConfig;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref ADVANCED_CONFIG: Arc<Mutex<AdvancedConfig>> =
|
||||
Arc::new(Mutex::new(AdvancedConfig::new()));
|
||||
}
|
||||
|
101
lib/src/utils/advanced_config.rs
Normal file
101
lib/src/utils/advanced_config.rs
Normal file
@ -0,0 +1,101 @@
|
||||
use std::{
|
||||
env,
|
||||
fs::File,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use shlex::split;
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub struct AdvancedConfig {
|
||||
pub help: Option<String>,
|
||||
pub decoder: DecoderConfig,
|
||||
pub encoder: EncoderConfig,
|
||||
pub ingest: IngestConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub struct DecoderConfig {
|
||||
pub input_param: Option<String>,
|
||||
pub output_param: Option<String>,
|
||||
pub filters: Filters,
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub input_cmd: Option<Vec<String>>,
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub output_cmd: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub struct EncoderConfig {
|
||||
pub input_param: Option<String>,
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub input_cmd: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub struct IngestConfig {
|
||||
pub input_param: Option<String>,
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub input_cmd: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub struct Filters {
|
||||
pub deinterlace: Option<String>,
|
||||
pub pad_scale_w: Option<String>,
|
||||
pub pad_scale_h: Option<String>,
|
||||
pub pad_video: Option<String>,
|
||||
pub fps: Option<String>,
|
||||
pub scale: Option<String>,
|
||||
pub set_dar: Option<String>,
|
||||
pub fade_in: Option<String>,
|
||||
pub fade_out: Option<String>,
|
||||
pub overlay_logo_scale: Option<String>,
|
||||
pub overlay_logo: Option<String>,
|
||||
pub overlay_logo_fade_in: Option<String>,
|
||||
pub overlay_logo_fade_out: Option<String>,
|
||||
pub tpad: Option<String>,
|
||||
pub drawtext_from_file: Option<String>,
|
||||
pub drawtext_from_zmq: Option<String>,
|
||||
pub apad: Option<String>,
|
||||
pub volume: Option<String>,
|
||||
pub split: Option<String>,
|
||||
}
|
||||
|
||||
impl AdvancedConfig {
|
||||
pub fn new() -> Self {
|
||||
let mut config: AdvancedConfig = Default::default();
|
||||
let mut config_path = PathBuf::from("/etc/ffplayout/advanced.yml");
|
||||
|
||||
if !config_path.is_file() {
|
||||
if Path::new("./assets/advanced.yml").is_file() {
|
||||
config_path = PathBuf::from("./assets/advanced.yml")
|
||||
} else if let Some(p) = env::current_exe().ok().as_ref().and_then(|op| op.parent()) {
|
||||
config_path = p.join("advanced.yml")
|
||||
};
|
||||
}
|
||||
|
||||
if let Ok(f) = File::open(&config_path) {
|
||||
config = serde_yaml::from_reader(f).expect("Could not read advanced config file");
|
||||
|
||||
if let Some(input_parm) = &config.decoder.input_param {
|
||||
config.decoder.input_cmd = split(&input_parm);
|
||||
}
|
||||
|
||||
if let Some(output_param) = &config.decoder.output_param {
|
||||
config.decoder.output_cmd = split(&output_param);
|
||||
}
|
||||
|
||||
if let Some(input_param) = &config.encoder.input_param {
|
||||
config.encoder.input_cmd = split(&input_param);
|
||||
}
|
||||
|
||||
if let Some(input_param) = &config.ingest.input_param {
|
||||
config.ingest.input_cmd = split(&input_param);
|
||||
}
|
||||
};
|
||||
|
||||
config
|
||||
}
|
||||
}
|
@ -11,6 +11,8 @@ use log::LevelFilter;
|
||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use shlex::split;
|
||||
|
||||
use crate::ADVANCED_CONFIG;
|
||||
|
||||
use super::vec_strings;
|
||||
use crate::utils::{free_tcp_socket, home_dir, time_to_sec, OutputMode::*};
|
||||
|
||||
@ -424,6 +426,19 @@ impl PlayoutConfig {
|
||||
config.processing.audio_tracks = 1
|
||||
}
|
||||
|
||||
let mut process_cmd = vec_strings![];
|
||||
|
||||
if config.processing.audio_only {
|
||||
process_cmd.append(&mut vec_strings!["-vn"]);
|
||||
} else if config.processing.copy_video {
|
||||
process_cmd.append(&mut vec_strings!["-c:v", "copy"]);
|
||||
} else if let Some(decoder_cmd) = &ADVANCED_CONFIG.lock().unwrap().decoder.output_cmd {
|
||||
if !decoder_cmd.contains(&"-r".to_string()) {
|
||||
process_cmd.append(&mut vec_strings!["-r", &config.processing.fps]);
|
||||
}
|
||||
|
||||
process_cmd.append(&mut decoder_cmd.clone());
|
||||
} else {
|
||||
let bitrate = format!(
|
||||
"{}k",
|
||||
config.processing.width * config.processing.height / 16
|
||||
@ -434,13 +449,6 @@ impl PlayoutConfig {
|
||||
(config.processing.width * config.processing.height / 16) / 2
|
||||
);
|
||||
|
||||
let mut process_cmd = vec_strings![];
|
||||
|
||||
if config.processing.audio_only {
|
||||
process_cmd.append(&mut vec_strings!["-vn"]);
|
||||
} else if config.processing.copy_video {
|
||||
process_cmd.append(&mut vec_strings!["-c:v", "copy"]);
|
||||
} else {
|
||||
process_cmd.append(&mut vec_strings![
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
@ -463,7 +471,7 @@ impl PlayoutConfig {
|
||||
|
||||
if config.processing.copy_audio {
|
||||
process_cmd.append(&mut vec_strings!["-c:a", "copy"]);
|
||||
} else {
|
||||
} else if ADVANCED_CONFIG.lock().unwrap().decoder.output_cmd.is_none() {
|
||||
process_cmd.append(&mut pre_audio_codec(
|
||||
&config.processing.custom_filter,
|
||||
&config.ingest.custom_filter,
|
||||
|
@ -1,5 +1,6 @@
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fmt,
|
||||
fs::{self, metadata, File},
|
||||
io::{BufRead, BufReader, Error},
|
||||
net::TcpListener,
|
||||
@ -21,6 +22,7 @@ use serde::{de::Deserializer, Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use simplelog::*;
|
||||
|
||||
pub mod advanced_config;
|
||||
pub mod config;
|
||||
pub mod controller;
|
||||
pub mod errors;
|
||||
@ -928,6 +930,46 @@ pub fn parse_log_level_filter(s: &str) -> Result<LevelFilter, &'static str> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn custom_format<T: fmt::Display>(template: &str, args: &[T]) -> String {
|
||||
let mut filled_template = String::new();
|
||||
let mut arg_iter = args.iter().map(|x| format!("{}", x));
|
||||
let mut template_iter = template.chars();
|
||||
|
||||
while let Some(c) = template_iter.next() {
|
||||
if c == '{' {
|
||||
if let Some(nc) = template_iter.next() {
|
||||
if nc == '{' {
|
||||
filled_template.push('{');
|
||||
} else if nc == '}' {
|
||||
if let Some(arg) = arg_iter.next() {
|
||||
filled_template.push_str(&arg);
|
||||
} else {
|
||||
filled_template.push(c);
|
||||
filled_template.push(nc);
|
||||
}
|
||||
} else if let Some(n) = nc.to_digit(10) {
|
||||
filled_template.push_str(&args[n as usize].to_string());
|
||||
} else {
|
||||
filled_template.push(nc);
|
||||
}
|
||||
}
|
||||
} else if c == '}' {
|
||||
if let Some(nc) = template_iter.next() {
|
||||
if nc == '}' {
|
||||
filled_template.push('}');
|
||||
continue;
|
||||
} else {
|
||||
filled_template.push(nc);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
filled_template.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
filled_template
|
||||
}
|
||||
|
||||
pub fn home_dir() -> Option<PathBuf> {
|
||||
home_dir_inner()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user