option to configure layout

This commit is contained in:
figsoda 2020-10-30 12:20:37 -04:00
parent 446a2edaa2
commit cd57e56a94
5 changed files with 523 additions and 90 deletions

45
Cargo.lock generated
View file

@ -12,6 +12,12 @@ version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d25d88fd6b8041580a654f9d0c581a047baee2b3efee13275f2fc392fc75034"
[[package]]
name = "base64"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
[[package]]
name = "bitflags"
version = "1.2.1"
@ -78,9 +84,9 @@ dependencies = [
[[package]]
name = "crossterm"
version = "0.18.1"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cef9149b29071d44c9fb98fd9c27fcf74405bbdb761889ad6a03f36be93b0b15"
checksum = "4e86d73f2a0b407b5768d10a8c720cf5d2df49a9efc10ca09176d201ead4b7fb"
dependencies = [
"bitflags",
"crossterm_winapi",
@ -208,8 +214,10 @@ name = "mmtc"
version = "0.1.0"
dependencies = [
"anyhow",
"crossterm 0.18.1",
"crossterm 0.18.2",
"expand",
"ron",
"serde",
"tokio",
"tui",
]
@ -313,12 +321,43 @@ version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]]
name = "ron"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8a58080b7bb83b2ea28c3b7a9a994fd5e310330b7c8ca5258d99b98128ecfe4"
dependencies = [
"base64",
"bitflags",
"serde",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "serde"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "signal-hook"
version = "0.1.16"

View file

@ -13,8 +13,10 @@ readme = "README.md"
[dependencies]
anyhow = "1.0.33"
crossterm = "0.18.1"
crossterm = "0.18.2"
expand = "0.1.2"
ron = "0.6.2"
serde = { version = "1.0.117", features = ["derive"] }
tui = { version = "0.12.0", default-features = false, features = ["crossterm"] }
[dependencies.tokio]

191
src/config.rs Normal file
View file

@ -0,0 +1,191 @@
use serde::{
de::{self, EnumAccess, SeqAccess, VariantAccess, Visitor},
Deserialize, Deserializer,
};
use std::{
cmp::min,
error,
fmt::{self, Formatter},
};
#[derive(Debug, Deserialize)]
pub struct Config {
pub layout: Widget,
}
#[derive(Debug, Deserialize)]
pub enum Widget {
Rows(Vec<Constrained<Widget>>),
Columns(Vec<Constrained<Widget>>),
Textbox(Texts),
Queue { columns: Vec<Constrained<Texts>> },
}
#[derive(Debug, Deserialize)]
pub enum Constrained<T> {
Free(T),
Fixed(u16, T),
Ratio(u32, T),
}
#[derive(Debug)]
pub enum Texts {
Empty,
Plain(String),
CurrentFile,
CurrentTitle,
CurrentArtist,
CurrentAlbum,
QueueFile,
QueueTitle,
QueueArtist,
QueueAlbum,
Parts(Vec<Texts>),
If(Condition, Box<Texts>, Box<Texts>),
}
#[derive(Debug, Deserialize)]
pub enum Condition {
TitleExist,
ArtistExist,
AlbumExist,
}
impl<'de> Deserialize<'de> for Texts {
fn deserialize<D>(de: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct TextsVisitor;
impl<'de> Visitor<'de> for TextsVisitor {
type Value = Texts;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
formatter.write_str("enum Texts")
}
fn visit_unit<E: error::Error>(self) -> Result<Self::Value, E> {
Ok(Texts::Empty)
}
fn visit_seq<A: SeqAccess<'de>>(self, mut sa: A) -> Result<Self::Value, A::Error> {
let mut xs = Vec::with_capacity(min(sa.size_hint().unwrap_or(0), 4096));
while let Some(x) = sa.next_element()? {
xs.push(x);
}
Ok(Texts::Parts(xs))
}
fn visit_enum<A: EnumAccess<'de>>(self, ea: A) -> Result<Self::Value, A::Error> {
#[derive(Deserialize)]
#[serde(field_identifier)]
enum Variant {
Plain,
CurrentFile,
CurrentTitle,
CurrentArtist,
CurrentAlbum,
QueueFile,
QueueTitle,
QueueArtist,
QueueAlbum,
Parts,
If,
IfNot,
}
struct IfVisitor;
impl<'de> Visitor<'de> for IfVisitor {
type Value = Texts;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
formatter.write_str("IfNot variant")
}
fn visit_seq<A: SeqAccess<'de>>(
self,
mut sa: A,
) -> Result<Self::Value, A::Error> {
Ok(Texts::If(
sa.next_element()?
.ok_or_else(|| de::Error::invalid_length(0, &self))?,
sa.next_element()?.map_or_else(
|| Err(de::Error::invalid_length(1, &self)),
|x| Ok(Box::new(x)),
)?,
Box::new(sa.next_element()?.unwrap_or(Texts::Empty)),
))
}
}
struct IfNotVisitor;
impl<'de> Visitor<'de> for IfNotVisitor {
type Value = Texts;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
formatter.write_str("IfNot variant")
}
fn visit_seq<A: SeqAccess<'de>>(
self,
mut sa: A,
) -> Result<Self::Value, A::Error> {
let cond = sa
.next_element()?
.ok_or_else(|| de::Error::invalid_length(0, &self))?;
let no = sa.next_element()?.map_or_else(
|| Err(de::Error::invalid_length(1, &self)),
|x| Ok(Box::new(x)),
)?;
let yes = Box::new(sa.next_element()?.unwrap_or(Texts::Empty));
Ok(Texts::If(cond, yes, no))
}
}
let (variant, va) = ea.variant()?;
macro_rules! unit_variant {
($v:ident) => {{
va.unit_variant()?;
Ok(Texts::$v)
}};
}
match variant {
Variant::Plain => Ok(Texts::Plain(va.newtype_variant()?)),
Variant::CurrentFile => unit_variant!(CurrentFile),
Variant::CurrentTitle => unit_variant!(CurrentTitle),
Variant::CurrentArtist => unit_variant!(CurrentArtist),
Variant::CurrentAlbum => unit_variant!(CurrentAlbum),
Variant::QueueFile => unit_variant!(QueueFile),
Variant::QueueTitle => unit_variant!(QueueTitle),
Variant::QueueArtist => unit_variant!(QueueArtist),
Variant::QueueAlbum => unit_variant!(QueueAlbum),
Variant::Parts => Ok(Texts::Parts(va.newtype_variant()?)),
Variant::If => va.tuple_variant(3, IfVisitor),
Variant::IfNot => va.tuple_variant(3, IfNotVisitor),
}
}
}
de.deserialize_enum(
"Texts",
&[
"Plain",
"CurrentFile",
"CurrentTitle",
"CurrentArtist",
"CurrentAlbum",
"QueueFile",
"QueueTitle",
"QueueArtist",
"QueueAlbum",
"Parts",
"If",
"IfNot",
],
TextsVisitor,
)
}
}

279
src/layout.rs Normal file
View file

@ -0,0 +1,279 @@
use tui::{
backend::Backend,
layout::{Constraint, Direction, Layout, Rect},
text::{Span, Spans},
widgets::{List, ListItem, Paragraph},
Frame,
};
use crate::{
config::{Condition, Constrained, Texts, Widget},
mpd::{Song, Status, Track},
};
pub fn render(
frame: &mut Frame<impl Backend>,
size: Rect,
widget: &Widget,
queue: &Vec<Track>,
status: &Status,
) {
match widget {
Widget::Rows(xs) => {
let len = xs.capacity();
let mut ws = Vec::with_capacity(len);
let mut cs = Vec::with_capacity(len);
let denom = xs.iter().fold(0, |n, x| {
if let Constrained::Ratio(m, _) = x {
n + m
} else {
n
}
});
for x in xs {
match x {
Constrained::Free(w) => {
ws.push(w);
cs.push(Constraint::Min(0));
}
Constrained::Fixed(n, w) => {
ws.push(w);
cs.push(Constraint::Length(*n));
}
Constrained::Ratio(n, w) => {
ws.push(w);
cs.push(Constraint::Ratio(*n, denom));
}
}
}
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(cs);
let mut chunks = layout.split(size).into_iter();
let mut ws = ws.into_iter();
while let (Some(chunk), Some(w)) = (chunks.next(), ws.next()) {
render(frame, chunk, w, queue, status);
}
}
Widget::Columns(xs) => {
let len = xs.capacity();
let mut ws = Vec::with_capacity(len);
let mut cs = Vec::with_capacity(len);
let denom = xs.iter().fold(0, |n, x| {
if let Constrained::Ratio(m, _) = x {
n + m
} else {
n
}
});
for x in xs {
match x {
Constrained::Free(w) => {
ws.push(w);
cs.push(Constraint::Min(1));
}
Constrained::Fixed(n, w) => {
ws.push(w);
cs.push(Constraint::Length(*n));
}
Constrained::Ratio(n, w) => {
ws.push(w);
cs.push(Constraint::Ratio(*n, denom));
}
}
}
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(cs);
let mut chunks = layout.split(size).into_iter();
let mut ws = ws.into_iter();
while let (Some(chunk), Some(w)) = (chunks.next(), ws.next()) {
render(frame, chunk, w, queue, status);
}
}
Widget::Textbox(xss) => {
let mut spans = Vec::new();
flatten(
&mut spans,
&xss,
if let Some(Song { pos, .. }) = status.song {
queue.get(pos)
} else {
None
},
None,
);
frame.render_widget(Paragraph::new(Spans::from(spans)), size);
}
Widget::Queue { columns } => {
let len = columns.capacity();
let mut ws = Vec::with_capacity(len);
let mut cs = Vec::with_capacity(len);
let denom = columns.iter().fold(0, |n, x| {
if let Constrained::Ratio(m, _) = x {
n + m
} else {
n
}
});
let len = columns.capacity();
let current_track = if let Some(Song { pos, .. }) = status.song {
queue.get(pos)
} else {
None
};
for column in columns {
match column {
Constrained::Free(xs) => {
let mut items = Vec::with_capacity(len);
for x in queue {
let mut spans = Vec::new();
flatten(&mut spans, xs, current_track, Some(x));
items.push(ListItem::new(Spans::from(spans)));
}
ws.push(List::new(items));
cs.push(Constraint::Min(1));
}
Constrained::Fixed(n, xs) => {
let mut items = Vec::with_capacity(len);
for x in queue {
let mut spans = Vec::new();
flatten(&mut spans, xs, current_track, Some(x));
items.push(ListItem::new(Spans::from(spans)));
}
ws.push(List::new(items));
cs.push(Constraint::Length(*n));
}
Constrained::Ratio(n, xs) => {
let mut items = Vec::with_capacity(len);
for x in queue {
let mut spans = Vec::new();
flatten(&mut spans, xs, current_track, Some(x));
items.push(ListItem::new(Spans::from(spans)));
}
ws.push(List::new(items));
cs.push(Constraint::Ratio(*n, denom));
}
}
}
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(cs);
let mut chunks = layout.split(size).into_iter();
let mut ws = ws.into_iter();
while let (Some(chunk), Some(w)) = (chunks.next(), ws.next()) {
frame.render_widget(w, chunk);
}
}
}
}
fn flatten(
spans: &mut Vec<Span>,
xs: &Texts,
current_track: Option<&Track>,
queue_track: Option<&Track>,
) {
match xs {
Texts::Empty => (),
Texts::Plain(x) => spans.push(Span::raw(x.clone())),
Texts::CurrentFile => {
if let Some(Track { file, .. }) = current_track {
spans.push(Span::raw(file.clone()));
}
}
Texts::CurrentTitle => {
if let Some(Track {
title: Some(title), ..
}) = current_track
{
spans.push(Span::raw(title.clone()));
}
}
Texts::CurrentArtist => {
if let Some(Track {
artist: Some(artist),
..
}) = current_track
{
spans.push(Span::raw(artist.clone()));
}
}
Texts::CurrentAlbum => {
if let Some(Track {
album: Some(album), ..
}) = current_track
{
spans.push(Span::raw(album.clone()));
}
}
Texts::QueueFile => {
if let Some(Track { file, .. }) = current_track {
spans.push(Span::raw(file.clone()));
}
}
Texts::QueueTitle => {
if let Some(Track {
title: Some(title), ..
}) = queue_track
{
spans.push(Span::raw(title.clone()));
}
}
Texts::QueueArtist => {
if let Some(Track {
artist: Some(artist),
..
}) = queue_track
{
spans.push(Span::raw(artist.clone()));
}
}
Texts::QueueAlbum => {
if let Some(Track {
album: Some(album), ..
}) = queue_track
{
spans.push(Span::raw(album.clone()));
}
}
Texts::Parts(xss) => {
for xs in xss {
flatten(spans, xs, current_track, queue_track);
}
}
Texts::If(cond, box yes, box no) => {
let xs = if match cond {
Condition::TitleExist => {
matches!(current_track, Some(Track { title: Some(_), .. }))
}
Condition::ArtistExist => matches!(
current_track,
Some(Track {
artist: Some(_), ..
}),
),
Condition::AlbumExist => matches!(queue_track, Some(Track { album: Some(_), .. })),
} {
yes
} else {
no
};
flatten(spans, xs, current_track, queue_track);
}
}
}

View file

@ -1,7 +1,10 @@
#![feature(async_closure)]
#![feature(box_patterns)]
#![forbid(unsafe_code)]
mod config;
mod fail;
mod layout;
mod mpd;
use anyhow::{Context, Result};
@ -14,12 +17,7 @@ use tokio::{
sync::Mutex,
time::{sleep_until, Duration, Instant},
};
use tui::{
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout, Rect},
widgets::{List, ListItem, Paragraph},
Terminal,
};
use tui::{backend::CrosstermBackend, Terminal};
use std::{
io::{stdout, Write},
@ -28,7 +26,7 @@ use std::{
sync::Arc,
};
use crate::mpd::{Song, Status, Track};
use crate::config::Config;
fn cleanup() -> Result<()> {
disable_raw_mode().context("Failed to clean up terminal")?;
@ -53,16 +51,9 @@ async fn main() -> Result<()> {
}
async fn run() -> Result<()> {
let cfg: Config = ron::from_str(&std::fs::read_to_string("mmtc.ron").unwrap()).unwrap();
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 6600);
let queue_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
]);
let mut idle_cl = mpd::init(addr).await.with_context(fail::connect(addr))?;
let mut status_cl = mpd::init(addr).await.with_context(fail::connect(addr))?;
@ -106,79 +97,10 @@ async fn run() -> Result<()> {
let deadline = Instant::now() + Duration::from_secs_f32(1.0 / 30.0);
let queue = &*queue.lock().await;
let status = (*status.lock().await).clone();
let status = &*status.lock().await;
term.draw(|frame| {
let len = queue.len();
let mut titles = Vec::with_capacity(len);
let mut artists = Vec::with_capacity(len);
let mut albums = Vec::with_capacity(len);
for Track {
title,
artist,
album,
..
} in queue
{
titles.push(ListItem::new(title.clone().unwrap_or_default()));
artists.push(ListItem::new(artist.clone().unwrap_or_default()));
albums.push(ListItem::new(album.clone().unwrap_or_default()));
}
let Rect {
x,
y,
width,
height,
} = frame.size();
let chunks = queue_layout.split(Rect {
x,
y,
width,
height: y + height - 1,
});
frame.render_widget(List::new(titles), chunks[0]);
frame.render_widget(List::new(artists), chunks[1]);
frame.render_widget(List::new(albums), chunks[2]);
if let Status {
song: Some(Song { pos, elapsed }),
..
} = status
{
if let Some(Track {
file,
artist,
album,
title,
time,
}) = queue.get(pos)
{
frame.render_widget(
Paragraph::new(format!(
"[{:02}:{:02}/{:02}:{:02}] {}",
elapsed / 60,
elapsed % 60,
time / 60,
time % 60,
match (title, artist, album) {
(Some(title), Some(artist), Some(album)) =>
format!("{} - {} - {}", title, artist, album),
(Some(title), Some(artist), _) => format!("{} - {}", title, artist),
(Some(title), ..) => title.clone(),
_ => file.clone(),
}
)),
Rect {
x,
y: y + height - 1,
width,
height: 1,
},
)
}
}
layout::render(frame, frame.size(), &cfg.layout, queue, status);
})
.context("Failed to draw to terminal")?;