Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: test multi layershell feature #2

Merged
merged 8 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
524 changes: 468 additions & 56 deletions Cargo.lock

Large diffs are not rendered by default.

15 changes: 13 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
iced = { version = "0.12", features = ["tokio", "debug", "image", "advanced"] }
iced = { version = "0.12", features = [
"tokio",
"debug",
"image",
"advanced",
"svg",
] }
#iced_native = "0.12"
iced_runtime = "0.12"
iced_layershell = "0.3.0"
iced_layershell = "0.4.0-rc1"
tokio = { version = "1.39", features = ["full"] }
iced_futures = "0.12.0"
env_logger = "0.11.5"
Expand All @@ -23,3 +29,8 @@ zbus = { version = "4.4.0", default-features = false, features = ["tokio"] }
tracing-subscriber = "0.3.18"
anyhow = "1.0.86"
alsa = "0.9.0"

gio = "0.20.0"
regex = "1.10.5"
xdg = "2.5.2"
url = "2.5.2"
1 change: 1 addition & 0 deletions misc/launcher.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions misc/reset.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions misc/text-plain.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
149 changes: 149 additions & 0 deletions src/launcher.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
mod applications;

use applications::{all_apps, App};
use iced::widget::{column, scrollable, text_input};
use iced::{Command, Element, Event, Length};
use iced_runtime::command::Action;
use iced_runtime::window::Action as WindowAction;

use super::Message;

use std::sync::LazyLock;

static SCROLLABLE_ID: LazyLock<scrollable::Id> = LazyLock::new(scrollable::Id::unique);
pub static INPUT_ID: LazyLock<text_input::Id> = LazyLock::new(text_input::Id::unique);

pub struct Launcher {
text: String,
apps: Vec<App>,
scrollpos: usize,
pub should_delete: bool,
}

impl Launcher {
pub fn new() -> Self {
Self {
text: "".to_string(),
apps: all_apps(),
scrollpos: 0,
should_delete: false,
}
}

pub fn focus_input(&self) -> Command<super::Message> {
text_input::focus(INPUT_ID.clone())
}

pub fn update(&mut self, message: Message, id: iced::window::Id) -> Command<Message> {
use iced::keyboard::key::Named;
use iced_runtime::keyboard;
match message {
Message::SearchSubmit => {
let re = regex::Regex::new(&self.text).ok();
let index = self
.apps
.iter()
.enumerate()
.filter(|(_, app)| {
if re.is_none() {
return true;
}
let re = re.as_ref().unwrap();

re.is_match(app.title().to_lowercase().as_str())
|| re.is_match(app.description().to_lowercase().as_str())
})
.enumerate()
.find(|(index, _)| *index == self.scrollpos);
if let Some((_, (_, app))) = index {
app.launch();
self.should_delete = true;
Command::single(Action::Window(WindowAction::Close(id)))
} else {
Command::none()
}
}
Message::SearchEditChanged(edit) => {
self.scrollpos = 0;
self.text = edit;
Command::none()
}
Message::Launch(index) => {
self.apps[index].launch();
self.should_delete = true;
Command::single(Action::Window(WindowAction::Close(id)))
}
Message::IcedEvent(event) => {
let mut len = self.apps.len();

let re = regex::Regex::new(&self.text).ok();
if let Some(re) = re {
len = self
.apps
.iter()
.filter(|app| {
re.is_match(app.title().to_lowercase().as_str())
|| re.is_match(app.description().to_lowercase().as_str())
})
.count();
}
if let Event::Keyboard(keyboard::Event::KeyReleased { key, .. })
| Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) = event
{
match key {
keyboard::Key::Named(Named::ArrowUp) => {
if self.scrollpos == 0 {
return Command::none();
}
self.scrollpos -= 1;
}
keyboard::Key::Named(Named::ArrowDown) => {
if self.scrollpos >= len - 1 {
return Command::none();
}
self.scrollpos += 1;
}
keyboard::Key::Named(Named::Escape) => {
self.should_delete = true;
return Command::single(Action::Window(WindowAction::Close(id)));
}
_ => {}
}
}
text_input::focus(INPUT_ID.clone())
}
_ => Command::none(),
}
}

pub fn view(&self) -> Element<Message> {
let re = regex::Regex::new(&self.text).ok();
let text_ip: Element<Message> = text_input("put the launcher name", &self.text)
.padding(10)
.on_input(Message::SearchEditChanged)
.on_submit(Message::SearchSubmit)
.id(INPUT_ID.clone())
.into();
let bottom_vec: Vec<Element<Message>> = self
.apps
.iter()
.enumerate()
.filter(|(_, app)| {
if re.is_none() {
return true;
}
let re = re.as_ref().unwrap();

re.is_match(app.title().to_lowercase().as_str())
|| re.is_match(app.description().to_lowercase().as_str())
})
.enumerate()
.filter(|(index, _)| *index >= self.scrollpos)
.map(|(filter_index, (index, app))| app.view(index, filter_index == self.scrollpos))
.collect();
let bottom: Element<Message> = scrollable(column(bottom_vec).width(Length::Fill))
.id(SCROLLABLE_ID.clone())
.into();
column![text_ip, bottom].into()
}
}
179 changes: 179 additions & 0 deletions src/launcher/applications.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
use std::path::PathBuf;
use std::str::FromStr;

use gio::{AppLaunchContext, DesktopAppInfo};

use gio::prelude::*;
use iced::widget::{button, column, image, row, svg, text};
use iced::{theme, Pixels};
use iced::{Element, Length};

use super::Message;

static DEFAULT_ICON: &[u8] = include_bytes!("../../misc/text-plain.svg");

#[allow(unused)]
#[derive(Debug, Clone)]
pub struct App {
appinfo: DesktopAppInfo,
name: String,
descriptions: Option<gio::glib::GString>,
pub categrades: Option<Vec<String>>,
pub actions: Option<Vec<gio::glib::GString>>,
icon: Option<PathBuf>,
}

impl App {
pub fn launch(&self) {
if let Err(err) = self.appinfo.launch(&[], AppLaunchContext::NONE) {
println!("{}", err);
};
}

pub fn title(&self) -> &str {
&self.name
}

fn icon(&self) -> Element<Message> {
match &self.icon {
Some(path) => {
if path
.as_os_str()
.to_str()
.is_some_and(|pathname| pathname.ends_with("png"))
{
image(image::Handle::from_path(path))
.width(Length::Fixed(80.))
.height(Length::Fixed(80.))
.into()
} else {
svg(svg::Handle::from_path(path))
.width(Length::Fixed(80.))
.height(Length::Fixed(80.))
.into()
}
}
None => svg(svg::Handle::from_memory(DEFAULT_ICON))
.width(Length::Fixed(80.))
.height(Length::Fixed(80.))
.into(),
}
}

pub fn description(&self) -> &str {
match &self.descriptions {
None => "",
Some(description) => description,
}
}

pub fn view(&self, index: usize, selected: bool) -> Element<Message> {
button(
row![
self.icon(),
column![
text(self.title()).size(Pixels::from(20)),
text(self.description()).size(Pixels::from(10))
]
.spacing(4)
]
.spacing(10),
)
.on_press(Message::Launch(index))
.width(Length::Fill)
.height(Length::Fixed(85.))
.style(if selected {
theme::Button::Primary
} else {
theme::Button::Secondary
})
.into()
}
}

static ICONS_SIZE: &[&str] = &["256x256", "128x128"];

static THEMES_LIST: &[&str] = &["breeze", "Adwaita"];

fn get_icon_path_from_xdgicon(iconname: &str) -> Option<PathBuf> {
let scalable_icon_path =
xdg::BaseDirectories::with_prefix("icons/hicolor/scalable/apps").unwrap();
if let Some(iconpath) = scalable_icon_path.find_data_file(format!("{iconname}.svg")) {
return Some(iconpath);
}
for prefix in ICONS_SIZE {
let iconpath =
xdg::BaseDirectories::with_prefix(&format!("icons/hicolor/{prefix}/apps")).unwrap();
if let Some(iconpath) = iconpath.find_data_file(format!("{iconname}.png")) {
return Some(iconpath);
}
}
let pixmappath = xdg::BaseDirectories::with_prefix("pixmaps").unwrap();
if let Some(iconpath) = pixmappath.find_data_file(format!("{iconname}.svg")) {
return Some(iconpath);
}
if let Some(iconpath) = pixmappath.find_data_file(format!("{iconname}.png")) {
return Some(iconpath);
}
for themes in THEMES_LIST {
let iconpath =
xdg::BaseDirectories::with_prefix(&format!("icons/{themes}/apps/48")).unwrap();
if let Some(iconpath) = iconpath.find_data_file(format!("{iconname}.svg")) {
return Some(iconpath);
}
let iconpath =
xdg::BaseDirectories::with_prefix(&format!("icons/{themes}/apps/64")).unwrap();
if let Some(iconpath) = iconpath.find_data_file(format!("{iconname}.svg")) {
return Some(iconpath);
}
}
None
}

fn get_icon_path(iconname: &str) -> Option<PathBuf> {
if iconname.contains('/') {
PathBuf::from_str(iconname).ok()
} else {
get_icon_path_from_xdgicon(iconname)
}
}

pub fn all_apps() -> Vec<App> {
let re = regex::Regex::new(r"([a-zA-Z]+);").unwrap();
gio::AppInfo::all()
.iter()
.filter(|app| app.should_show() && app.downcast_ref::<gio::DesktopAppInfo>().is_some())
.map(|app| app.clone().downcast::<gio::DesktopAppInfo>().unwrap())
.map(|app| App {
appinfo: app.clone(),
name: app.name().to_string(),
descriptions: app.description(),
categrades: match app.categories() {
None => None,
Some(categrades) => {
let tomatch = categrades.to_string();
let tips = re
.captures_iter(&tomatch)
.map(|unit| unit.get(1).unwrap().as_str().to_string())
.collect();
Some(tips)
}
},
actions: {
let actions = app.list_actions();
if actions.is_empty() {
None
} else {
Some(actions)
}
},
icon: match &app.icon() {
None => None,
Some(icon) => {
let iconname = gio::prelude::IconExt::to_string(icon).unwrap();
get_icon_path(iconname.as_str())
}
},
})
.collect()
}
Loading