gui improvements

* implement normal window
* set placeholder text from config / args
* key handler is now using key codes instead of hard coded values
* drun now is able to spawn an application, although process is not
  forked yet
This commit is contained in:
Alexander Mohr 2025-04-07 22:42:43 +02:00
parent 43cb14b9e8
commit ff22c0c9c6
7 changed files with 194 additions and 106 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
/target /target
.idea

51
Cargo.lock generated
View file

@ -318,6 +318,56 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe1d7dcda7d1da79e444bdfba1465f2f849a58b07774e1df473ee77030cb47a7" checksum = "fe1d7dcda7d1da79e444bdfba1465f2f849a58b07774e1df473ee77030cb47a7"
[[package]]
name = "crossbeam"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-epoch",
"crossbeam-queue",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
dependencies = [
"crossbeam-utils",
]
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.21" version = "0.8.21"
@ -1904,6 +1954,7 @@ dependencies = [
"anyhow", "anyhow",
"calloop 0.14.2", "calloop 0.14.2",
"clap", "clap",
"crossbeam",
"env_logger", "env_logger",
"gdk4", "gdk4",
"gtk4", "gtk4",

View file

@ -25,4 +25,5 @@ wayland-client = "0.31.8"
wayland-protocols = "0.32.6" wayland-protocols = "0.32.6"
smithay-client-toolkit = { version = "0.19.2", features = ["calloop"]} smithay-client-toolkit = { version = "0.19.2", features = ["calloop"]}
calloop = "0.14.2" calloop = "0.14.2"
crossbeam = "0.8.4"
libc = "0.2.171" libc = "0.2.171"

View file

@ -77,11 +77,11 @@ pub struct Args {
/// The x offset /// The x offset
#[clap(short = 'x', long = "xoffset")] #[clap(short = 'x', long = "xoffset")]
x: Option<String>, x: Option<i32>,
/// The y offset /// The y offset
#[clap(short = 'y', long = "yoffset")] #[clap(short = 'y', long = "yoffset")]
y: Option<String>, y: Option<i32>,
/// Render to a normal window /// Render to a normal window
#[clap(short = 'n', long = "normal-window")] #[clap(short = 'n', long = "normal-window")]
@ -149,7 +149,7 @@ pub struct Args {
/// Sets the number of columns to display /// Sets the number of columns to display
#[clap(short = 'w', long = "columns")] #[clap(short = 'w', long = "columns")]
columns: Option<String>, columns: Option<u8>,
/// Sets the sort order /// Sets the sort order
#[clap(short = 'O', long = "sort-order")] #[clap(short = 'O', long = "sort-order")]

View file

@ -144,12 +144,12 @@ impl Default for Config {
} }
fn default_normal_window() -> Option<bool> { fn default_normal_window() -> Option<bool> {
Some(true) Some(false)
} }
// TODO // TODO
// GtkOrientation orientation = config_get_mnemonic(config, "orientation", "vertical", 2, "vertical", "horizontal"); // GtkOrientation orientation = config_get_mnemonic(config, "orientation", "vertical", 2, "vertical", "horizontal");
// outer_orientation = config_get_mnemonic(config, "orientation", "vertical", 2, "horizontal", "vertical"); // outer_orientation = config_get_mnemonic(cstoonfig, "orientation", "vertical", 2, "horizontal", "vertical");
// GtkAlign halign = config_get_mnemonic(config, "halign", "fill", 4, "fill", "start", "end", "center"); // GtkAlign halign = config_get_mnemonic(config, "halign", "fill", 4, "fill", "start", "end", "center");
// content_halign = config_get_mnemonic(config, "content_halign", "fill", 4, "fill", "start", "end", "center"); // content_halign = config_get_mnemonic(config, "content_halign", "fill", 4, "fill", "start", "end", "center");
// char* default_valign = "start"; // char* default_valign = "start";
@ -252,7 +252,9 @@ fn merge_json(a: &mut Value, b: &Value) {
} }
} }
(a_val, b_val) => { (a_val, b_val) => {
if *b_val != Value::Null {
*a_val = b_val.clone(); *a_val = b_val.clone();
} }
} }
} }
}

View file

@ -1,11 +1,13 @@
use crate::config::Config; use crate::config::Config;
use anyhow::Context; use anyhow::{Context, anyhow};
use gdk4::Display; use crossbeam::channel;
use crossbeam::channel::Sender;
use gdk4::gio::File; use gdk4::gio::File;
use gdk4::glib::Propagation; use gdk4::glib::Propagation;
use gdk4::prelude::{Cast, DisplayExt, MonitorExt}; use gdk4::prelude::{Cast, DisplayExt, MonitorExt};
use gdk4::{Display, Key};
use gtk4::prelude::{ use gtk4::prelude::{
ApplicationExt, ApplicationExtManual, BoxExt, ButtonExt, EditableExt, EntryExt, ApplicationExt, ApplicationExtManual, BoxExt, ButtonExt, EditableExt, EntryExt, FileChooserExt,
FlowBoxChildExt, GtkWindowExt, ListBoxRowExt, NativeExt, WidgetExt, FlowBoxChildExt, GtkWindowExt, ListBoxRowExt, NativeExt, WidgetExt,
}; };
use gtk4::{ use gtk4::{
@ -14,17 +16,18 @@ use gtk4::{
}; };
use gtk4::{Application, ApplicationWindow, CssProvider, Orientation}; use gtk4::{Application, ApplicationWindow, CssProvider, Orientation};
use gtk4_layer_shell::{KeyboardMode, LayerShell}; use gtk4_layer_shell::{KeyboardMode, LayerShell};
use log::error; use log::{debug, error, info};
use std::process::exit; use std::process::exit;
#[derive(Clone)]
pub struct EntryElement { pub struct EntryElement {
pub label: String, // todo support empty label? pub label: String, // todo support empty label?
pub icon_path: Option<String>, pub icon_path: Option<String>,
pub action: Box<dyn Fn() + Send + 'static>, pub action: Option<String>,
pub sub_elements: Option<Vec<EntryElement>>, pub sub_elements: Option<Vec<EntryElement>>,
} }
pub fn init(config: Config, elements: Vec<EntryElement>) -> anyhow::Result<()> { pub fn show(config: Config, elements: Vec<EntryElement>) -> anyhow::Result<(i32)> {
// Load CSS // Load CSS
let provider = CssProvider::new(); let provider = CssProvider::new();
let css_file_path = File::for_path("/home/me/.config/wofi/style.css"); let css_file_path = File::for_path("/home/me/.config/wofi/style.css");
@ -48,6 +51,7 @@ pub fn init(config: Config, elements: Vec<EntryElement>) -> anyhow::Result<()> {
// No need for application_id unless you want portal support // No need for application_id unless you want portal support
let app = Application::builder().application_id("ravi").build(); let app = Application::builder().application_id("ravi").build();
let (sender, receiver) = channel::bounded(1);
app.connect_activate(move |app| { app.connect_activate(move |app| {
// Create a toplevel undecorated window // Create a toplevel undecorated window
@ -59,11 +63,16 @@ pub fn init(config: Config, elements: Vec<EntryElement>) -> anyhow::Result<()> {
.default_height(20) .default_height(20)
.build(); .build();
window.set_widget_name("window");
config.normal_window.map(|normal| {
if !normal {
window.set_layer(gtk4_layer_shell::Layer::Overlay);
window.init_layer_shell(); window.init_layer_shell();
window.set_keyboard_mode(KeyboardMode::Exclusive); window.set_keyboard_mode(KeyboardMode::Exclusive);
window.set_widget_name("window"); window.set_namespace(Some("worf"));
window.set_layer(gtk4_layer_shell::Layer::Overlay); }
window.set_namespace(Some("ravi")); });
let outer_box = gtk4::Box::new(Orientation::Vertical, 0); let outer_box = gtk4::Box::new(Orientation::Vertical, 0);
outer_box.set_widget_name("outer-box"); outer_box.set_widget_name("outer-box");
@ -72,18 +81,11 @@ pub fn init(config: Config, elements: Vec<EntryElement>) -> anyhow::Result<()> {
let entry = SearchEntry::new(); let entry = SearchEntry::new();
entry.set_widget_name("input"); entry.set_widget_name("input");
entry.set_css_classes(&["input"]); entry.set_css_classes(&["input"]);
entry.set_placeholder_text(Some("Enter search...")); entry.set_placeholder_text(config.prompt.as_deref());
// Create key event controller
let entry_clone = entry.clone();
setup_key_event_handler(&window, entry_clone);
// Example `search` and `password_char` usage // Example `search` and `password_char` usage
let password_char = Some('*'); // let password_char = Some('*');
// todo\
entry.set_placeholder_text(Some("placeholder"));
// todo
// if let Some(c) = password_char { // if let Some(c) = password_char {
// let entry_casted: Entry = entry.clone().upcast(); // let entry_casted: Entry = entry.clone().upcast();
// entry_casted.set_visibility(false); // entry_casted.set_visibility(false);
@ -119,7 +121,6 @@ pub fn init(config: Config, elements: Vec<EntryElement>) -> anyhow::Result<()> {
add_entry_element(&inner_box, &entry); add_entry_element(&inner_box, &entry);
} }
// todo
// Set focus after everything is realized // Set focus after everything is realized
inner_box.connect_map(|fb| { inner_box.connect_map(|fb| {
fb.grab_focus(); fb.grab_focus();
@ -130,17 +131,19 @@ pub fn init(config: Config, elements: Vec<EntryElement>) -> anyhow::Result<()> {
wrapper_box.append(&inner_box); wrapper_box.append(&inner_box);
scroll.set_child(Some(&wrapper_box)); scroll.set_child(Some(&wrapper_box));
// todo // todo implement search function
// // Dummy filter and sort funcs replace with actual logic // // Dummy filter and sort funcs replace with actual logic
// inner_box.set_filter_func(Some(Box::new(|_child| { // inner_box.set_filter_func(Some(Box::new(|_child| {
// true // filter logic here // true // filter logic here
// }))); // })));
// todo
// inner_box.set_sort_func(Some(Box::new(|child1, child2| { // inner_box.set_sort_func(Some(Box::new(|child1, child2| {
// child1.widget_name().cmp(&child2.widget_name()) // child1.widget_name().cmp(&child2.widget_name())
// }))); // })));
// Create key event controller
let entry_clone = entry.clone();
setup_key_event_handler(&window, entry_clone, inner_box, app.clone(), sender.clone());
window.show(); window.show();
// Get the display where the window resides // Get the display where the window resides
@ -151,18 +154,14 @@ pub fn init(config: Config, elements: Vec<EntryElement>) -> anyhow::Result<()> {
let monitor = display.monitor_at_surface(&surface); let monitor = display.monitor_at_surface(&surface);
if let Some(monitor) = monitor { if let Some(monitor) = monitor {
let geometry = monitor.geometry(); let geometry = monitor.geometry();
if let Some(w) = percent_or_absolute( config.width.as_ref().map(|width| {
&config.width.clone().unwrap_or("800".to_owned()), percent_or_absolute(&width, geometry.width())
geometry.width(), .map(|w| window.set_width_request(w))
) { });
window.set_width_request(w); config.height.as_ref().map(|height| {
} percent_or_absolute(&height, geometry.height())
if let Some(h) = percent_or_absolute( .map(|h| window.set_height_request(h))
&config.height.clone().unwrap_or("500".to_owned()), });
geometry.height(),
) {
window.set_height_request(h);
}
} else { } else {
error!("failed to get monitor to init window size"); error!("failed to get monitor to init window size");
} }
@ -172,17 +171,35 @@ pub fn init(config: Config, elements: Vec<EntryElement>) -> anyhow::Result<()> {
let empty_array: [&str; 0] = []; let empty_array: [&str; 0] = [];
app.run_with_args(&empty_array); app.run_with_args(&empty_array);
Ok(()) let selected_index = receiver.recv()?;
Ok(selected_index)
} }
fn setup_key_event_handler(window: &ApplicationWindow, entry_clone: SearchEntry) { fn setup_key_event_handler(
window: &ApplicationWindow,
entry_clone: SearchEntry,
inner_box: FlowBox,
app: Application,
sender: Sender<i32>,
) {
let key_controller = EventControllerKey::new(); let key_controller = EventControllerKey::new();
let x = key_controller.connect_key_pressed(move |_controller, key_value, code, mode| { key_controller.connect_key_pressed(move |_, key_value, _, _| {
if code == 9 { match key_value {
// todo find better way to handle escape Key::Escape => exit(1),
exit(1); Key::Return => {
for s in &inner_box.selected_children() {
// let element : &Option<&EntryElement> = &elements.get(s.index() as usize);
// if let Some(element) = *element {
// debug!("Running action on element with name {}", element.label);
// (element.action)();
// }
if let Err(e) = sender.send(s.index()) {
error!("failed to send selected child {e:?}")
} }
app.quit();
}
}
_ => {
if let Some(c) = key_value.name() { if let Some(c) = key_value.name() {
// Only proceed if it's a single alphanumeric character // Only proceed if it's a single alphanumeric character
if c.len() == 1 && c.chars().all(|ch| ch.is_alphanumeric()) { if c.len() == 1 && c.chars().all(|ch| ch.is_alphanumeric()) {
@ -190,13 +207,16 @@ fn setup_key_event_handler(window: &ApplicationWindow, entry_clone: SearchEntry)
entry_clone.set_text(&format!("{current}{c}")); entry_clone.set_text(&format!("{current}{c}"));
} }
} }
}
}
Propagation::Proceed Propagation::Proceed
}); });
// Add the controller to the window // Add the controller to the window
window.add_controller(key_controller); window.add_controller(key_controller);
} }
fn add_entry_element(inner_box: &gtk4::FlowBox, entry_element: &EntryElement) { fn add_entry_element(inner_box: &FlowBox, entry_element: &EntryElement) {
let parent: Widget = if entry_element.sub_elements.is_some() { let parent: Widget = if entry_element.sub_elements.is_some() {
let expander = Expander::new(None); let expander = Expander::new(None);

View file

@ -2,7 +2,7 @@
#![allow(clippy::implicit_return)] #![allow(clippy::implicit_return)]
use crate::args::{Args, Mode}; use crate::args::{Args, Mode};
use crate::config::Config; use crate::config::{Config, merge_config_with_args};
use crate::desktop::find_desktop_files; use crate::desktop::find_desktop_files;
use crate::gui::EntryElement; use crate::gui::EntryElement;
use clap::Parser; use clap::Parser;
@ -13,12 +13,13 @@ use gtk4::prelude::{
}; };
use gtk4_layer_shell::LayerShell; use gtk4_layer_shell::LayerShell;
use merge::Merge; use merge::Merge;
use std::fs;
use std::ops::Deref; use std::ops::Deref;
use std::os::unix::process::CommandExt; use std::os::unix::process::CommandExt;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use std::sync::Arc; use std::sync::Arc;
use std::thread::sleep;
use std::{fs, time};
mod args; mod args;
mod config; mod config;
@ -45,10 +46,9 @@ fn main() -> anyhow::Result<()> {
PathBuf::from(home_dir.clone()).join(".config"), PathBuf::from(home_dir.clone()).join(".config"),
|xdg_conf_home| PathBuf::from(&xdg_conf_home), |xdg_conf_home| PathBuf::from(&xdg_conf_home),
) )
.join("wofi") // todo change to ravi .join("wofi") // todo change to worf
.join("config") .join("config")
}); });
// todo use this? // todo use this?
let colors_dir = std::env::var("XDG_CACHE_HOME") let colors_dir = std::env::var("XDG_CACHE_HOME")
.map_or( .map_or(
@ -58,21 +58,36 @@ fn main() -> anyhow::Result<()> {
.join("wal") .join("wal")
.join("colors"); .join("colors");
let drun_cache = std::env::var("XDG_CACHE_HOME")
.map_or(
PathBuf::from(home_dir.clone()).join(".cache"),
|xdg_conf_home| PathBuf::from(&xdg_conf_home),
)
.join("worf-drun"); // todo change to worf
let toml_content = fs::read_to_string(config_path)?; let toml_content = fs::read_to_string(config_path)?;
let mut config: Config = toml::from_str(&toml_content)?; // todo bail out properly let mut config: Config = toml::from_str(&toml_content)?; // todo bail out properly
let config = merge_config_with_args(&mut config, &args)?;
let icon_resolver = desktop::IconResolver::new();
match args.mode { match args.mode {
Mode::Run => {} Mode::Run => {}
Mode::Drun => { Mode::Drun => {
drun(config)?;
}
Mode::Dmenu => {}
}
Ok(())
}
fn drun(mut config: Config) -> anyhow::Result<()> {
let mut entries: Vec<EntryElement> = Vec::new(); let mut entries: Vec<EntryElement> = Vec::new();
for file in &find_desktop_files() { for file in &find_desktop_files() {
if let Some(desktop_entry) = file.get("desktop entry") { if let Some(desktop_entry) = file.get("desktop entry") {
let icon = desktop_entry let icon = desktop_entry
.get("icon") .get("icon")
.and_then(|x| x.as_ref().map(|x| x.to_owned())); .and_then(|x| x.as_ref().map(|x| x.to_owned()));
let Some(exec) = desktop_entry.get("exec").and_then(|x| x.as_ref().cloned()) let Some(exec) = desktop_entry.get("exec").and_then(|x| x.as_ref().cloned()) else {
else {
continue; continue;
}; };
@ -82,14 +97,6 @@ fn main() -> anyhow::Result<()> {
} }
} }
let exec: Arc<String> = Arc::new(exec.into());
let action: Box<dyn Fn() + Send> = {
let exec = Arc::clone(&exec); // ✅ now it's correct
Box::new(move || {
spawn_fork(&exec);
})
};
let name = desktop_entry let name = desktop_entry
.get("name") .get("name")
.and_then(|x| x.as_ref().map(|x| x.to_owned())); .and_then(|x| x.as_ref().map(|x| x.to_owned()));
@ -98,32 +105,38 @@ fn main() -> anyhow::Result<()> {
EntryElement { EntryElement {
label: name, label: name,
icon_path: icon, icon_path: icon,
action, action: Some(exec),
sub_elements: None, sub_elements: None,
} }
}) })
} }
} }
} }
entries.sort_by(|l, r| l.label.cmp(&r.label)); entries.sort_by(|l, r| l.label.cmp(&r.label));
if config.prompt.is_none() { if config.prompt.is_none() {
config.prompt = Some("dmenu".to_owned()); config.prompt = Some("drun".to_owned());
}
gui::init(config.clone(), entries)?;
}
Mode::Dmenu => {}
} }
// todo ues a arc instead of cloning the config
let selected_index = gui::show(config.clone(), entries.clone())?;
entries.get(selected_index as usize).map(|e| {
e.action.as_ref().map(|a| {
spawn_fork(&a);
})
});
Ok(()) Ok(())
} }
fn spawn_fork(cmd: &str) { fn spawn_fork(cmd: &str) {
// todo fork this for real
// Unix-like systems (Linux, macOS) // Unix-like systems (Linux, macOS)
let _ = Command::new(cmd) let _ = Command::new(cmd)
.stdin(Stdio::null()) // Disconnect stdin .stdin(Stdio::null()) // Disconnect stdin
.stdout(Stdio::null()) // Disconnect stdout .stdout(Stdio::null()) // Disconnect stdout
.stderr(Stdio::null()) // Disconnect stderr .stderr(Stdio::null()) // Disconnect stderr
.spawn(); .spawn();
sleep(time::Duration::from_secs(30));
} }
// //
// fn main() -> anyhow::Result<()> { // fn main() -> anyhow::Result<()> {