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:
parent
43cb14b9e8
commit
ff22c0c9c6
7 changed files with 194 additions and 106 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
|||
/target
|
||||
.idea
|
||||
|
|
51
Cargo.lock
generated
51
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
120
src/gui.rs
120
src/gui.rs
|
@ -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: >k4::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);
|
||||
|
||||
|
|
113
src/main.rs
113
src/main.rs
|
@ -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<()> {
|
||||
|
|
Loading…
Add table
Reference in a new issue