mirror of
https://github.com/danbulant/rshell
synced 2026-05-19 04:18:33 +00:00
initial commit
This commit is contained in:
commit
721576aa50
8 changed files with 4853 additions and 0 deletions
9
.envrc
Normal file
9
.envrc
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#!/usr/bin/env bash
|
||||
# the shebang is ignored, but nice for editors
|
||||
|
||||
if type -P lorri &>/dev/null; then
|
||||
eval "$(lorri direnv)"
|
||||
else
|
||||
echo 'while direnv evaluated .envrc, could not find the command "lorri" [https://github.com/nix-community/lorri]'
|
||||
use nix
|
||||
fi
|
||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/target
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"nixEnvSelector.nixFile": "${workspaceFolder}/shell.nix"
|
||||
}
|
||||
4583
Cargo.lock
generated
Normal file
4583
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "barrs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
cushy = { version = "0.4.0", features=["tokio", "tokio-multi-thread", "plotters", "roboto-flex"], default-features = false }
|
||||
tokio = { version = "1.40.0", features = ["rt", "rt-multi-thread"] }
|
||||
plotters = { version = "0.3.7", default-features = false }
|
||||
image = { version = "0.25.0", features = ["png"] }
|
||||
mpris = "2.0.1"
|
||||
reqwest = "0.12.8"
|
||||
40
shell.nix
Normal file
40
shell.nix
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
{ pkgs ? import <nixpkgs> {} }:
|
||||
let
|
||||
# rust-rover things
|
||||
rust-toolchain =
|
||||
pkgs.symlinkJoin {
|
||||
name = "rust-toolchain";
|
||||
paths = with pkgs; [rustc cargo rustPlatform.rustcSrc clippy rustfmt gcc rust-analyzer];
|
||||
};
|
||||
in
|
||||
pkgs.mkShell rec {
|
||||
buildInputs = with pkgs;[
|
||||
openssl
|
||||
pkg-config
|
||||
cmake
|
||||
zlib
|
||||
rust-toolchain
|
||||
|
||||
# common glutin
|
||||
libxkbcommon
|
||||
libGL
|
||||
dbus
|
||||
|
||||
# winit wayland
|
||||
wayland
|
||||
|
||||
# winit x11
|
||||
xorg.libXcursor
|
||||
xorg.libXrandr
|
||||
xorg.libXi
|
||||
xorg.libX11
|
||||
];
|
||||
nativeBuildInputs = with pkgs; [
|
||||
pkg-config
|
||||
fontconfig
|
||||
];
|
||||
LD_LIBRARY_PATH = "${pkgs.lib.makeLibraryPath buildInputs}";
|
||||
OPENSSL_DIR="${pkgs.openssl.dev}";
|
||||
OPENSSL_LIB_DIR="${pkgs.openssl.out}/lib";
|
||||
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
|
||||
}
|
||||
12
src/main.rs
Normal file
12
src/main.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
use cushy::{Open, PendingApp, Run, TokioRuntime};
|
||||
|
||||
mod spotify;
|
||||
|
||||
fn main() -> cushy::Result {
|
||||
let mut app = PendingApp::new(TokioRuntime::default());
|
||||
|
||||
spotify::spotify_controls()
|
||||
.open(&mut app)?;
|
||||
|
||||
app.run()
|
||||
}
|
||||
193
src/spotify.rs
Normal file
193
src/spotify.rs
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
use std::{sync::{Arc, Mutex}, thread, time::Duration};
|
||||
|
||||
use mpris::{LoopStatus, PlaybackStatus, PlayerFinder};
|
||||
use cushy::{kludgine::{AnyTexture, LazyTexture}, styles::components::WidgetBackground, value::{Destination, Dynamic, IntoReader, Source}, widget::MakeWidget, widgets::Image, Open, PendingApp};
|
||||
use reqwest::Client;
|
||||
use image;
|
||||
use tokio::{runtime, task::JoinHandle};
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct PlayingTrack {
|
||||
title: String,
|
||||
artist: String,
|
||||
album: String,
|
||||
duration: Duration,
|
||||
|
||||
album_art: Option<String>,
|
||||
|
||||
status: PlaybackStatus,
|
||||
shuffle: bool,
|
||||
loop_status: LoopStatus,
|
||||
}
|
||||
|
||||
pub fn spotify_controls() -> impl MakeWidget {
|
||||
let (progress, track) = get_track_dynamics();
|
||||
let texture = get_texture_dynamic(track.clone());
|
||||
|
||||
track.map_each(|track| {
|
||||
if let Some(track) = track {
|
||||
format!(
|
||||
"{} - {}",
|
||||
track.artist,
|
||||
track.title,
|
||||
)
|
||||
} else {
|
||||
"No track playing".to_string()
|
||||
}
|
||||
})
|
||||
.to_label()
|
||||
.and(
|
||||
Image::new(texture)
|
||||
)
|
||||
.into_rows()
|
||||
}
|
||||
|
||||
fn get_empty_texture() -> AnyTexture {
|
||||
AnyTexture::Lazy(
|
||||
LazyTexture::from_image(
|
||||
image::DynamicImage::ImageRgba8(
|
||||
image::ImageBuffer::new(1, 1)
|
||||
),
|
||||
cushy::kludgine::wgpu::FilterMode::Linear
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fn tokio_runtime() -> &'static runtime::Handle {
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
|
||||
use cushy::value::{Destination, Dynamic};
|
||||
use cushy::widget::MakeWidget;
|
||||
use cushy::widgets::progress::Progressable;
|
||||
use cushy::Run;
|
||||
use tokio::time::sleep;
|
||||
|
||||
static RUNTIME: OnceLock<runtime::Handle> = OnceLock::new();
|
||||
RUNTIME.get_or_init(|| {
|
||||
let rt = runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("tokio initialization error");
|
||||
let handle = rt.handle().clone();
|
||||
std::thread::spawn(move || {
|
||||
// Replace with the async main loop, or some sync structure to
|
||||
// control shutting it down if desired.
|
||||
rt.block_on(async {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(10000)).await
|
||||
}
|
||||
});
|
||||
});
|
||||
handle
|
||||
})
|
||||
}
|
||||
|
||||
fn get_texture_dynamic(track: Dynamic<Option<PlayingTrack>>) -> Dynamic<AnyTexture> {
|
||||
let client = Client::new();
|
||||
|
||||
|
||||
let texture = Dynamic::new(get_empty_texture());
|
||||
|
||||
let prev_request_join = Arc::new(Mutex::new(None::<JoinHandle<()>>));
|
||||
track.for_each({
|
||||
let texture = texture.clone();
|
||||
move |track| {
|
||||
if let Some(track) = track {
|
||||
let mut prev_request_join = prev_request_join.lock().unwrap();
|
||||
if let Some(prev_request_join) = prev_request_join.take() {
|
||||
prev_request_join.abort();
|
||||
}
|
||||
let texture = texture.clone();
|
||||
let client = client.clone();
|
||||
let track_url = track.album_art.clone().unwrap();
|
||||
*prev_request_join = Some(tokio_runtime().spawn(async move {
|
||||
let response = client.get(track_url).send().await.unwrap();
|
||||
let bytes = response.bytes().await.unwrap();
|
||||
let image = image::load_from_memory(&bytes).unwrap();
|
||||
let image_texture = LazyTexture::from_image(image, cushy::kludgine::wgpu::FilterMode::Linear);
|
||||
let image_texture = AnyTexture::Lazy(image_texture);
|
||||
texture.map_mut(move |mut t| *t = image_texture);
|
||||
// texture.set(image_texture);
|
||||
}));
|
||||
} else {
|
||||
texture.map_mut(move |mut t| *t = get_empty_texture());
|
||||
// texture.set(empty_texture);
|
||||
}
|
||||
}
|
||||
}).persist();
|
||||
|
||||
texture
|
||||
}
|
||||
|
||||
/// This spawns a new thread to track the current playing track and its progress.
|
||||
/// The two objects are separate as track info is updated less frequently than progress.
|
||||
fn get_track_dynamics() -> (Dynamic<Duration>, Dynamic<Option<PlayingTrack>>) {
|
||||
let track = Dynamic::new(None::<PlayingTrack>);
|
||||
let progress = Dynamic::new(Duration::from_secs(0));
|
||||
thread::spawn({
|
||||
let track = track.clone();
|
||||
let progress = progress.clone();
|
||||
move || {
|
||||
let player_finder = PlayerFinder::new();
|
||||
let player_finder = match player_finder {
|
||||
Ok(finder) => finder,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to find player: {:?}. Dbus/libdbus may not be installed? Track data will be unavailable", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
const SLEEP_TIME: Duration = Duration::from_millis(200);
|
||||
|
||||
loop {
|
||||
let player = player_finder.find_active();
|
||||
let player = match player {
|
||||
Ok(player) => player,
|
||||
Err(_) => {
|
||||
track.set(None);
|
||||
thread::sleep(SLEEP_TIME);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let tracker = player.track_progress(200);
|
||||
let mut tracker = match tracker {
|
||||
Ok(tracker) => tracker,
|
||||
Err(_) => {
|
||||
track.set(None);
|
||||
thread::sleep(SLEEP_TIME);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let mut first_run = true;
|
||||
loop {
|
||||
let tick = tracker.tick();
|
||||
if tick.player_quit {
|
||||
track.set(None);
|
||||
thread::sleep(SLEEP_TIME);
|
||||
break;
|
||||
}
|
||||
if tick.progress_changed || first_run {
|
||||
first_run = false;
|
||||
let p = tick.progress;
|
||||
let meta = p.metadata();
|
||||
track.set(Some(PlayingTrack {
|
||||
title: meta.title().unwrap_or_default().to_string(),
|
||||
artist: meta.artists().unwrap_or_default().join(", "),
|
||||
album: meta.album_name().unwrap_or_default().to_string(),
|
||||
duration: meta.length().unwrap_or_default(),
|
||||
|
||||
album_art: meta.art_url().map(|url| url.to_string()),
|
||||
|
||||
status: p.playback_status(),
|
||||
shuffle: p.shuffle(),
|
||||
loop_status: p.loop_status(),
|
||||
}));
|
||||
}
|
||||
progress.set(tick.progress.position());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
(progress, track)
|
||||
}
|
||||
Loading…
Reference in a new issue