Had claude un-abstract all that BS
This commit is contained in:
parent
2d909dd716
commit
7cf1b9efc7
10
src/main.rs
10
src/main.rs
@ -2,7 +2,6 @@ use gtk::Application;
|
|||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use waycast::launcher::WaycastLauncher;
|
use waycast::launcher::WaycastLauncher;
|
||||||
use waycast::plugins;
|
use waycast::plugins;
|
||||||
use waycast::ui::controller::LauncherController;
|
|
||||||
use waycast::ui::gtk::GtkLauncherUI;
|
use waycast::ui::gtk::GtkLauncherUI;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@ -17,12 +16,9 @@ fn main() {
|
|||||||
.add_plugin(Box::new(plugins::file_search::FileSearchPlugin::new()))
|
.add_plugin(Box::new(plugins::file_search::FileSearchPlugin::new()))
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
// Create the GTK UI
|
// Create and show the GTK UI
|
||||||
let ui = GtkLauncherUI::new(app);
|
let ui = GtkLauncherUI::new(app, launcher);
|
||||||
|
ui.show();
|
||||||
// Create and run the controller
|
|
||||||
let controller = LauncherController::new(launcher, ui, app.clone());
|
|
||||||
controller.run();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.run();
|
app.run();
|
||||||
|
@ -1,107 +0,0 @@
|
|||||||
use super::gtk::GtkLauncherUI;
|
|
||||||
use super::traits::{LauncherUI, UIEvent};
|
|
||||||
use crate::{LaunchError, launcher::WaycastLauncher};
|
|
||||||
use gio::prelude::ApplicationExt;
|
|
||||||
use gtk::glib;
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::rc::Rc;
|
|
||||||
use std::sync::mpsc::{self, Receiver};
|
|
||||||
|
|
||||||
/// Controller that coordinates between the core launcher and UI
|
|
||||||
pub struct LauncherController {
|
|
||||||
launcher: Rc<RefCell<WaycastLauncher>>,
|
|
||||||
ui: GtkLauncherUI,
|
|
||||||
event_receiver: Receiver<UIEvent>,
|
|
||||||
app: gtk::Application,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LauncherController {
|
|
||||||
pub fn new(launcher: WaycastLauncher, mut ui: GtkLauncherUI, app: gtk::Application) -> Self {
|
|
||||||
let (event_sender, event_receiver) = mpsc::channel();
|
|
||||||
|
|
||||||
// Set up the event sender in the UI
|
|
||||||
ui.set_event_sender(event_sender);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
launcher: Rc::new(RefCell::new(launcher)),
|
|
||||||
ui,
|
|
||||||
event_receiver,
|
|
||||||
app,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn initialize(&mut self) {
|
|
||||||
// Populate with default results
|
|
||||||
let mut launcher = self.launcher.borrow_mut();
|
|
||||||
let results = launcher.get_default_results();
|
|
||||||
self.ui.set_results(results);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn show(&self) {
|
|
||||||
self.ui.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handle_event(&mut self, event: UIEvent) -> Result<(), LaunchError> {
|
|
||||||
match event {
|
|
||||||
UIEvent::SearchChanged(query) => {
|
|
||||||
let mut launcher = self.launcher.borrow_mut();
|
|
||||||
let results = if query.trim().is_empty() {
|
|
||||||
launcher.get_default_results()
|
|
||||||
} else {
|
|
||||||
launcher.search(&query)
|
|
||||||
};
|
|
||||||
self.ui.set_results(results);
|
|
||||||
}
|
|
||||||
|
|
||||||
UIEvent::ItemActivated(index) => match self.launcher.borrow().execute_item(index) {
|
|
||||||
Ok(_) => {
|
|
||||||
// Exit the application completely instead of just hiding
|
|
||||||
self.app.quit();
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Failed to launch item: {:?}", e);
|
|
||||||
return Err(e);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
UIEvent::ItemSelected(_index) => {
|
|
||||||
// Handle selection change if needed
|
|
||||||
// For now, this is just for keyboard navigation
|
|
||||||
}
|
|
||||||
|
|
||||||
UIEvent::CloseRequested => {
|
|
||||||
// Exit the application completely instead of just hiding
|
|
||||||
self.app.quit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn process_events(&mut self) -> Result<(), LaunchError> {
|
|
||||||
while let Ok(event) = self.event_receiver.try_recv() {
|
|
||||||
self.handle_event(event)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run(mut self) {
|
|
||||||
self.initialize();
|
|
||||||
self.show();
|
|
||||||
|
|
||||||
// Set up periodic event processing using glib's idle callback
|
|
||||||
let controller = Rc::new(RefCell::new(self));
|
|
||||||
let controller_clone = controller.clone();
|
|
||||||
|
|
||||||
glib::idle_add_local(move || {
|
|
||||||
if let Ok(mut ctrl) = controller_clone.try_borrow_mut() {
|
|
||||||
if let Err(e) = ctrl.process_events() {
|
|
||||||
eprintln!("Error processing events: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
glib::ControlFlow::Continue
|
|
||||||
});
|
|
||||||
|
|
||||||
// The GTK main loop will handle the rest
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +1,9 @@
|
|||||||
use crate::LauncherListItem;
|
use crate::launcher::WaycastLauncher;
|
||||||
use super::traits::{LauncherUI, UIEvent};
|
|
||||||
use gtk::gdk::Texture;
|
use gtk::gdk::Texture;
|
||||||
use gtk::gdk_pixbuf::Pixbuf;
|
use gtk::gdk_pixbuf::Pixbuf;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use gtk::{
|
use gtk::{
|
||||||
Application, ApplicationWindow, Box as GtkBox, Entry, EventControllerKey, IconTheme, Image,
|
ApplicationWindow, Box as GtkBox, Entry, EventControllerKey, IconTheme, Image,
|
||||||
Label, ListView, Orientation, ScrolledWindow, SignalListItemFactory, SingleSelection,
|
Label, ListView, Orientation, ScrolledWindow, SignalListItemFactory, SingleSelection,
|
||||||
};
|
};
|
||||||
use gio::ListStore;
|
use gio::ListStore;
|
||||||
@ -12,7 +11,9 @@ use gtk::subclass::prelude::ObjectSubclassIsExt;
|
|||||||
use gtk4_layer_shell as layerShell;
|
use gtk4_layer_shell as layerShell;
|
||||||
use layerShell::LayerShell;
|
use layerShell::LayerShell;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::mpsc::Sender;
|
use std::rc::Rc;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use gio::prelude::ApplicationExt;
|
||||||
|
|
||||||
// GObject wrapper to store LauncherListItem in GTK's model system
|
// GObject wrapper to store LauncherListItem in GTK's model system
|
||||||
mod imp {
|
mod imp {
|
||||||
@ -75,12 +76,13 @@ pub struct GtkLauncherUI {
|
|||||||
list_store: ListStore,
|
list_store: ListStore,
|
||||||
selection: SingleSelection,
|
selection: SingleSelection,
|
||||||
search_input: Entry,
|
search_input: Entry,
|
||||||
event_sender: Option<Sender<UIEvent>>,
|
launcher: Rc<RefCell<WaycastLauncher>>,
|
||||||
current_results: Vec<Box<dyn LauncherListItem>>, // Keep reference for indexing
|
app: gtk::Application,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GtkLauncherUI {
|
impl GtkLauncherUI {
|
||||||
pub fn new(app: &Application) -> Self {
|
pub fn new(app: >k::Application, launcher: WaycastLauncher) -> Self {
|
||||||
|
let launcher = Rc::new(RefCell::new(launcher));
|
||||||
let window = ApplicationWindow::builder()
|
let window = ApplicationWindow::builder()
|
||||||
.application(app)
|
.application(app)
|
||||||
.title("Waycast")
|
.title("Waycast")
|
||||||
@ -181,115 +183,20 @@ impl GtkLauncherUI {
|
|||||||
// Set initial focus to search input so user can start typing immediately
|
// Set initial focus to search input so user can start typing immediately
|
||||||
search_input.grab_focus();
|
search_input.grab_focus();
|
||||||
|
|
||||||
Self {
|
// Set up event handlers directly
|
||||||
window,
|
let launcher_for_search = launcher.clone();
|
||||||
list_view,
|
let list_store_for_search = list_store.clone();
|
||||||
list_store,
|
search_input.connect_changed(move |entry| {
|
||||||
selection,
|
|
||||||
search_input,
|
|
||||||
event_sender: None,
|
|
||||||
current_results: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_event_sender(&mut self, sender: Sender<UIEvent>) {
|
|
||||||
self.event_sender = Some(sender.clone());
|
|
||||||
|
|
||||||
// Connect search input signal
|
|
||||||
let sender_clone = sender.clone();
|
|
||||||
self.search_input.connect_changed(move |entry| {
|
|
||||||
let query = entry.text().to_string();
|
let query = entry.text().to_string();
|
||||||
let _ = sender_clone.send(UIEvent::SearchChanged(query));
|
let mut launcher_ref = launcher_for_search.borrow_mut();
|
||||||
});
|
let results = if query.trim().is_empty() {
|
||||||
|
launcher_ref.get_default_results()
|
||||||
// Connect Enter key activation for search input
|
|
||||||
let sender_clone = sender.clone();
|
|
||||||
let selection_clone = self.selection.clone();
|
|
||||||
self.search_input.connect_activate(move |_| {
|
|
||||||
if let Some(selected_item) = selection_clone.selected_item() {
|
|
||||||
if let Some(item_obj) = selected_item.downcast_ref::<LauncherItemObject>() {
|
|
||||||
let index = item_obj.index();
|
|
||||||
let _ = sender_clone.send(UIEvent::ItemActivated(index));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add key handler for launcher-style navigation
|
|
||||||
let search_key_controller = EventControllerKey::new();
|
|
||||||
let selection_clone = self.selection.clone();
|
|
||||||
search_key_controller.connect_key_pressed(move |_controller, keyval, _keycode, _state| {
|
|
||||||
match keyval {
|
|
||||||
gtk::gdk::Key::Down => {
|
|
||||||
let current_pos = selection_clone.selected();
|
|
||||||
let n_items = selection_clone.model().unwrap().n_items();
|
|
||||||
if current_pos < n_items - 1 {
|
|
||||||
selection_clone.set_selected(current_pos + 1);
|
|
||||||
} else if n_items > 0 && current_pos == gtk::INVALID_LIST_POSITION {
|
|
||||||
selection_clone.set_selected(0);
|
|
||||||
}
|
|
||||||
gtk::glib::Propagation::Stop
|
|
||||||
}
|
|
||||||
gtk::gdk::Key::Up => {
|
|
||||||
let current_pos = selection_clone.selected();
|
|
||||||
if current_pos > 0 {
|
|
||||||
selection_clone.set_selected(current_pos - 1);
|
|
||||||
}
|
|
||||||
gtk::glib::Propagation::Stop
|
|
||||||
}
|
|
||||||
_ => gtk::glib::Propagation::Proceed,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
self.search_input.add_controller(search_key_controller);
|
|
||||||
|
|
||||||
// Add ESC key handler at window level
|
|
||||||
let window_key_controller = EventControllerKey::new();
|
|
||||||
let sender_clone = sender.clone();
|
|
||||||
window_key_controller.connect_key_pressed(move |_controller, keyval, _keycode, _state| {
|
|
||||||
if keyval == gtk::gdk::Key::Escape {
|
|
||||||
let _ = sender_clone.send(UIEvent::CloseRequested);
|
|
||||||
gtk::glib::Propagation::Stop
|
|
||||||
} else {
|
} else {
|
||||||
gtk::glib::Propagation::Proceed
|
launcher_ref.search(&query)
|
||||||
}
|
};
|
||||||
});
|
|
||||||
self.window.add_controller(window_key_controller);
|
|
||||||
|
|
||||||
// Connect list activation signal
|
// Update the list store
|
||||||
let sender_clone = sender;
|
list_store_for_search.remove_all();
|
||||||
self.list_view.connect_activate(move |_, position| {
|
|
||||||
let _ = sender_clone.send(UIEvent::ItemActivated(position as usize));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LauncherUI for GtkLauncherUI {
|
|
||||||
fn show(&self) {
|
|
||||||
self.window.present();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hide(&self) {
|
|
||||||
self.window.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_results(&mut self, results: &[Box<dyn LauncherListItem>]) {
|
|
||||||
// Clear the list store
|
|
||||||
self.list_store.remove_all();
|
|
||||||
|
|
||||||
// Store results for indexing
|
|
||||||
self.current_results = results.iter()
|
|
||||||
.map(|item| {
|
|
||||||
// Create a simple wrapper that implements the trait
|
|
||||||
// This is a workaround since we can't clone trait objects
|
|
||||||
Box::new(SimpleListItem {
|
|
||||||
title: item.title(),
|
|
||||||
description: item.description(),
|
|
||||||
icon: item.icon(),
|
|
||||||
original_ptr: 0, // Not used for execution
|
|
||||||
}) as Box<dyn LauncherListItem>
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Add all entries to the store
|
|
||||||
for (index, entry) in results.iter().enumerate() {
|
for (index, entry) in results.iter().enumerate() {
|
||||||
let item_obj = LauncherItemObject::new(
|
let item_obj = LauncherItemObject::new(
|
||||||
entry.title(),
|
entry.title(),
|
||||||
@ -297,45 +204,110 @@ impl LauncherUI for GtkLauncherUI {
|
|||||||
entry.icon(),
|
entry.icon(),
|
||||||
index
|
index
|
||||||
);
|
);
|
||||||
self.list_store.append(&item_obj);
|
list_store_for_search.append(&item_obj);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Connect Enter key activation for search input
|
||||||
|
let launcher_for_enter = launcher.clone();
|
||||||
|
let selection_for_enter = selection.clone();
|
||||||
|
let app_for_enter = app.clone();
|
||||||
|
search_input.connect_activate(move |_| {
|
||||||
|
if let Some(selected_item) = selection_for_enter.selected_item() {
|
||||||
|
if let Some(item_obj) = selected_item.downcast_ref::<LauncherItemObject>() {
|
||||||
|
let index = item_obj.index();
|
||||||
|
match launcher_for_enter.borrow().execute_item(index) {
|
||||||
|
Ok(_) => app_for_enter.quit(),
|
||||||
|
Err(e) => eprintln!("Failed to launch app: {:?}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add key handler for launcher-style navigation
|
||||||
|
let search_key_controller = EventControllerKey::new();
|
||||||
|
let selection_for_keys = selection.clone();
|
||||||
|
search_key_controller.connect_key_pressed(move |_controller, keyval, _keycode, _state| {
|
||||||
|
match keyval {
|
||||||
|
gtk::gdk::Key::Down => {
|
||||||
|
let current_pos = selection_for_keys.selected();
|
||||||
|
let n_items = selection_for_keys.model().unwrap().n_items();
|
||||||
|
if current_pos < n_items - 1 {
|
||||||
|
selection_for_keys.set_selected(current_pos + 1);
|
||||||
|
} else if n_items > 0 && current_pos == gtk::INVALID_LIST_POSITION {
|
||||||
|
selection_for_keys.set_selected(0);
|
||||||
|
}
|
||||||
|
gtk::glib::Propagation::Stop
|
||||||
|
}
|
||||||
|
gtk::gdk::Key::Up => {
|
||||||
|
let current_pos = selection_for_keys.selected();
|
||||||
|
if current_pos > 0 {
|
||||||
|
selection_for_keys.set_selected(current_pos - 1);
|
||||||
|
}
|
||||||
|
gtk::glib::Propagation::Stop
|
||||||
|
}
|
||||||
|
_ => gtk::glib::Propagation::Proceed,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
search_input.add_controller(search_key_controller);
|
||||||
|
|
||||||
|
// Add ESC key handler at window level
|
||||||
|
let window_key_controller = EventControllerKey::new();
|
||||||
|
let app_for_esc = app.clone();
|
||||||
|
window_key_controller.connect_key_pressed(move |_controller, keyval, _keycode, _state| {
|
||||||
|
if keyval == gtk::gdk::Key::Escape {
|
||||||
|
app_for_esc.quit();
|
||||||
|
gtk::glib::Propagation::Stop
|
||||||
|
} else {
|
||||||
|
gtk::glib::Propagation::Proceed
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.add_controller(window_key_controller);
|
||||||
|
|
||||||
|
// Connect list activation signal
|
||||||
|
let launcher_for_activate = launcher.clone();
|
||||||
|
let app_for_activate = app.clone();
|
||||||
|
list_view.connect_activate(move |_, position| {
|
||||||
|
match launcher_for_activate.borrow().execute_item(position as usize) {
|
||||||
|
Ok(_) => app_for_activate.quit(),
|
||||||
|
Err(e) => eprintln!("Failed to launch app: {:?}", e),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize with default results
|
||||||
|
let mut launcher_ref = launcher.borrow_mut();
|
||||||
|
let results = launcher_ref.get_default_results();
|
||||||
|
for (index, entry) in results.iter().enumerate() {
|
||||||
|
let item_obj = LauncherItemObject::new(
|
||||||
|
entry.title(),
|
||||||
|
entry.description(),
|
||||||
|
entry.icon(),
|
||||||
|
index
|
||||||
|
);
|
||||||
|
list_store.append(&item_obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Select the first item if available
|
// Select the first item if available
|
||||||
if self.list_store.n_items() > 0 {
|
if list_store.n_items() > 0 {
|
||||||
self.selection.set_selected(0);
|
selection.set_selected(0);
|
||||||
|
}
|
||||||
|
drop(launcher_ref); // Release the borrow
|
||||||
|
|
||||||
|
Self {
|
||||||
|
window,
|
||||||
|
list_view,
|
||||||
|
list_store,
|
||||||
|
selection,
|
||||||
|
search_input,
|
||||||
|
launcher,
|
||||||
|
app: app.clone(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_visible(&self) -> bool {
|
impl GtkLauncherUI {
|
||||||
self.window.is_visible()
|
pub fn show(&self) {
|
||||||
}
|
self.window.present();
|
||||||
}
|
|
||||||
|
|
||||||
// Helper struct to store launcher item data
|
|
||||||
struct SimpleListItem {
|
|
||||||
title: String,
|
|
||||||
description: Option<String>,
|
|
||||||
icon: String,
|
|
||||||
original_ptr: usize, // Not used for execution, just for storage
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LauncherListItem for SimpleListItem {
|
|
||||||
fn title(&self) -> String {
|
|
||||||
self.title.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn description(&self) -> Option<String> {
|
|
||||||
self.description.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn icon(&self) -> String {
|
|
||||||
self.icon.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn execute(&self) -> Result<(), crate::LaunchError> {
|
|
||||||
// This should never be called - the controller handles execution
|
|
||||||
// using the original items
|
|
||||||
Err(crate::LaunchError::CouldNotLaunch("Cannot execute from UI wrapper".into()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,4 @@
|
|||||||
pub mod traits;
|
|
||||||
pub mod controller;
|
|
||||||
pub mod gtk;
|
pub mod gtk;
|
||||||
|
|
||||||
// Re-export commonly used items
|
// Re-export commonly used items
|
||||||
pub use traits::{LauncherUI, UIEvent, ControllerEvent};
|
|
||||||
pub use controller::LauncherController;
|
|
||||||
pub use gtk::GtkLauncherUI;
|
pub use gtk::GtkLauncherUI;
|
@ -1,40 +0,0 @@
|
|||||||
use crate::LauncherListItem;
|
|
||||||
|
|
||||||
/// Trait defining the interface for any launcher UI implementation
|
|
||||||
pub trait LauncherUI {
|
|
||||||
/// Show the launcher UI
|
|
||||||
fn show(&self);
|
|
||||||
|
|
||||||
/// Hide the launcher UI
|
|
||||||
fn hide(&self);
|
|
||||||
|
|
||||||
/// Update the UI with new search results
|
|
||||||
fn set_results(&mut self, results: &[Box<dyn LauncherListItem>]);
|
|
||||||
|
|
||||||
/// Check if the UI is currently visible
|
|
||||||
fn is_visible(&self) -> bool;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Events that the UI can send to the controller
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum UIEvent {
|
|
||||||
/// User typed in the search box
|
|
||||||
SearchChanged(String),
|
|
||||||
/// User selected an item (by index)
|
|
||||||
ItemSelected(usize),
|
|
||||||
/// User activated an item (pressed Enter or clicked)
|
|
||||||
ItemActivated(usize),
|
|
||||||
/// User requested to close the launcher (Escape key)
|
|
||||||
CloseRequested,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Events that the controller can send to the UI
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum ControllerEvent {
|
|
||||||
/// Results have been updated
|
|
||||||
ResultsUpdated,
|
|
||||||
/// An error occurred
|
|
||||||
Error(String),
|
|
||||||
/// Close the UI
|
|
||||||
Close,
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user