feat: restructure k-launcher workspace and add core functionality
- Updated Cargo.toml to include a new k-launcher crate and reorganized workspace members. - Introduced a README.md file detailing the project philosophy, architecture, and technical specifications. - Implemented a new Kernel struct in k-launcher-kernel for managing plugins and search functionality. - Created a Plugin trait for plugins to implement, allowing for asynchronous search operations. - Developed k-launcher-ui with an Iced-based UI for user interaction, including search input and result display. - Added AppsPlugin and CalcPlugin to handle application launching and basic calculations, respectively. - Established a theme module for UI styling, focusing on an Aero aesthetic. - Removed unnecessary main.rs files from plugin crates, streamlining the project structure.
This commit is contained in:
179
crates/k-launcher-ui/src/app.rs
Normal file
179
crates/k-launcher-ui/src/app.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use iced::{
|
||||
Color, Element, Length, Size, Subscription, Task,
|
||||
keyboard::{self, Event as KeyEvent, Key, key::Named},
|
||||
widget::{column, container, image, row, scrollable, svg, text, text_input, Space},
|
||||
window,
|
||||
};
|
||||
|
||||
use k_launcher_kernel::{Kernel, SearchResult};
|
||||
|
||||
use crate::theme::AeroColors;
|
||||
|
||||
static INPUT_ID: std::sync::LazyLock<iced::widget::Id> =
|
||||
std::sync::LazyLock::new(|| iced::widget::Id::new("search"));
|
||||
|
||||
pub struct KLauncherApp {
|
||||
kernel: Arc<Kernel>,
|
||||
query: String,
|
||||
results: Arc<Vec<SearchResult>>,
|
||||
selected: usize,
|
||||
}
|
||||
|
||||
impl KLauncherApp {
|
||||
fn new(kernel: Arc<Kernel>) -> Self {
|
||||
Self {
|
||||
kernel,
|
||||
query: String::new(),
|
||||
results: Arc::new(vec![]),
|
||||
selected: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Message {
|
||||
QueryChanged(String),
|
||||
ResultsReady(Arc<Vec<SearchResult>>),
|
||||
KeyPressed(KeyEvent),
|
||||
}
|
||||
|
||||
fn update(state: &mut KLauncherApp, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::QueryChanged(q) => {
|
||||
state.query = q.clone();
|
||||
state.selected = 0;
|
||||
let kernel = state.kernel.clone();
|
||||
Task::perform(
|
||||
async move { kernel.search(&q).await },
|
||||
|results| Message::ResultsReady(Arc::new(results)),
|
||||
)
|
||||
}
|
||||
Message::ResultsReady(results) => {
|
||||
state.results = results;
|
||||
Task::none()
|
||||
}
|
||||
Message::KeyPressed(event) => {
|
||||
let key = match event {
|
||||
KeyEvent::KeyPressed { key, .. } => key,
|
||||
_ => return Task::none(),
|
||||
};
|
||||
let Key::Named(named) = key else {
|
||||
return Task::none();
|
||||
};
|
||||
let len = state.results.len();
|
||||
match named {
|
||||
Named::Escape => std::process::exit(0),
|
||||
Named::ArrowDown => {
|
||||
if len > 0 {
|
||||
state.selected = (state.selected + 1).min(len - 1);
|
||||
}
|
||||
}
|
||||
Named::ArrowUp => {
|
||||
if state.selected > 0 {
|
||||
state.selected -= 1;
|
||||
}
|
||||
}
|
||||
Named::Enter => {
|
||||
if let Some(result) = state.results.get(state.selected) {
|
||||
(result.on_execute)();
|
||||
}
|
||||
std::process::exit(0);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(state: &KLauncherApp) -> Element<'_, Message> {
|
||||
let colors = AeroColors::standard();
|
||||
|
||||
let search_bar = text_input("Search...", &state.query)
|
||||
.id(INPUT_ID.clone())
|
||||
.on_input(Message::QueryChanged)
|
||||
.padding(12)
|
||||
.size(18);
|
||||
|
||||
let result_rows: Vec<Element<'_, Message>> = state
|
||||
.results
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, result)| {
|
||||
let is_selected = i == state.selected;
|
||||
let bg_color = if is_selected {
|
||||
colors.border_cyan
|
||||
} else {
|
||||
Color::from_rgba8(255, 255, 255, 0.07)
|
||||
};
|
||||
let icon_el: Element<'_, Message> = match &result.icon {
|
||||
Some(p) if p.ends_with(".svg") =>
|
||||
svg(svg::Handle::from_path(p)).width(24).height(24).into(),
|
||||
Some(p) =>
|
||||
image(image::Handle::from_path(p)).width(24).height(24).into(),
|
||||
None => Space::new().width(24).height(24).into(),
|
||||
};
|
||||
container(
|
||||
row![icon_el, text(result.title.as_str()).size(15)]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.padding([6, 12])
|
||||
.style(move |_theme| container::Style {
|
||||
background: Some(iced::Background::Color(bg_color)),
|
||||
..Default::default()
|
||||
})
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let results_list =
|
||||
scrollable(column(result_rows).spacing(2).width(Length::Fill)).height(Length::Fill);
|
||||
|
||||
let content = column![search_bar, results_list]
|
||||
.spacing(8)
|
||||
.padding(12)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill);
|
||||
|
||||
container(content)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.style(|_theme| container::Style {
|
||||
background: Some(iced::Background::Color(Color::from_rgba8(
|
||||
20, 20, 30, 0.9,
|
||||
))),
|
||||
..Default::default()
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
fn subscription(_state: &KLauncherApp) -> Subscription<Message> {
|
||||
keyboard::listen().map(Message::KeyPressed)
|
||||
}
|
||||
|
||||
pub fn run(kernel: Arc<Kernel>) -> iced::Result {
|
||||
iced::application(
|
||||
move || {
|
||||
let app = KLauncherApp::new(kernel.clone());
|
||||
let focus = iced::widget::operation::focus(INPUT_ID.clone());
|
||||
(app, focus)
|
||||
},
|
||||
update,
|
||||
view,
|
||||
)
|
||||
.title("K-Launcher")
|
||||
.subscription(subscription)
|
||||
.window(window::Settings {
|
||||
size: Size::new(600.0, 400.0),
|
||||
position: window::Position::Centered,
|
||||
decorations: false,
|
||||
transparent: true,
|
||||
resizable: false,
|
||||
..Default::default()
|
||||
})
|
||||
.run()
|
||||
}
|
||||
Reference in New Issue
Block a user