get started on basic menu

This commit is contained in:
Daniel Bulant 2025-01-07 21:29:37 +01:00
parent 23e638285a
commit a337d3f470
No known key found for this signature in database
5 changed files with 1262 additions and 653 deletions

1582
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,12 @@ edition = "2021"
[dependencies] [dependencies]
# cushy = { version = "0.4.0", features=["tokio", "tokio-multi-thread", "plotters", "roboto-flex"], default-features = false } # cushy = { version = "0.4.0", features=["tokio", "tokio-multi-thread", "plotters", "roboto-flex"], default-features = false }
cushy = { git = "https://github.com/khonsulabs/cushy.git", branch = "main", features=["tokio", "tokio-multi-thread", "plotters", "roboto-flex"] } cushy = { git = "https://github.com/khonsulabs/cushy.git", branch = "main", features = [
"tokio",
"tokio-multi-thread",
"plotters",
"roboto-flex",
] }
tokio = { version = "1.40.0", features = ["rt", "rt-multi-thread"] } tokio = { version = "1.40.0", features = ["rt", "rt-multi-thread"] }
plotters = { version = "0.3.7", default-features = false } plotters = { version = "0.3.7", default-features = false }
image = { version = "0.25.0", features = ["png"] } image = { version = "0.25.0", features = ["png"] }
@ -19,3 +24,9 @@ hsl = "0.1.1"
itertools = "0.10.0" itertools = "0.10.0"
palette = "0.7.3" palette = "0.7.3"
clap = { version = "4.5.20", features = ["derive"] } clap = { version = "4.5.20", features = ["derive"] }
fuzzy-matcher = "0.3.5"
freedesktop-desktop-entry = "0.7.5"
which = "7.0.1"
[patch.crates-io]
winit = { path = "../winit" }

View file

@ -1,13 +1,30 @@
use std::{sync::{Arc, Mutex}, thread, time::Duration}; use std::{
sync::{Arc, Mutex},
thread,
time::Duration,
};
use crate::{
theme::{BG_DEFAULT, TEXT_SPOTIFY},
vibrancy::Vibrancy,
};
use cushy::{
figures::{units::Lp, Size, Zero},
kludgine::{AnyTexture, LazyTexture},
styles::{
components::{FontWeight, TextColor, WidgetBackground},
Color, CornerRadii, Dimension, DimensionRange, Weight,
},
value::{Destination, Dynamic, Source},
widget::MakeWidget,
widgets::{image::ImageCornerRadius, label::Displayable, Image},
};
use http_cache_reqwest::{CACacheManager, Cache, CacheMode, HttpCache, HttpCacheOptions}; use http_cache_reqwest::{CACacheManager, Cache, CacheMode, HttpCache, HttpCacheOptions};
use image::{self, imageops::FilterType, Rgb};
use mpris::{LoopStatus, PlaybackStatus, PlayerFinder}; use mpris::{LoopStatus, PlaybackStatus, PlayerFinder};
use cushy::{figures::{units::Lp, Size, Zero}, kludgine::{AnyTexture, LazyTexture}, styles::{components::{FontWeight, TextColor, WidgetBackground}, Color, CornerRadii, Dimension, DimensionRange, Weight}, value::{Destination, Dynamic, IntoReader, Source}, widget::MakeWidget, widgets::{image::ImageCornerRadius, Image}};
use reqwest::Client; use reqwest::Client;
use reqwest_middleware::ClientBuilder; use reqwest_middleware::ClientBuilder;
use image::{self, imageops::FilterType, Rgb};
use tokio::{runtime, task::JoinHandle}; use tokio::{runtime, task::JoinHandle};
use crate::{theme::{BG_DEFAULT, TEXT_SPOTIFY}, vibrancy::Vibrancy};
#[derive(PartialEq)] #[derive(PartialEq)]
struct PlayingTrack { struct PlayingTrack {
@ -34,31 +51,31 @@ pub fn spotify_controls() -> impl MakeWidget {
Image::new(texture) Image::new(texture)
.aspect_fit() .aspect_fit()
.with(&ImageCornerRadius, CornerRadii { .with(
&ImageCornerRadius,
CornerRadii {
top_left: CORNER_RADIUS, top_left: CORNER_RADIUS,
top_right: Dimension::ZERO, top_right: Dimension::ZERO,
bottom_left: CORNER_RADIUS, bottom_left: CORNER_RADIUS,
bottom_right: Dimension::ZERO, bottom_right: Dimension::ZERO,
}) },
)
// default pad is 6, default line height is 16 // default pad is 6, default line height is 16
.size(image_size) .size(image_size)
.and( .and(
track.map_each(|track| { track
.map_each(|track| {
if let Some(track) = track { if let Some(track) = track {
format!( format!("{} - {}", track.artist, track.title,)
"{} - {}",
track.artist,
track.title,
)
} else { } else {
"No track playing".to_string() "No track playing".to_string()
} }
}) })
.to_label() .into_label()
.with(&TextColor, TEXT_SPOTIFY) .with(&TextColor, TEXT_SPOTIFY)
.with(&FontWeight, Weight::BOLD) .with(&FontWeight, Weight::BOLD)
.centered() .centered()
.pad() .pad(),
) )
.into_columns() .into_columns()
.with(&WidgetBackground, BG_DEFAULT) .with(&WidgetBackground, BG_DEFAULT)
@ -66,14 +83,10 @@ pub fn spotify_controls() -> impl MakeWidget {
} }
fn get_empty_texture() -> AnyTexture { fn get_empty_texture() -> AnyTexture {
AnyTexture::Lazy( AnyTexture::Lazy(LazyTexture::from_image(
LazyTexture::from_image( image::DynamicImage::ImageRgba8(image::ImageBuffer::new(1, 1)),
image::DynamicImage::ImageRgba8( cushy::kludgine::wgpu::FilterMode::Linear,
image::ImageBuffer::new(1, 1) ))
),
cushy::kludgine::wgpu::FilterMode::Linear
)
)
} }
fn tokio_runtime() -> &'static runtime::Handle { fn tokio_runtime() -> &'static runtime::Handle {
@ -110,7 +123,9 @@ pub struct ImageVibrancy {
light_muted: Option<Color>, light_muted: Option<Color>,
} }
fn get_texture_dynamic(track: Dynamic<Option<PlayingTrack>>) -> (Dynamic<AnyTexture>, Dynamic<ImageVibrancy>) { fn get_texture_dynamic(
track: Dynamic<Option<PlayingTrack>>,
) -> (Dynamic<AnyTexture>, Dynamic<ImageVibrancy>) {
let client = ClientBuilder::new(Client::new()) let client = ClientBuilder::new(Client::new())
.with(Cache(HttpCache { .with(Cache(HttpCache {
mode: CacheMode::Default, mode: CacheMode::Default,
@ -119,12 +134,12 @@ fn get_texture_dynamic(track: Dynamic<Option<PlayingTrack>>) -> (Dynamic<AnyText
})) }))
.build(); .build();
let texture = Dynamic::new(get_empty_texture()); let texture = Dynamic::new(get_empty_texture());
let vibrancy = Dynamic::new(ImageVibrancy::default()); let vibrancy = Dynamic::new(ImageVibrancy::default());
let prev_request_join = Arc::new(Mutex::new(None::<JoinHandle<()>>)); let prev_request_join = Arc::new(Mutex::new(None::<JoinHandle<()>>));
track.for_each({ track
.for_each({
let texture = texture.clone(); let texture = texture.clone();
let vibrancy = vibrancy.clone(); let vibrancy = vibrancy.clone();
move |track| { move |track| {
@ -151,7 +166,10 @@ fn get_texture_dynamic(track: Dynamic<Option<PlayingTrack>>) -> (Dynamic<AnyText
dark_muted: image_vibrancy.dark_muted.map(|c| rgb_to_color(c)), dark_muted: image_vibrancy.dark_muted.map(|c| rgb_to_color(c)),
light_muted: image_vibrancy.light_muted.map(|c| rgb_to_color(c)), light_muted: image_vibrancy.light_muted.map(|c| rgb_to_color(c)),
}); });
let image_texture = LazyTexture::from_image(image, cushy::kludgine::wgpu::FilterMode::Linear); let image_texture = LazyTexture::from_image(
image,
cushy::kludgine::wgpu::FilterMode::Linear,
);
let image_texture = AnyTexture::Lazy(image_texture); let image_texture = AnyTexture::Lazy(image_texture);
texture.set(image_texture); texture.set(image_texture);
})); }));
@ -160,7 +178,8 @@ fn get_texture_dynamic(track: Dynamic<Option<PlayingTrack>>) -> (Dynamic<AnyText
texture.set(get_empty_texture()); texture.set(get_empty_texture());
} }
} }
}).persist(); })
.persist();
(texture, vibrancy) (texture, vibrancy)
} }

View file

@ -4,11 +4,11 @@ use cli::{Args, Commands};
use cushy::{PendingApp, Run, TokioRuntime}; use cushy::{PendingApp, Run, TokioRuntime};
use menu::start_menu; use menu::start_menu;
mod vibrancy;
mod theme;
mod bar; mod bar;
mod cli; mod cli;
mod menu; mod menu;
mod theme;
mod vibrancy;
fn main() -> cushy::Result { fn main() -> cushy::Result {
let args = Args::parse(); let args = Args::parse();
@ -20,5 +20,6 @@ fn main() -> cushy::Result {
Commands::Power => todo!(), Commands::Power => todo!(),
} }
// Ok(())
app.run() app.run()
} }

View file

@ -1,17 +1,145 @@
use cushy::{kludgine::app::winit::window::WindowLevel, widget::MakeWidget, Application, Open}; use std::sync::Arc;
use cushy::{
kludgine::{app::winit::window::WindowLevel, Color},
value::{Dynamic, Source},
widget::{MakeWidget, MakeWidgetList},
widgets::{input::InputValue, Stack},
Application, Open,
};
use freedesktop_desktop_entry::{default_paths, get_languages_from_env, Iter};
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
use which::which;
struct AppMenuEntry {
name: String,
icon: Option<String>,
keywords: Vec<String>,
start: AppMenuStart,
}
struct AppExecutable {
exec: String,
exec_path: Option<String>,
}
enum AppMenuStart {
Executable(AppExecutable),
Url(String),
}
struct SearchResult {
app: usize,
fuzzy: i64,
}
pub fn start_menu(app: &mut impl Application) -> cushy::Result { pub fn start_menu(app: &mut impl Application) -> cushy::Result {
let mut window = "Hello, World!" let locales = get_languages_from_env();
let mut entries: Vec<AppMenuEntry> = Vec::new();
for entry in Iter::new(default_paths()).entries(Some(&locales)) {
let Some(name) = entry.name(&locales) else {
continue;
};
let name = name.into();
let Some(type_) = entry.type_() else { continue };
if let Some(try_exec) = entry.desktop_entry("TryExec") {
let path = std::path::Path::new(try_exec);
if path.is_absolute() {
if !path.exists() {
continue;
}
} else {
let res = which(path);
if res.is_err() {
continue;
}
}
}
let icon = entry.icon().map(Into::into);
let keywords = entry
.keywords(&locales)
.map(|s| s.into_iter().map(Into::into).collect())
.unwrap_or_default();
let start = match type_.to_lowercase().as_str() {
"application" => {
let Some(exec) = entry.exec().map(Into::into) else {
continue;
};
let exec_path = entry.desktop_entry("Path").map(Into::into);
AppMenuStart::Executable(AppExecutable { exec, exec_path })
}
"link" => {
let Some(url) = entry.desktop_entry("URL") else {
continue;
};
AppMenuStart::Url(url.into())
}
_ => continue,
};
entries.push(AppMenuEntry {
name,
icon,
keywords,
start,
});
}
let entries = Arc::new(entries);
let search_text = Dynamic::new(String::new());
let results = search_text.map_each({
let entries = entries.clone();
let matcher = SkimMatcherV2::default();
move |search_text| {
entries
.iter()
.enumerate()
.filter_map(|(i, e)| {
let mut max_score = matcher.fuzzy_match(&e.name, &search_text);
for keyword in &e.keywords {
let score = matcher.fuzzy_match(&keyword, &search_text);
if score > max_score {
max_score = score;
}
}
max_score.map(|max_score| SearchResult {
app: i,
fuzzy: max_score,
})
})
.map(|res| {
let entry = &entries[res.app];
entry.name.clone()
})
.make_widget_list()
}
});
let search = search_text.to_input().placeholder("Search");
let mut window = search
.and(Stack::rows(results))
.into_rows()
.pad() .pad()
.background_color(Color::BLACK.with_alpha(1))
.into_window() .into_window()
.transparent() .transparent()
.app_name("rshell") .app_name("rshell")
.decorated(false) .decorated(false)
// .resizable(false)
.window_level(WindowLevel::AlwaysOnTop); .window_level(WindowLevel::AlwaysOnTop);
window.sans_serif_font_family.push(cushy::styles::FamilyOwned::Name("Iosevka NF".into()));
window window
.open(app).map(|_| ()) .sans_serif_font_family
.push(cushy::styles::FamilyOwned::Name("Iosevka NF".into()));
window.open(app).map(|_| ())
} }