Skip to content

Commit

Permalink
Merge pull request #2 from Decodetalkers/launcher
Browse files Browse the repository at this point in the history
feat: test multi layershell feature
  • Loading branch information
Decodetalkers authored Aug 8, 2024
2 parents 42c04e1 + 1839264 commit 0b8aa69
Show file tree
Hide file tree
Showing 9 changed files with 1,034 additions and 143 deletions.
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

0 comments on commit 0b8aa69

Please sign in to comment.