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,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-03 09:15:53 +00:00
|
|
|
/// 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;
|
2026-10-03 09:15:53 +00:00
|
|
|
let len = self.len() as isize;
|
2023-01-11 00:28:54 +01:00
|
|
|
if new_cursor < 0 {
|
|
|
|
|
self.cursor = 0;
|
2026-10-03 09:15:53 +00:00
|
|
|
} 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();
|
2026-10-03 09:15:53 +00:00
|
|
|
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();
|
2026-10-03 09:15:53 +00:00
|
|
|
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;
|
2026-10-03 09:15:53 +00:00
|
|
|
self.input.insert_str(index_at, v);
|
2023-01-11 00:28:54 +01:00
|
|
|
}
|
2026-10-03 09:15:53 +00: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() {
|
2026-10-03 09:15:53 +00:00
|
|
|
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() {
|
2026-10-03 09:15:53 +00:00
|
|
|
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) {
|
2026-10-03 09:15:53 +00:00
|
|
|
self.cursor = self.len();
|
2023-01-11 00:28:54 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-03 09:15:53 +00: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 {
|
2026-10-03 09:15:53 +00:00
|
|
|
/// 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);
|
|
|
|
|
}
|
2026-10-03 09:15:53 +00:00
|
|
|
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 {
|
2026-10-03 09:15:53 +00:00
|
|
|
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 {
|
2026-10-03 09:15:53 +00:00
|
|
|
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![]));
|
|
|
|
|
|
2026-10-03 09:15:53 +00:00
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-04 01:53:29 +02:00
|
|
|
struct Shell {
|
|
|
|
|
next_token: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Shell {
|
|
|
|
|
fn new() -> Shell {
|
2025-01-12 22:51:45 +01:00
|
|
|
Shell { next_token: None }
|
2023-04-04 01:53:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
2026-10-03 09:15:53 +00:00
|
|
|
match cmd.args(&args[1..]).spawn() {
|
|
|
|
|
Ok(_) => exit(0),
|
|
|
|
|
Err(e) => {
|
|
|
|
|
eprintln!("wldash: failed to run {:?}: {}", args, e);
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-04-04 01:53:29 +02:00
|
|
|
}
|
|
|
|
|
}
|
2023-01-11 00:28:54 +01:00
|
|
|
|
|
|
|
|
impl InterfaceWidget for Shell {
|
2026-10-03 09:15:53 +00:00
|
|
|
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(),
|
|
|
|
|
];
|
2023-04-04 01:53:29 +02:00
|
|
|
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 {
|
2026-10-03 09:15:53 +00:00
|
|
|
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),
|
2023-04-04 01:53:29 +02:00
|
|
|
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;
|
2026-10-03 09:15:53 +00:00
|
|
|
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;
|
|
|
|
|
};
|
2026-10-03 09:15:53 +00:00
|
|
|
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 => {
|
2026-10-03 09:15:53 +00:00
|
|
|
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 {
|
2026-10-03 09:15:53 +00:00
|
|
|
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
|
|
|
}
|
2023-04-04 01:53:29 +02: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
|
|
|
}
|
|
|
|
|
}
|
2026-10-03 09:15:53 +00: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");
|
|
|
|
|
}
|
|
|
|
|
}
|