mirror of
https://github.com/danbulant/rshell
synced 2026-05-24 12:33:57 +00:00
get started on basic menu
This commit is contained in:
parent
23e638285a
commit
a337d3f470
5 changed files with 1262 additions and 653 deletions
1582
Cargo.lock
generated
1582
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
13
Cargo.toml
13
Cargo.toml
|
|
@ -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" }
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
140
src/menu/mod.rs
140
src/menu/mod.rs
|
|
@ -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(|_| ())
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue