Files
wldash/src/widgets/launcher.rs
T

900 lines
26 KiB
Rust
Raw Normal View History

2023-01-13 21:53:07 +01:00
use std::{
2023-01-16 00:25:34 +01:00
cmp::{max, min, Ordering},
2023-01-13 21:53:07 +01:00
collections::HashMap,
default::Default,
2023-02-08 22:53:41 +01:00
fs::{read_to_string, File},
io::Write,
2023-01-13 21:53:07 +01:00
process::{exit, Command},
sync::{Arc, Mutex},
thread,
};
2023-01-11 00:28:54 +01:00
use crate::{
buffer::BufferView,
color::Color,
2023-02-08 22:53:41 +01:00
event::{Event, Events, PointerButton, PointerEvent},
2023-01-11 00:28:54 +01:00
fonts::FontMap,
keyboard::{keysyms, KeyEvent},
2023-04-04 01:53:02 +02:00
utils::{
2025-01-12 23:10:21 +01:00
desktop::{load_desktop_cache, load_desktop_files, write_desktop_cache, Desktop},
2023-04-04 01:53:02 +02:00
xdg,
},
2023-01-11 00:28:54 +01:00
widgets::{Geometry, Widget},
};
use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher;
use unicode_segmentation::UnicodeSegmentation;
use wayland_client::{protocol::wl_keyboard, WEnum};
struct Matcher {
matches: HashMap<Desktop, i64>,
}
impl Matcher {
fn new() -> Self {
Self {
matches: HashMap::new(),
}
}
fn try_match(&mut self, dtop: Desktop, val: &str, input: &str, prio: f32) {
let fuzzy_matcher = SkimMatcherV2::default();
if let Some(ma) = fuzzy_matcher.fuzzy_match(val, input) {
let ma = ((ma as f32) * prio) as i64;
if let Some(ma_old) = self.matches.get(&dtop) {
// Skip over new matches for the same program that are worse
// than the one we already have.
if ma_old > &ma {
return;
}
}
self.matches.insert(dtop, ma);
}
}
fn matches(&self) -> Vec<Desktop> {
let mut m = self
.matches
.iter()
.map(|(key, ma)| (*ma, key.clone()))
.collect::<Vec<(i64, Desktop)>>();
m.sort_by(|(ma1, d1), (ma2, d2)| {
if ma1 > ma2 {
Ordering::Less
} else if ma1 < ma2 {
Ordering::Greater
} else {
d1.cmp(d2)
}
});
m.into_iter().map(|(_, x)| x).collect()
}
}
enum PromptMode {
Normal,
Shell,
Calc,
}
struct Prompt {
input: String,
mode: PromptMode,
cursor: usize,
}
impl Prompt {
fn new() -> Prompt {
Prompt {
input: String::new(),
mode: PromptMode::Normal,
cursor: 0,
}
}
/// Length of the input in graphemes. The cursor is a grapheme index, not
/// a byte offset, so this (and never `input.len()`) is the upper bound.
fn len(&self) -> usize {
self.input.graphemes(true).count()
}
2023-01-11 00:28:54 +01:00
fn move_cursor(&mut self, distance: isize) {
let new_cursor = self.cursor as isize + distance;
let len = self.len() as isize;
2023-01-11 00:28:54 +01:00
if new_cursor < 0 {
self.cursor = 0;
} else if new_cursor > len {
self.cursor = len as usize;
2023-01-11 00:28:54 +01:00
} else {
self.cursor = new_cursor as usize;
}
}
2023-02-06 18:52:31 +01:00
fn set(&mut self, v: &str) {
self.input = v.to_string();
self.cursor = self.len();
2023-02-06 18:52:31 +01:00
}
2023-01-11 00:28:54 +01:00
fn append(&mut self, v: &str) {
if self.input.len() == 0 {
match v {
"!" => {
self.mode = PromptMode::Shell;
return;
2023-01-12 00:13:27 +01:00
}
2023-01-11 00:28:54 +01:00
"=" => {
self.mode = PromptMode::Calc;
return;
2023-01-12 00:13:27 +01:00
}
2023-01-11 00:28:54 +01:00
_ => (),
}
}
let indices: Vec<(usize, &str)> = self.input.grapheme_indices(true).collect();
if self.cursor >= indices.len() {
self.cursor = indices.len();
self.input += v;
2023-01-11 00:28:54 +01:00
} else {
let index_at = indices[self.cursor].0;
self.input.insert_str(index_at, v);
2023-01-11 00:28:54 +01:00
}
self.cursor += v.graphemes(true).count();
2023-01-11 00:28:54 +01:00
}
fn backspace(&mut self) {
let mut indices: Vec<(usize, &str)> = self.input.grapheme_indices(true).collect();
2023-01-11 23:54:14 +01:00
if indices.is_empty() {
self.mode = PromptMode::Normal;
return;
}
if self.cursor == 0 {
return;
}
self.cursor -= 1;
indices.remove(self.cursor);
self.input = indices.iter().fold("".into(), |acc, el| acc + el.1);
}
fn delete(&mut self) {
let mut indices: Vec<(usize, &str)> = self.input.grapheme_indices(true).collect();
if indices.is_empty() {
self.mode = PromptMode::Normal;
return;
}
if self.cursor >= indices.len() {
return;
}
indices.remove(self.cursor);
self.input = indices.iter().fold("".into(), |acc, el| acc + el.1);
}
fn clear_right(&mut self) {
let indices: Vec<(usize, &str)> = self.input.grapheme_indices(true).collect();
if !indices.is_empty() {
self.cursor = min(self.cursor, indices.len());
2023-01-12 00:13:27 +01:00
self.input = indices[0..self.cursor]
.iter()
.fold("".into(), |acc, el| acc + el.1);
2023-01-11 00:28:54 +01:00
} else {
self.mode = PromptMode::Normal;
}
}
2023-01-11 23:54:14 +01:00
fn clear_left(&mut self) {
let indices: Vec<(usize, &str)> = self.input.grapheme_indices(true).collect();
if !indices.is_empty() {
self.cursor = min(self.cursor, indices.len());
2023-01-12 00:13:27 +01:00
self.input = indices[self.cursor..indices.len()]
.iter()
.fold("".into(), |acc, el| acc + el.1);
2023-01-11 23:54:14 +01:00
self.cursor = 0;
2023-01-11 00:28:54 +01:00
} else {
self.mode = PromptMode::Normal;
}
}
fn home(&mut self) {
self.cursor = 0;
}
fn end(&mut self) {
self.cursor = self.len();
2023-01-11 00:28:54 +01:00
}
}
/// Strip desktop-entry field codes (%f, %u, %i, %c, ...) from an Exec line and
/// unescape `%%`. We never pass files/URLs, so the codes expand to nothing.
fn strip_field_codes(exec: &str) -> String {
let mut out = String::with_capacity(exec.len());
let mut chars = exec.chars().peekable();
while let Some(c) = chars.next() {
if c != '%' {
out.push(c);
continue;
}
match chars.next() {
Some('%') => out.push('%'),
Some(_) => (),
None => (),
}
}
out
}
2023-01-11 00:28:54 +01:00
trait InterfaceWidget {
/// Number of selectable rows (used to clamp the selection).
fn rows(&self) -> usize;
2023-02-06 18:52:31 +01:00
fn trigger(&mut self, intf: &mut InnerInterface);
2023-01-12 00:13:27 +01:00
fn draw(
&mut self,
intf: &InnerInterface,
fonts: &mut FontMap,
view: &mut BufferView,
) -> Geometry;
2023-01-11 00:28:54 +01:00
fn update(&mut self, intf: &InnerInterface);
}
struct Launcher {
next_token: Option<String>,
options: Arc<Mutex<Vec<Desktop>>>,
matches: Vec<Desktop>,
2025-01-12 22:51:45 +01:00
launch_cmd: Option<String>,
2023-01-11 00:28:54 +01:00
}
impl Launcher {
2025-01-12 22:51:45 +01:00
fn new(events: Arc<Mutex<Events>>, launch_cmd: Option<String>) -> Launcher {
2023-01-11 00:28:54 +01:00
let options = Arc::new(Mutex::new(Vec::new()));
{
let options = Arc::clone(&options);
2023-02-02 23:52:59 +01:00
thread::Builder::new()
.name("desktopini".to_string())
.spawn(move || {
let mut options = options.lock().unwrap();
2023-04-04 01:53:02 +02:00
*options = match load_desktop_cache() {
Ok(v) => v,
Err(_) => {
let v = load_desktop_files();
write_desktop_cache(&v).unwrap();
v
}
};
2023-02-02 23:52:59 +01:00
drop(options);
let mut events = events.lock().unwrap();
events.add_event(Event::LauncherUpdate);
})
.unwrap();
2023-01-11 00:28:54 +01:00
}
Launcher {
next_token: None,
options: options,
matches: Vec::new(),
2025-01-12 22:51:45 +01:00
launch_cmd,
2023-01-11 00:28:54 +01:00
}
}
2025-01-12 22:51:45 +01:00
fn exec(&self, cmdline: &str) {
let mut cmd = Command::new("/bin/sh");
2023-01-11 00:28:54 +01:00
if let Some(token) = &self.next_token {
cmd.env("XDG_ACTIVATION_TOKEN", token);
}
2025-01-12 22:51:45 +01:00
cmd.arg("-c");
if let Some(l) = &self.launch_cmd {
cmd.arg(format!("{} {}", l, cmdline));
} else {
cmd.arg(cmdline);
}
match cmd.spawn() {
Ok(_) => exit(0),
Err(e) => {
eprintln!("wldash: failed to launch {:?}: {}", cmdline, e);
exit(1);
}
}
2023-01-11 00:28:54 +01:00
}
}
impl InterfaceWidget for Launcher {
fn rows(&self) -> usize {
self.matches.len()
}
2023-02-06 18:52:31 +01:00
fn trigger(&mut self, intf: &mut InnerInterface) {
2023-01-11 00:28:54 +01:00
if self.matches.len() > intf.selection {
let d = &self.matches[intf.selection];
if let Some(exec) = &d.exec {
self.exec(&strip_field_codes(exec));
2023-01-11 00:28:54 +01:00
}
}
}
fn update(&mut self, intf: &InnerInterface) {
let mut matcher = Matcher::new();
let options = self.options.lock().unwrap();
for desktop in options.iter() {
matcher.try_match(
desktop.clone(),
&desktop.name.to_lowercase(),
&intf.prompt.input.to_lowercase(),
1.0,
);
for keyword in desktop.keywords.iter() {
matcher.try_match(
desktop.clone(),
&keyword.to_lowercase(),
&intf.prompt.input.to_lowercase(),
0.5,
);
}
}
self.matches = matcher.matches();
}
2023-01-12 00:13:27 +01:00
fn draw(
&mut self,
intf: &InnerInterface,
fonts: &mut FontMap,
view: &mut BufferView,
) -> Geometry {
2023-02-02 23:52:59 +01:00
let fg = Color::WHITE;
2023-01-11 00:28:54 +01:00
2023-01-13 21:53:07 +01:00
let line_height = intf.size.ceil() as u32;
2023-01-11 00:28:54 +01:00
// Draw line
let mut prompt_offset = intf.geometry.height - line_height;
2023-02-02 23:52:59 +01:00
let mut prompt_line = view.offset((0, prompt_offset));
2023-01-11 00:28:54 +01:00
2023-01-13 21:53:07 +01:00
let font = fonts.get_font(intf.font, intf.size);
2023-01-16 00:25:34 +01:00
let mut x_max = font
.auto_draw_text_with_cursor(
&mut prompt_line,
2023-02-02 23:52:59 +01:00
fg,
2023-01-16 00:25:34 +01:00
&format!(" {}", &intf.prompt.input),
intf.prompt.cursor + 3,
)
.unwrap();
2023-01-11 00:28:54 +01:00
2023-02-08 22:53:41 +01:00
font.auto_draw_text(&mut prompt_line, fg, ">").unwrap();
2023-01-11 00:28:54 +01:00
// Draw entries
2023-02-02 23:52:59 +01:00
let dimfg = Color::GREY50;
2023-02-08 22:57:06 +01:00
prompt_offset -= 8;
2023-01-11 00:28:54 +01:00
for (idx, m) in self.matches.iter().enumerate() {
if prompt_offset < line_height {
break;
}
prompt_offset -= line_height;
2023-02-02 23:52:59 +01:00
let mut line = view.offset((0, prompt_offset));
2023-01-11 00:28:54 +01:00
if idx == intf.selection {
let fuzzy_matcher = SkimMatcherV2::default();
let (_, indices) = fuzzy_matcher
.fuzzy_indices(&m.name.to_lowercase(), &intf.prompt.input.to_lowercase())
.unwrap_or((0, vec![]));
let nchars = m.name.chars().count();
let mut colors = Vec::with_capacity(nchars);
for pos in 0..nchars {
2023-01-11 00:28:54 +01:00
if indices.contains(&pos) {
2023-02-02 23:52:59 +01:00
colors.push(Color::LIGHTORANGE);
2023-01-11 00:28:54 +01:00
} else {
2023-02-02 23:52:59 +01:00
colors.push(Color::GREY75);
2023-01-11 00:28:54 +01:00
}
}
2023-01-16 00:25:34 +01:00
x_max = max(
x_max,
2023-02-06 18:34:07 +01:00
font.auto_draw_text_individual_colors(&mut line, &colors, &m.name)
2023-01-16 00:25:34 +01:00
.unwrap(),
);
2023-01-11 00:28:54 +01:00
} else {
2023-01-16 00:25:34 +01:00
x_max = max(
x_max,
2023-02-08 22:53:41 +01:00
font.auto_draw_text(&mut line, dimfg, &m.name).unwrap(),
2023-01-16 00:25:34 +01:00
);
2023-01-11 00:28:54 +01:00
}
}
2023-01-12 00:13:27 +01:00
let content_height = min(
intf.geometry.height,
2023-02-08 22:57:06 +01:00
(self.matches.len() + 1) as u32 * line_height + 8,
2023-01-12 00:13:27 +01:00
);
2023-02-13 19:43:36 +01:00
let content_width = min(intf.geometry.width, x_max.0 + 1);
2023-01-12 00:13:27 +01:00
Geometry {
2023-01-11 00:28:54 +01:00
x: intf.geometry.x,
y: intf.geometry.y + intf.geometry.height - content_height,
2023-02-13 19:43:36 +01:00
width: content_width,
2023-01-11 00:28:54 +01:00
height: content_height,
}
}
}
struct Shell {
next_token: Option<String>,
}
impl Shell {
fn new() -> Shell {
2025-01-12 22:51:45 +01:00
Shell { next_token: None }
}
fn exec(&self, args: Vec<String>) {
let mut cmd = Command::new(args[0].clone());
if let Some(token) = &self.next_token {
cmd.env("XDG_ACTIVATION_TOKEN", token);
}
match cmd.args(&args[1..]).spawn() {
Ok(_) => exit(0),
Err(e) => {
eprintln!("wldash: failed to run {:?}: {}", args, e);
exit(1);
}
}
}
}
2023-01-11 00:28:54 +01:00
impl InterfaceWidget for Shell {
fn rows(&self) -> usize {
1
}
2023-02-06 18:52:31 +01:00
fn trigger(&mut self, intf: &mut InnerInterface) {
2023-01-11 00:28:54 +01:00
let args = vec![
"/bin/sh".to_string(),
"-c".to_string(),
intf.prompt.input.clone(),
];
self.exec(args);
2023-01-11 00:28:54 +01:00
}
fn update(&mut self, _intf: &InnerInterface) {}
2023-01-12 00:13:27 +01:00
fn draw(
&mut self,
intf: &InnerInterface,
fonts: &mut FontMap,
view: &mut BufferView,
) -> Geometry {
2023-02-02 23:52:59 +01:00
let fg = Color::WHITE;
2023-01-11 00:28:54 +01:00
2023-01-13 21:53:07 +01:00
let line_height = intf.size.ceil() as u32;
2023-01-11 00:28:54 +01:00
// Draw line
let prompt_offset = intf.geometry.height - line_height;
2023-02-02 23:52:59 +01:00
let mut prompt_line = view.offset((0, prompt_offset));
2023-01-11 00:28:54 +01:00
// Draw prompt
2023-01-13 21:53:07 +01:00
let font = fonts.get_font(intf.font, intf.size);
2023-01-16 00:25:34 +01:00
let x_max = font
.auto_draw_text_with_cursor(
&mut prompt_line,
2023-02-02 23:52:59 +01:00
fg,
2023-01-16 00:25:34 +01:00
&format!(" {}", &intf.prompt.input),
intf.prompt.cursor + 3,
)
.unwrap();
2023-02-06 18:34:07 +01:00
font.auto_draw_text(&mut prompt_line, Color::BUFF, "!")
2023-01-12 00:13:27 +01:00
.unwrap();
2023-01-11 00:28:54 +01:00
2023-01-12 00:13:27 +01:00
Geometry {
2023-01-11 00:28:54 +01:00
x: intf.geometry.x,
y: intf.geometry.y + intf.geometry.height - line_height,
2023-01-13 21:53:07 +01:00
width: x_max.0 + 1,
2023-01-11 00:28:54 +01:00
height: line_height,
}
}
}
struct Calc {
2023-02-07 00:36:06 +01:00
old: Arc<Mutex<Vec<(String, String)>>>,
2023-01-11 00:28:54 +01:00
result: Option<String>,
}
2023-02-07 00:36:06 +01:00
impl Calc {
fn new() -> Calc {
let old: Arc<Mutex<Vec<(String, String)>>> = Arc::new(Mutex::new(Vec::new()));
let old_local = old.clone();
thread::Builder::new()
.name("calc".to_string())
.spawn(move || {
2023-04-04 01:53:02 +02:00
let s = match read_to_string(format!("{}/wldash/calc", xdg::cache_folder())) {
2023-02-07 00:36:06 +01:00
Ok(s) => s,
_ => return,
};
let mut old = old_local.lock().unwrap();
for line in s.lines() {
let pos = match line.find("=") {
Some(v) => v,
_ => continue,
};
let (input, res) = line.split_at(pos);
let (input, res) = (input.trim(), res[1..].trim());
old.push((input.to_string(), res.to_string()));
}
})
.unwrap();
2023-02-08 22:53:41 +01:00
Calc { old, result: None }
2023-02-07 00:36:06 +01:00
}
fn sync(&self) {
2023-04-04 01:53:02 +02:00
let mut f = match File::create(format!("{}/wldash/calc", xdg::cache_folder())) {
2023-02-07 00:36:06 +01:00
Ok(f) => f,
_ => return,
};
let old = self.old.lock().unwrap();
for (input, res) in old.iter() {
write!(f, "{}={}\n", input, res).unwrap();
}
f.sync_data().unwrap();
}
}
2023-01-11 00:28:54 +01:00
impl InterfaceWidget for Calc {
fn rows(&self) -> usize {
// The live result plus the history entries.
self.old.lock().unwrap().len() + 1
}
2023-02-06 18:52:31 +01:00
fn trigger(&mut self, intf: &mut InnerInterface) {
2023-02-07 00:36:06 +01:00
let mut old = self.old.lock().unwrap();
2023-02-06 18:52:31 +01:00
if intf.selection == 0 {
if let Some(res) = &self.result {
2023-02-07 00:36:06 +01:00
old.push((intf.prompt.input.to_string(), res.to_string()));
while old.len() > 32 {
old.remove(0);
}
drop(old);
self.sync();
2023-02-06 18:52:31 +01:00
}
2023-02-07 00:36:06 +01:00
} else if old.len() >= intf.selection {
let (input, res) = &old[old.len() - intf.selection];
2023-02-06 18:52:31 +01:00
self.result = Some(res.to_string());
intf.prompt.set(input);
intf.selection = 0;
2023-01-11 00:28:54 +01:00
}
}
fn update(&mut self, intf: &InnerInterface) {
2023-01-12 00:13:27 +01:00
let res =
rcalc_lib::parse::eval(&intf.prompt.input, &mut rcalc_lib::parse::CalcState::new())
.map(|x| format!("{}", x))
.map_err(|x| format!("{}", x));
2023-01-11 00:28:54 +01:00
self.result = match res {
Ok(v) => Some(v),
Err(_) => None,
};
}
2023-01-12 00:13:27 +01:00
fn draw(
&mut self,
intf: &InnerInterface,
fonts: &mut FontMap,
view: &mut BufferView,
) -> Geometry {
2023-02-07 00:36:06 +01:00
let old = self.old.lock().unwrap();
2023-02-02 23:52:59 +01:00
let fg = Color::WHITE;
2023-01-11 00:28:54 +01:00
2023-01-13 21:53:07 +01:00
let line_height = intf.size.ceil() as u32;
2023-01-11 00:28:54 +01:00
// Draw line
2023-01-13 21:53:07 +01:00
let mut prompt_offset = intf.geometry.height - (line_height);
2023-02-02 23:52:59 +01:00
let mut prompt_line = view.offset((0, prompt_offset));
2023-01-11 00:28:54 +01:00
// Draw prompt
2023-01-13 21:53:07 +01:00
let font = fonts.get_font(intf.font, intf.size);
2023-02-08 22:53:41 +01:00
font.auto_draw_text_with_cursor(
&mut prompt_line,
fg,
&format!(" {} ", &intf.prompt.input),
intf.prompt.cursor + 3,
)
.unwrap();
2023-01-11 00:28:54 +01:00
2023-02-06 18:34:07 +01:00
font.auto_draw_text(&mut prompt_line, Color::BUFF, "=")
2023-01-12 00:13:27 +01:00
.unwrap();
2023-01-11 00:28:54 +01:00
2023-02-08 22:57:06 +01:00
prompt_offset -= 8;
2023-02-06 18:52:31 +01:00
prompt_offset -= line_height;
2023-01-11 00:28:54 +01:00
if let Some(res) = &self.result {
2023-02-06 18:52:31 +01:00
let c = if intf.selection == 0 {
Color::LIGHTORANGE
} else {
Color::GREY50
};
let mut result_line = view.offset((0, prompt_offset));
font.auto_draw_text(&mut result_line, c, res).unwrap();
2023-01-11 00:28:54 +01:00
}
2023-02-07 00:36:06 +01:00
for (idx, (input, res)) in old.iter().rev().enumerate() {
2023-01-13 21:53:07 +01:00
if prompt_offset < line_height {
2023-01-11 00:28:54 +01:00
break;
}
2023-02-06 18:52:31 +01:00
let c = if idx + 1 == intf.selection {
Color::GREY75
} else {
Color::GREY50
};
2023-01-13 21:53:07 +01:00
prompt_offset -= line_height;
2023-02-02 23:52:59 +01:00
let mut result_line = view.offset((0, prompt_offset));
2023-02-08 22:53:41 +01:00
font.auto_draw_text(&mut result_line, c, &format!("{} = {}", input, res))
.unwrap();
2023-01-11 00:28:54 +01:00
}
2023-01-12 00:13:27 +01:00
let content_height = min(
intf.geometry.height,
2023-02-08 22:57:06 +01:00
(old.len() + 2) as u32 * line_height + 8,
2023-01-12 00:13:27 +01:00
);
Geometry {
2023-01-11 00:28:54 +01:00
x: intf.geometry.x,
y: intf.geometry.y + intf.geometry.height - content_height,
width: intf.geometry.width,
height: content_height,
}
}
}
struct InnerInterface {
2023-01-13 21:53:07 +01:00
font: &'static str,
size: f32,
2023-01-11 00:28:54 +01:00
dirty: bool,
geometry: Geometry,
selection: usize,
prompt: Prompt,
}
pub struct Interface {
launcher: Launcher,
shell: Shell,
calc: Calc,
inner: InnerInterface,
}
impl Interface {
2023-02-08 22:53:41 +01:00
pub fn new(
events: Arc<Mutex<Events>>,
fm: &mut FontMap,
font: &'static str,
size: f32,
2025-01-12 23:10:21 +01:00
launch_cmd: Option<String>,
2023-02-08 22:53:41 +01:00
) -> Interface {
2023-02-02 23:52:59 +01:00
fm.queue_font(
2023-02-08 22:53:41 +01:00
font,
size,
2023-02-02 23:52:59 +01:00
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 >=!,.-/()",
);
2023-01-12 00:13:27 +01:00
Interface {
2025-01-12 22:51:45 +01:00
launcher: Launcher::new(events, launch_cmd),
shell: Shell::new(),
2023-02-07 00:36:06 +01:00
calc: Calc::new(),
2023-01-12 00:13:27 +01:00
inner: InnerInterface {
2023-01-16 00:25:34 +01:00
font,
size,
2023-01-11 00:28:54 +01:00
dirty: false,
geometry: Default::default(),
selection: 0,
prompt: Prompt::new(),
},
}
}
2023-02-02 23:52:59 +01:00
fn pointer_input(&mut self, event: &PointerEvent) {
if let PointerButton::Left = event.button {
let line_height = self.inner.size.ceil() as u32;
if line_height == 0 {
return;
}
let height = self
.inner
.geometry
.height
.saturating_sub(line_height + 8);
2023-02-02 23:52:59 +01:00
let lines = height / line_height;
let offset = height % line_height;
let pos = if event.pos.1 >= offset {
event.pos.1 - offset
} else {
return;
};
let row = pos / line_height;
if row >= lines {
// Click landed on the prompt line or below.
return;
}
self.inner.selection = (lines - row - 1) as usize;
2023-02-02 23:52:59 +01:00
self.inner.dirty = true;
}
}
2023-02-14 00:04:58 +01:00
fn exit(&mut self) {
if self.inner.prompt.input.len() == 0 {
std::process::exit(0);
}
2023-04-04 01:53:02 +02:00
if let Ok(mut f) = File::create(format!("{}/wldash/prompt", xdg::cache_folder())) {
2023-02-14 00:04:58 +01:00
write!(f, "{}", self.inner.prompt.input).unwrap();
f.sync_data().unwrap();
}
std::process::exit(0);
}
2023-01-11 00:28:54 +01:00
fn keyboard_input(&mut self, event: &KeyEvent) {
if event.state != WEnum::Value(wl_keyboard::KeyState::Pressed) {
return;
}
self.inner.dirty = true;
let widget = match self.inner.prompt.mode {
PromptMode::Shell => &mut self.shell as &mut dyn InterfaceWidget,
PromptMode::Normal => &mut self.launcher as &mut dyn InterfaceWidget,
PromptMode::Calc => &mut self.calc as &mut dyn InterfaceWidget,
};
match event.keysym {
2023-11-10 14:00:06 +01:00
keysyms::KEY_a if event.modifiers.ctrl => self.inner.prompt.home(),
keysyms::KEY_e if event.modifiers.ctrl => self.inner.prompt.end(),
keysyms::KEY_u if event.modifiers.ctrl => {
2023-01-11 23:54:14 +01:00
self.inner.prompt.clear_left();
self.inner.selection = 0;
widget.update(&self.inner);
2023-01-12 00:13:27 +01:00
}
2023-11-10 14:00:06 +01:00
keysyms::KEY_k if event.modifiers.ctrl => {
2023-01-11 23:54:14 +01:00
self.inner.prompt.clear_right();
self.inner.selection = 0;
widget.update(&self.inner);
2023-01-12 00:13:27 +01:00
}
2023-11-10 14:00:06 +01:00
keysyms::KEY_r if event.modifiers.ctrl => {
2023-04-04 01:53:02 +02:00
if let Ok(s) = read_to_string(format!("{}/wldash/prompt", xdg::cache_folder())) {
2023-02-14 00:04:58 +01:00
self.inner.prompt.set(&s);
}
}
2023-11-10 14:00:06 +01:00
keysyms::KEY_Home => self.inner.prompt.home(),
keysyms::KEY_End => self.inner.prompt.end(),
keysyms::KEY_BackSpace => {
2023-01-11 00:28:54 +01:00
self.inner.prompt.backspace();
self.inner.selection = 0;
widget.update(&self.inner);
}
2023-11-10 14:00:06 +01:00
keysyms::KEY_Delete => {
2023-01-11 00:28:54 +01:00
self.inner.prompt.delete();
self.inner.selection = 0;
widget.update(&self.inner);
}
2023-11-10 14:00:06 +01:00
keysyms::KEY_Escape => self.exit(),
keysyms::KEY_Return => widget.trigger(&mut self.inner),
keysyms::KEY_Left => self.inner.prompt.move_cursor(-1),
keysyms::KEY_Right => self.inner.prompt.move_cursor(1),
keysyms::KEY_Up => {
let max = widget.rows().saturating_sub(1);
if self.inner.selection < max {
self.inner.selection += 1;
}
2023-01-11 00:28:54 +01:00
}
2023-11-10 14:00:06 +01:00
keysyms::KEY_Down => {
2023-01-11 00:28:54 +01:00
if self.inner.selection > 0 {
self.inner.selection -= 1;
}
}
_ => {
if let Some(utf8) = &event.utf8 {
self.inner.prompt.append(utf8);
2023-01-11 00:28:54 +01:00
self.inner.selection = 0;
widget.update(&self.inner);
} else {
self.inner.dirty = false;
}
}
}
}
2023-01-16 00:25:34 +01:00
}
2023-01-11 00:28:54 +01:00
2023-01-16 00:25:34 +01:00
impl Widget for Interface {
fn get_dirty(&self) -> bool {
self.inner.dirty
}
fn geometry(&self) -> Geometry {
self.inner.geometry
}
fn draw(&mut self, fonts: &mut FontMap, view: &mut BufferView) -> Geometry {
self.inner.dirty = false;
match self.inner.prompt.mode {
PromptMode::Shell => self.shell.draw(&self.inner, fonts, view),
PromptMode::Normal => self.launcher.draw(&self.inner, fonts, view),
PromptMode::Calc => self.calc.draw(&self.inner, fonts, view),
}
}
fn geometry_update(&mut self, _fonts: &mut FontMap, geometry: &Geometry) -> Geometry {
self.inner.geometry = geometry.clone();
self.inner.geometry
}
2023-02-08 22:53:41 +01:00
fn minimum_size(&mut self, _fonts: &mut FontMap) -> Geometry {
Geometry {
x: 0,
y: 0,
width: 256,
2023-02-08 22:57:06 +01:00
height: (self.inner.size.ceil() as u32) * 4 + 8,
2023-02-08 22:53:41 +01:00
}
}
2023-01-16 00:25:34 +01:00
fn event(&mut self, event: &Event) {
match event {
Event::KeyEvent(e) => self.keyboard_input(e),
2023-02-02 23:52:59 +01:00
Event::PointerEvent(e) => self.pointer_input(e),
2023-02-14 00:04:58 +01:00
Event::FocusLost => self.exit(),
2023-02-02 23:52:59 +01:00
Event::LauncherUpdate => {
self.inner.dirty = true;
self.launcher.update(&self.inner);
2023-02-08 22:53:41 +01:00
}
Event::TokenUpdate(t) => {
self.launcher.next_token = Some(t.to_string());
self.shell.next_token = Some(t.to_string());
}
2023-01-16 00:25:34 +01:00
_ => (),
}
2023-01-11 00:28:54 +01:00
}
}
#[cfg(test)]
mod tests {
use super::{strip_field_codes, Prompt, PromptMode};
#[test]
fn prompt_cursor_is_grapheme_based() {
let mut p = Prompt::new();
p.append("ø");
p.append("å");
assert_eq!(p.cursor, 2);
p.end();
assert_eq!(p.cursor, 2);
// Used to index past the grapheme table and panic.
p.append("a");
assert_eq!(p.input, "øåa");
p.move_cursor(-10);
assert_eq!(p.cursor, 0);
p.move_cursor(100);
assert_eq!(p.cursor, 3);
p.backspace();
assert_eq!(p.input, "øå");
p.home();
p.delete();
assert_eq!(p.input, "å");
p.set("blå");
assert_eq!(p.cursor, 3);
p.move_cursor(-1);
p.clear_right();
assert_eq!(p.input, "bl");
p.clear_left();
assert_eq!(p.input, "");
assert_eq!(p.cursor, 0);
}
#[test]
fn prompt_prefix_switches_mode() {
let mut p = Prompt::new();
p.append("=");
assert!(matches!(p.mode, PromptMode::Calc));
assert_eq!(p.input, "");
p.backspace();
assert!(matches!(p.mode, PromptMode::Normal));
}
#[test]
fn field_codes_are_stripped() {
assert_eq!(strip_field_codes("firefox %u"), "firefox ");
assert_eq!(strip_field_codes("app --icon %i %c %k"), "app --icon ");
assert_eq!(strip_field_codes("echo 100%% %F"), "echo 100% ");
assert_eq!(strip_field_codes("plain"), "plain");
}
}