-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from Decodetalkers/launcher
feat: test multi layershell feature
- Loading branch information
Showing
9 changed files
with
1,034 additions
and
143 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
Oops, something went wrong.