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
.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"
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]]
name = "crossbeam-utils"
version = "0.8.21"
@ -1904,6 +1954,7 @@ dependencies = [
"anyhow",
"calloop 0.14.2",
"clap",
"crossbeam",
"env_logger",
"gdk4",
"gtk4",

View file

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

View file

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

View file

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

View file

@ -1,11 +1,13 @@
use crate::config::Config;
use anyhow::Context;
use gdk4::Display;
use anyhow::{Context, anyhow};
use crossbeam::channel;
use crossbeam::channel::Sender;
use gdk4::gio::File;
use gdk4::glib::Propagation;
use gdk4::prelude::{Cast, DisplayExt, MonitorExt};
use gdk4::{Display, Key};
use gtk4::prelude::{
ApplicationExt, ApplicationExtManual, BoxExt, ButtonExt, EditableExt, EntryExt,
ApplicationExt, ApplicationExtManual, BoxExt, ButtonExt, EditableExt, EntryExt, FileChooserExt,
FlowBoxChildExt, GtkWindowExt, ListBoxRowExt, NativeExt, WidgetExt,
};
use gtk4::{
@ -14,17 +16,18 @@ use gtk4::{
};
use gtk4::{Application, ApplicationWindow, CssProvider, Orientation};
use gtk4_layer_shell::{KeyboardMode, LayerShell};
use log::error;
use log::{debug, error, info};
use std::process::exit;
#[derive(Clone)]
pub struct EntryElement {
pub label: String, // todo support empty label?
pub icon_path: Option<String>,
pub action: Box<dyn Fn() + Send + 'static>,
pub action: Option<String>,
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
let provider = CssProvider::new();
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
let app = Application::builder().application_id("ravi").build();
let (sender, receiver) = channel::bounded(1);
app.connect_activate(move |app| {
// Create a toplevel undecorated window
@ -59,11 +63,16 @@ pub fn init(config: Config, elements: Vec<EntryElement>) -> anyhow::Result<()> {
.default_height(20)
.build();
window.init_layer_shell();
window.set_keyboard_mode(KeyboardMode::Exclusive);
window.set_widget_name("window");
window.set_layer(gtk4_layer_shell::Layer::Overlay);
window.set_namespace(Some("ravi"));
config.normal_window.map(|normal| {
if !normal {
window.set_layer(gtk4_layer_shell::Layer::Overlay);
window.init_layer_shell();
window.set_keyboard_mode(KeyboardMode::Exclusive);
window.set_namespace(Some("worf"));
}
});
let outer_box = gtk4::Box::new(Orientation::Vertical, 0);
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();
entry.set_widget_name("input");
entry.set_css_classes(&["input"]);
entry.set_placeholder_text(Some("Enter search..."));
// Create key event controller
let entry_clone = entry.clone();
setup_key_event_handler(&window, entry_clone);
entry.set_placeholder_text(config.prompt.as_deref());
// Example `search` and `password_char` usage
let password_char = Some('*');
entry.set_placeholder_text(Some("placeholder"));
// todo
// let password_char = Some('*');
// todo\
// if let Some(c) = password_char {
// let entry_casted: Entry = entry.clone().upcast();
// 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);
}
// todo
// Set focus after everything is realized
inner_box.connect_map(|fb| {
fb.grab_focus();
@ -130,17 +131,19 @@ pub fn init(config: Config, elements: Vec<EntryElement>) -> anyhow::Result<()> {
wrapper_box.append(&inner_box);
scroll.set_child(Some(&wrapper_box));
// todo
// todo implement search function
// // Dummy filter and sort funcs replace with actual logic
// inner_box.set_filter_func(Some(Box::new(|_child| {
// true // filter logic here
// })));
// todo
// inner_box.set_sort_func(Some(Box::new(|child1, child2| {
// 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();
// 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);
if let Some(monitor) = monitor {
let geometry = monitor.geometry();
if let Some(w) = percent_or_absolute(
&config.width.clone().unwrap_or("800".to_owned()),
geometry.width(),
) {
window.set_width_request(w);
}
if let Some(h) = percent_or_absolute(
&config.height.clone().unwrap_or("500".to_owned()),
geometry.height(),
) {
window.set_height_request(h);
}
config.width.as_ref().map(|width| {
percent_or_absolute(&width, geometry.width())
.map(|w| window.set_width_request(w))
});
config.height.as_ref().map(|height| {
percent_or_absolute(&height, geometry.height())
.map(|h| window.set_height_request(h))
});
} else {
error!("failed to get monitor to init window size");
}
@ -172,31 +171,52 @@ pub fn init(config: Config, elements: Vec<EntryElement>) -> anyhow::Result<()> {
let empty_array: [&str; 0] = [];
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 x = key_controller.connect_key_pressed(move |_controller, key_value, code, mode| {
if code == 9 {
// todo find better way to handle escape
exit(1);
}
if let Some(c) = key_value.name() {
// Only proceed if it's a single alphanumeric character
if c.len() == 1 && c.chars().all(|ch| ch.is_alphanumeric()) {
let current = entry_clone.text().to_string();
entry_clone.set_text(&format!("{current}{c}"));
key_controller.connect_key_pressed(move |_, key_value, _, _| {
match key_value {
Key::Escape => 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() {
// Only proceed if it's a single alphanumeric character
if c.len() == 1 && c.chars().all(|ch| ch.is_alphanumeric()) {
let current = entry_clone.text().to_string();
entry_clone.set_text(&format!("{current}{c}"));
}
}
}
}
Propagation::Proceed
});
// Add the controller to the window
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 expander = Expander::new(None);

View file

@ -2,7 +2,7 @@
#![allow(clippy::implicit_return)]
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::gui::EntryElement;
use clap::Parser;
@ -13,12 +13,13 @@ use gtk4::prelude::{
};
use gtk4_layer_shell::LayerShell;
use merge::Merge;
use std::fs;
use std::ops::Deref;
use std::os::unix::process::CommandExt;
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::Arc;
use std::thread::sleep;
use std::{fs, time};
mod args;
mod config;
@ -45,10 +46,9 @@ fn main() -> anyhow::Result<()> {
PathBuf::from(home_dir.clone()).join(".config"),
|xdg_conf_home| PathBuf::from(&xdg_conf_home),
)
.join("wofi") // todo change to ravi
.join("wofi") // todo change to worf
.join("config")
});
// todo use this?
let colors_dir = std::env::var("XDG_CACHE_HOME")
.map_or(
@ -58,58 +58,21 @@ fn main() -> anyhow::Result<()> {
.join("wal")
.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 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 {
Mode::Run => {}
Mode::Drun => {
let mut entries: Vec<EntryElement> = Vec::new();
for file in &find_desktop_files() {
if let Some(desktop_entry) = file.get("desktop entry") {
let icon = desktop_entry
.get("icon")
.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())
else {
continue;
};
if let Some((cmd, _)) = exec.split_once(' ') {
if !PathBuf::from(cmd).exists() {
continue;
}
}
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
.get("name")
.and_then(|x| x.as_ref().map(|x| x.to_owned()));
if let Some(name) = name {
entries.push({
EntryElement {
label: name,
icon_path: icon,
action,
sub_elements: None,
}
})
}
}
}
entries.sort_by(|l, r| l.label.cmp(&r.label));
if config.prompt.is_none() {
config.prompt = Some("dmenu".to_owned());
}
gui::init(config.clone(), entries)?;
drun(config)?;
}
Mode::Dmenu => {}
}
@ -117,13 +80,63 @@ fn main() -> anyhow::Result<()> {
Ok(())
}
fn drun(mut config: Config) -> anyhow::Result<()> {
let mut entries: Vec<EntryElement> = Vec::new();
for file in &find_desktop_files() {
if let Some(desktop_entry) = file.get("desktop entry") {
let icon = desktop_entry
.get("icon")
.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()) else {
continue;
};
if let Some((cmd, _)) = exec.split_once(' ') {
if !PathBuf::from(cmd).exists() {
continue;
}
}
let name = desktop_entry
.get("name")
.and_then(|x| x.as_ref().map(|x| x.to_owned()));
if let Some(name) = name {
entries.push({
EntryElement {
label: name,
icon_path: icon,
action: Some(exec),
sub_elements: None,
}
})
}
}
}
entries.sort_by(|l, r| l.label.cmp(&r.label));
if config.prompt.is_none() {
config.prompt = Some("drun".to_owned());
}
// 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(())
}
fn spawn_fork(cmd: &str) {
// todo fork this for real
// Unix-like systems (Linux, macOS)
let _ = Command::new(cmd)
.stdin(Stdio::null()) // Disconnect stdin
.stdout(Stdio::null()) // Disconnect stdout
.stderr(Stdio::null()) // Disconnect stderr
.spawn();
sleep(time::Duration::from_secs(30));
}
//
// fn main() -> anyhow::Result<()> {