Fixed a Windows CI issue.
This commit is contained in:
parent
488038c080
commit
22d52d5ac0
11 changed files with 450 additions and 101 deletions
|
|
@ -173,7 +173,7 @@ tutorial_seen = false
|
||||||
|
|
||||||
[search]
|
[search]
|
||||||
# Start with the fuzzy passes enabled.
|
# Start with the fuzzy passes enabled.
|
||||||
fuzzy_default = true
|
fuzzy_default = false
|
||||||
# Ceiling on the fuzzy stages' typo budget. The allowance grows with the
|
# Ceiling on the fuzzy stages' typo budget. The allowance grows with the
|
||||||
# search term, one edit per three characters, up to this value, so 2
|
# search term, one edit per three characters, up to this value, so 2
|
||||||
# means "1 edit for 3-5 character terms, 2 for anything longer". 0 turns
|
# means "1 edit for 3-5 character terms, 2 for anything longer". 0 turns
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,13 @@ chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
||||||
# This is the zero-setup path; `--toggle` covers the app not running.
|
# This is the zero-setup path; `--toggle` covers the app not running.
|
||||||
global-hotkey = "0.8"
|
global-hotkey = "0.8"
|
||||||
|
|
||||||
|
# The window handle behind both native raise paths — `activate::raise`'s X11
|
||||||
|
# `_NET_ACTIVE_WINDOW` message and its Win32 `SetForegroundWindow` — neither of
|
||||||
|
# which winit will do for us. Not target-gated: eframe depends on this
|
||||||
|
# unconditionally (it is what `HasWindowHandle for Frame` is written against),
|
||||||
|
# so naming it here adds no crate on any platform.
|
||||||
|
raw-window-handle = "0.6"
|
||||||
|
|
||||||
# Display backends, which only exist on Linux/BSD. `default-features = false`
|
# Display backends, which only exist on Linux/BSD. `default-features = false`
|
||||||
# has to be repeated: feature resolution unions the two stanzas, so a single
|
# has to be repeated: feature resolution unions the two stanzas, so a single
|
||||||
# permissive one would switch defaults back on for every target.
|
# permissive one would switch defaults back on for every target.
|
||||||
|
|
@ -83,10 +90,9 @@ pollster = "0.4"
|
||||||
# Raising the window from either shortcut on X11, which winit cannot
|
# Raising the window from either shortcut on X11, which winit cannot
|
||||||
# do: it asks with a source indication of "application", and every mainstream
|
# do: it asks with a source indication of "application", and every mainstream
|
||||||
# window manager refuses that from a window that is not already focused. See
|
# window manager refuses that from a window that is not already focused. See
|
||||||
# `activate::raise`. Both are already in the tree (winit's own X11 backend,
|
# `activate::raise`. Already in the tree as winit's own X11 backend, so this
|
||||||
# and eframe's window handle), so neither adds a crate.
|
# adds no crate.
|
||||||
x11rb = "0.13"
|
x11rb = "0.13"
|
||||||
raw-window-handle = "0.6"
|
|
||||||
|
|
||||||
# XTEST, for `examples/raiseprobe.rs` only: it synthesises the global key
|
# XTEST, for `examples/raiseprobe.rs` only: it synthesises the global key
|
||||||
# press that proves the shortcut path end to end, which no command-line tool
|
# press that proves the shortcut path end to end, which no command-line tool
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ pub mod raise;
|
||||||
|
|
||||||
pub use raise::raise;
|
pub use raise::raise;
|
||||||
|
|
||||||
use std::io::{Read, Write};
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
|
|
@ -133,6 +132,9 @@ pub(crate) fn fire(ctx: &egui::Context) {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
mod imp {
|
mod imp {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
// The socket is the only reader and writer here; Windows uses the Win32
|
||||||
|
// pipe calls rather than `std::io`.
|
||||||
|
use std::io::{Read, Write};
|
||||||
use std::os::unix::net::{UnixListener, UnixStream};
|
use std::os::unix::net::{UnixListener, UnixStream};
|
||||||
|
|
||||||
/// Ask the instance configured by `config_path` to come forward.
|
/// Ask the instance configured by `config_path` to come forward.
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
use crate::tips::{self, Tipped};
|
||||||
use crate::ui_util::hint;
|
use crate::ui_util::hint;
|
||||||
|
|
||||||
impl QuickSearchApp {
|
impl QuickSearchApp {
|
||||||
|
|
@ -48,18 +49,19 @@ impl QuickSearchApp {
|
||||||
ui.label(
|
ui.label(
|
||||||
egui::RichText::new(match (r.total, r.fraction()) {
|
egui::RichText::new(match (r.total, r.fraction()) {
|
||||||
(Some(total), Some(frac)) => format!(
|
(Some(total), Some(frac)) => format!(
|
||||||
"Applying configuration change · {} / {} ({:.0}%)",
|
"Rebuilding FTS cache · {} / {} ({:.0}%)",
|
||||||
group_thousands(r.examined as u64),
|
group_thousands(r.examined as u64),
|
||||||
group_thousands(total as u64),
|
group_thousands(total as u64),
|
||||||
frac * 100.0
|
frac * 100.0
|
||||||
),
|
),
|
||||||
_ => format!(
|
_ => format!(
|
||||||
"Applying configuration change · {} entries",
|
"Rebuilding FTS cache · {} entries",
|
||||||
group_thousands(r.examined as u64)
|
group_thousands(r.examined as u64)
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
.small(),
|
.small(),
|
||||||
);
|
)
|
||||||
|
.tip(&tips::REBUILDING_FTS);
|
||||||
progress_widget(ui, r.fraction());
|
progress_widget(ui, r.fraction());
|
||||||
}
|
}
|
||||||
ReconcileState::Finished(r) => {
|
ReconcileState::Finished(r) => {
|
||||||
|
|
@ -74,21 +76,28 @@ impl QuickSearchApp {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IndexingStatus::Preparing { start_time, step } => {
|
IndexingStatus::Preparing { start_time, step } => {
|
||||||
let (label, frac) = match step {
|
// Only the reconcile step has a wait worth explaining;
|
||||||
|
// the rest are over in a moment.
|
||||||
|
let (label, frac, tip) = match step {
|
||||||
PrepStep::PreviousRun => {
|
PrepStep::PreviousRun => {
|
||||||
("Finishing the previous run…".to_string(), None)
|
("Finishing the previous run…".to_string(), None, None)
|
||||||
|
}
|
||||||
|
PrepStep::OpeningIndex => {
|
||||||
|
("Opening the index…".to_string(), None, None)
|
||||||
|
}
|
||||||
|
PrepStep::Starting => {
|
||||||
|
("Getting the index ready…".to_string(), None, None)
|
||||||
}
|
}
|
||||||
PrepStep::OpeningIndex => ("Opening the index…".to_string(), None),
|
|
||||||
PrepStep::Starting => ("Getting the index ready…".to_string(), None),
|
|
||||||
PrepStep::Reconciling(r) => (
|
PrepStep::Reconciling(r) => (
|
||||||
format!(
|
format!(
|
||||||
"Applying configuration change · {} entries",
|
"Rebuilding FTS cache · {} entries",
|
||||||
group_thousands(r.examined as u64)
|
group_thousands(r.examined as u64)
|
||||||
),
|
),
|
||||||
r.fraction(),
|
r.fraction(),
|
||||||
|
Some(&tips::REBUILDING_FTS),
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
ui.label(
|
let response = ui.label(
|
||||||
egui::RichText::new(format!(
|
egui::RichText::new(format!(
|
||||||
"{} · {}",
|
"{} · {}",
|
||||||
label,
|
label,
|
||||||
|
|
@ -96,6 +105,9 @@ impl QuickSearchApp {
|
||||||
))
|
))
|
||||||
.small(),
|
.small(),
|
||||||
);
|
);
|
||||||
|
if let Some(tip) = tip {
|
||||||
|
response.tip(tip);
|
||||||
|
}
|
||||||
progress_widget(ui, frac);
|
progress_widget(ui, frac);
|
||||||
}
|
}
|
||||||
IndexingStatus::Idle => {
|
IndexingStatus::Idle => {
|
||||||
|
|
|
||||||
|
|
@ -156,9 +156,33 @@ impl Binding {
|
||||||
.expect("every Binding key comes from KEYS")
|
.expect("every Binding key comes from KEYS")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The key combination as the integer `Qt::Key | Qt::Modifier` value
|
||||||
|
/// KGlobalAccel's DBus `setShortcut` takes. Covered like [`Self::row`]:
|
||||||
|
/// `tests::every_key_has_a_qt_code` proves the mapping total over
|
||||||
|
/// [`KEYS`], so a new row cannot reach KDE as a panic.
|
||||||
|
///
|
||||||
|
/// This and the two spellings below exist for the desktops
|
||||||
|
/// `crate::shortcut_setup` and [`super::portal`] write to, all of which
|
||||||
|
/// are unix; off unix nothing calls them and the lint would say so.
|
||||||
|
#[cfg_attr(not(all(unix, not(target_os = "macos"))), allow(dead_code))]
|
||||||
|
pub fn qt_key_code(&self) -> u32 {
|
||||||
|
let mut code = qt_key(self.key);
|
||||||
|
for (held, bit) in [
|
||||||
|
(self.shift, 0x0200_0000),
|
||||||
|
(self.ctrl, 0x0400_0000),
|
||||||
|
(self.alt, 0x0800_0000),
|
||||||
|
] {
|
||||||
|
if held {
|
||||||
|
code |= bit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
code
|
||||||
|
}
|
||||||
|
|
||||||
/// The accelerator in GTK's syntax — `<Ctrl><Shift>f` — which is what a
|
/// The accelerator in GTK's syntax — `<Ctrl><Shift>f` — which is what a
|
||||||
/// GNOME custom keybinding's `binding` key stores. GTK keyval names are
|
/// GNOME custom keybinding's `binding` key stores. GTK keyval names are
|
||||||
/// the X11 keysym names, so the keysym column serves both spellings.
|
/// the X11 keysym names, so the keysym column serves both spellings.
|
||||||
|
#[cfg_attr(not(all(unix, not(target_os = "macos"))), allow(dead_code))]
|
||||||
pub fn gtk_accelerator(&self) -> String {
|
pub fn gtk_accelerator(&self) -> String {
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
for (held, name) in [
|
for (held, name) in [
|
||||||
|
|
@ -176,6 +200,7 @@ impl Binding {
|
||||||
|
|
||||||
/// The trigger in the XDG shortcuts spec's syntax: uppercase modifiers
|
/// The trigger in the XDG shortcuts spec's syntax: uppercase modifiers
|
||||||
/// and an xkbcommon keysym, joined with `+`.
|
/// and an xkbcommon keysym, joined with `+`.
|
||||||
|
#[cfg_attr(not(all(unix, not(target_os = "macos"))), allow(dead_code))]
|
||||||
pub fn portal_trigger(&self) -> String {
|
pub fn portal_trigger(&self) -> String {
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
for (held, name) in [
|
for (held, name) in [
|
||||||
|
|
@ -250,6 +275,90 @@ impl FromStr for Binding {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The `Qt::Key` value for a bindable key. Printable keys are their ASCII
|
||||||
|
/// uppercase; the named keys are Qt's `0x0100_00xx` block. A fourth [`KEYS`]
|
||||||
|
/// column in all but layout: kept as a match so the table stays readable,
|
||||||
|
/// with `tests::every_key_has_a_qt_code` holding the two together.
|
||||||
|
#[cfg_attr(not(all(unix, not(target_os = "macos"))), allow(dead_code))]
|
||||||
|
fn qt_key(key: Key) -> u32 {
|
||||||
|
match key {
|
||||||
|
Key::A => 0x41,
|
||||||
|
Key::B => 0x42,
|
||||||
|
Key::C => 0x43,
|
||||||
|
Key::D => 0x44,
|
||||||
|
Key::E => 0x45,
|
||||||
|
Key::F => 0x46,
|
||||||
|
Key::G => 0x47,
|
||||||
|
Key::H => 0x48,
|
||||||
|
Key::I => 0x49,
|
||||||
|
Key::J => 0x4A,
|
||||||
|
Key::K => 0x4B,
|
||||||
|
Key::L => 0x4C,
|
||||||
|
Key::M => 0x4D,
|
||||||
|
Key::N => 0x4E,
|
||||||
|
Key::O => 0x4F,
|
||||||
|
Key::P => 0x50,
|
||||||
|
Key::Q => 0x51,
|
||||||
|
Key::R => 0x52,
|
||||||
|
Key::S => 0x53,
|
||||||
|
Key::T => 0x54,
|
||||||
|
Key::U => 0x55,
|
||||||
|
Key::V => 0x56,
|
||||||
|
Key::W => 0x57,
|
||||||
|
Key::X => 0x58,
|
||||||
|
Key::Y => 0x59,
|
||||||
|
Key::Z => 0x5A,
|
||||||
|
Key::Num0 => 0x30,
|
||||||
|
Key::Num1 => 0x31,
|
||||||
|
Key::Num2 => 0x32,
|
||||||
|
Key::Num3 => 0x33,
|
||||||
|
Key::Num4 => 0x34,
|
||||||
|
Key::Num5 => 0x35,
|
||||||
|
Key::Num6 => 0x36,
|
||||||
|
Key::Num7 => 0x37,
|
||||||
|
Key::Num8 => 0x38,
|
||||||
|
Key::Num9 => 0x39,
|
||||||
|
Key::F1 => 0x0100_0030,
|
||||||
|
Key::F2 => 0x0100_0031,
|
||||||
|
Key::F3 => 0x0100_0032,
|
||||||
|
Key::F4 => 0x0100_0033,
|
||||||
|
Key::F5 => 0x0100_0034,
|
||||||
|
Key::F6 => 0x0100_0035,
|
||||||
|
Key::F7 => 0x0100_0036,
|
||||||
|
Key::F8 => 0x0100_0037,
|
||||||
|
Key::F9 => 0x0100_0038,
|
||||||
|
Key::F10 => 0x0100_0039,
|
||||||
|
Key::F11 => 0x0100_003A,
|
||||||
|
Key::F12 => 0x0100_003B,
|
||||||
|
Key::Space => 0x20,
|
||||||
|
Key::Enter => 0x0100_0004,
|
||||||
|
Key::Tab => 0x0100_0001,
|
||||||
|
Key::Backspace => 0x0100_0003,
|
||||||
|
Key::Delete => 0x0100_0007,
|
||||||
|
Key::Insert => 0x0100_0006,
|
||||||
|
Key::Home => 0x0100_0010,
|
||||||
|
Key::End => 0x0100_0011,
|
||||||
|
Key::PageUp => 0x0100_0016,
|
||||||
|
Key::PageDown => 0x0100_0017,
|
||||||
|
Key::ArrowUp => 0x0100_0013,
|
||||||
|
Key::ArrowDown => 0x0100_0015,
|
||||||
|
Key::ArrowLeft => 0x0100_0012,
|
||||||
|
Key::ArrowRight => 0x0100_0014,
|
||||||
|
Key::Comma => 0x2C,
|
||||||
|
Key::Period => 0x2E,
|
||||||
|
Key::Slash => 0x2F,
|
||||||
|
Key::Backslash => 0x5C,
|
||||||
|
Key::Semicolon => 0x3B,
|
||||||
|
Key::Quote => 0x27,
|
||||||
|
Key::Backtick => 0x60,
|
||||||
|
Key::Minus => 0x2D,
|
||||||
|
Key::Equals => 0x3D,
|
||||||
|
Key::OpenBracket => 0x5B,
|
||||||
|
Key::CloseBracket => 0x5D,
|
||||||
|
other => unreachable!("{other:?} is not in KEYS; see every_key_has_a_qt_code"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Empty means "no shortcut" rather than an error.
|
/// Empty means "no shortcut" rather than an error.
|
||||||
pub fn parse_setting(setting: &str) -> Result<Option<Binding>, BindingError> {
|
pub fn parse_setting(setting: &str) -> Result<Option<Binding>, BindingError> {
|
||||||
if setting.trim().is_empty() {
|
if setting.trim().is_empty() {
|
||||||
|
|
@ -271,6 +380,29 @@ mod tests {
|
||||||
assert_eq!(binding.gtk_accelerator(), "<Ctrl><Shift>f");
|
assert_eq!(binding.gtk_accelerator(), "<Ctrl><Shift>f");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `qt_key`'s `unreachable!` is only sound while every row of [`KEYS`]
|
||||||
|
/// has an arm; this is what holds the match and the table together.
|
||||||
|
#[test]
|
||||||
|
fn every_key_has_a_qt_code() {
|
||||||
|
for (key, token, _) in KEYS {
|
||||||
|
let code = qt_key(*key);
|
||||||
|
assert_ne!(code, 0, "{token} has no Qt key code");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The values KGlobalAccel actually receives, spot-checked against
|
||||||
|
/// `Qt::Key`: the default binding (verified live against Plasma 6.6),
|
||||||
|
/// a named key, and a punctuation key.
|
||||||
|
#[test]
|
||||||
|
fn qt_key_codes_match_qt() {
|
||||||
|
let binding: Binding = "Ctrl+Shift+F".parse().unwrap();
|
||||||
|
assert_eq!(binding.qt_key_code(), 0x0600_0046);
|
||||||
|
let binding: Binding = "Alt+PageUp".parse().unwrap();
|
||||||
|
assert_eq!(binding.qt_key_code(), 0x0900_0016);
|
||||||
|
let binding: Binding = "Ctrl+Comma".parse().unwrap();
|
||||||
|
assert_eq!(binding.qt_key_code(), 0x0400_002C);
|
||||||
|
}
|
||||||
|
|
||||||
/// The keysym column doubles as the GTK keyval, so a named key must come
|
/// The keysym column doubles as the GTK keyval, so a named key must come
|
||||||
/// out under GTK's name for it, not egui's.
|
/// out under GTK's name for it, not egui's.
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -735,7 +735,7 @@ fn waiting_row(ui: &mut egui::Ui, label: &str, elapsed: Duration) {
|
||||||
/// `elapsed` is `Some` for a run's prologue; the between-runs pass has none.
|
/// `elapsed` is `Some` for a run's prologue; the between-runs pass has none.
|
||||||
fn reconcile_row(ui: &mut egui::Ui, r: &ReconcileProgress, elapsed: Option<Duration>) {
|
fn reconcile_row(ui: &mut egui::Ui, r: &ReconcileProgress, elapsed: Option<Duration>) {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label("Applying configuration change");
|
ui.label("Rebuilding FTS cache").tip(&tips::REBUILDING_FTS);
|
||||||
ui.label(egui::RichText::new("|").weak());
|
ui.label(egui::RichText::new("|").weak());
|
||||||
match (r.total, r.fraction()) {
|
match (r.total, r.fraction()) {
|
||||||
(Some(total), Some(frac)) => {
|
(Some(total), Some(frac)) => {
|
||||||
|
|
|
||||||
|
|
@ -628,7 +628,7 @@ fn a_reconcile_reports_how_far_through_the_index_it_is() {
|
||||||
)
|
)
|
||||||
.join(" | ");
|
.join(" | ");
|
||||||
|
|
||||||
assert!(text.contains("Applying configuration change"), "{}", text);
|
assert!(text.contains("Rebuilding FTS cache"), "{}", text);
|
||||||
assert!(
|
assert!(
|
||||||
text.contains("2,500,000 / 8,000,000 (31%) entries checked"),
|
text.contains("2,500,000 / 8,000,000 (31%) entries checked"),
|
||||||
"{}",
|
"{}",
|
||||||
|
|
@ -637,6 +637,41 @@ fn a_reconcile_reports_how_far_through_the_index_it_is() {
|
||||||
assert!(text.contains("1,204 entries removed"), "{}", text);
|
assert!(text.contains("1,204 entries removed"), "{}", text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The pass can run for minutes with the window unresponsive, so the line
|
||||||
|
/// that names it has to be where the explanation is reachable from.
|
||||||
|
#[test]
|
||||||
|
fn the_reconcile_status_explains_the_wait_on_hover() {
|
||||||
|
let ctx = crate::test_ui::ctx();
|
||||||
|
ctx.style_mut(|s| {
|
||||||
|
s.interaction.tooltip_delay = 0.0;
|
||||||
|
s.interaction.show_tooltips_only_when_still = false;
|
||||||
|
});
|
||||||
|
let mut tab = ManageTab::new();
|
||||||
|
let cfg = cfg_with_root();
|
||||||
|
let state = preparing_state(PrepStep::Reconciling(ReconcileProgress::default()));
|
||||||
|
let mut run = |events: Vec<egui::Event>| {
|
||||||
|
ctx.run(raw_input(events), |ctx| {
|
||||||
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
|
tab.ui(ui, &state, &cfg);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
run(vec![]);
|
||||||
|
let settled = run(vec![]);
|
||||||
|
let pos = crate::test_ui::painted_text_center(&settled, "Rebuilding FTS cache")
|
||||||
|
.expect("status line painted");
|
||||||
|
let opening: String = crate::tips::REBUILDING_FTS.body.chars().take(40).collect();
|
||||||
|
let mut out = run(vec![egui::Event::PointerMoved(pos)]);
|
||||||
|
for _ in 0..3 {
|
||||||
|
if painted_text(&out).join("\n").contains(&opening) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
out = run(vec![]);
|
||||||
|
}
|
||||||
|
panic!("no tooltip on the status line: {:?}", painted_text(&out));
|
||||||
|
}
|
||||||
|
|
||||||
/// Whole-range deletions read no rows; the display must not invent a denominator.
|
/// Whole-range deletions read no rows; the display must not invent a denominator.
|
||||||
#[test]
|
#[test]
|
||||||
fn a_reconcile_without_a_row_count_shows_no_denominator() {
|
fn a_reconcile_without_a_row_count_shows_no_denominator() {
|
||||||
|
|
@ -669,7 +704,7 @@ fn a_prune_between_runs_is_reported_instead_of_idle() {
|
||||||
|
|
||||||
let text = frame_text(&ctx, &mut tab, &state).join(" | ");
|
let text = frame_text(&ctx, &mut tab, &state).join(" | ");
|
||||||
assert!(
|
assert!(
|
||||||
text.contains("Applying configuration change"),
|
text.contains("Rebuilding FTS cache"),
|
||||||
"the scan is invisible: {}",
|
"the scan is invisible: {}",
|
||||||
text
|
text
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -466,6 +466,21 @@ fn hotkey_note(ui: &mut egui::Ui, draft: &str, live: &str) {
|
||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
|
// On Windows a failed registration is the *expected* state
|
||||||
|
// whenever the Start-menu shortcut owns the same key — Explorer
|
||||||
|
// registers it at logon, wins, and every press then reaches us
|
||||||
|
// through the `--toggle` relay anyway. An error color would cry
|
||||||
|
// wolf on every installed copy.
|
||||||
|
Status::Error(why) if cfg!(windows) => (
|
||||||
|
format!(
|
||||||
|
"Another program holds this key ({}) — usually the Start \
|
||||||
|
menu shortcut that starts QuickSearch, which also brings \
|
||||||
|
it forward while it is running. If the key does nothing, \
|
||||||
|
pick a different combination.",
|
||||||
|
why
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
),
|
||||||
Status::Error(why) => (
|
Status::Error(why) => (
|
||||||
format!("The shortcut is not active: {}.", why),
|
format!("The shortcut is not active: {}.", why),
|
||||||
Some(crate::color::palette(ui.visuals().dark_mode).orange),
|
Some(crate::color::palette(ui.visuals().dark_mode).orange),
|
||||||
|
|
@ -570,10 +585,7 @@ fn shortcut_note_for(ui: &mut egui::Ui, hotkey_setting: &str, desktop: crate::sh
|
||||||
state.installed = true;
|
state.installed = true;
|
||||||
state.feedback = Some((
|
state.feedback = Some((
|
||||||
true,
|
true,
|
||||||
"Added to your desktop's keyboard shortcuts. If the \
|
"Added to your desktop's keyboard shortcuts.".to_string(),
|
||||||
key does not answer right away, it will after the \
|
|
||||||
next login."
|
|
||||||
.to_string(),
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Err(e) => state.feedback = Some((false, e)),
|
Err(e) => state.feedback = Some((false, e)),
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,11 @@ use crate::hotkey::Binding;
|
||||||
|
|
||||||
/// Where a binding can be written. `Unsupported` hides the one-click button
|
/// Where a binding can be written. `Unsupported` hides the one-click button
|
||||||
/// and leaves the manual flow.
|
/// and leaves the manual flow.
|
||||||
|
///
|
||||||
|
/// Off unix the two named desktops are unreachable — [`detect`] answers
|
||||||
|
/// `Unsupported` and the dispatchers below never name them — so the variants
|
||||||
|
/// exist there only to keep this one enum for every platform.
|
||||||
|
#[cfg_attr(not(all(unix, not(target_os = "macos"))), allow(dead_code))]
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
pub enum Desktop {
|
pub enum Desktop {
|
||||||
Gnome,
|
Gnome,
|
||||||
|
|
@ -52,31 +57,75 @@ fn desktop_for(desktops: &str) -> Desktop {
|
||||||
|
|
||||||
/// Whether the binding this module writes is currently present. Asks the
|
/// Whether the binding this module writes is currently present. Asks the
|
||||||
/// desktop, so callers should cache rather than poll every frame.
|
/// desktop, so callers should cache rather than poll every frame.
|
||||||
|
///
|
||||||
|
/// The three functions here are split by platform the same way [`detect`] is:
|
||||||
|
/// the desktops that have a home for a binding are all unix, and their modules
|
||||||
|
/// only exist there, so naming them off unix would not compile.
|
||||||
pub fn installed() -> bool {
|
pub fn installed() -> bool {
|
||||||
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
|
{
|
||||||
match detect() {
|
match detect() {
|
||||||
Desktop::Gnome => gnome::installed(),
|
Desktop::Gnome => gnome::installed(),
|
||||||
Desktop::Kde => kde::installed(),
|
Desktop::Kde => kde::installed(),
|
||||||
Desktop::Unsupported => false,
|
Desktop::Unsupported => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(not(all(unix, not(target_os = "macos"))))]
|
||||||
|
{
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Write `binding` → `quicksearch --toggle` into the desktop's keyboard
|
/// Write `binding` → `quicksearch --toggle` into the desktop's keyboard
|
||||||
/// configuration. Idempotent: a second install rewrites the same entry.
|
/// configuration. Idempotent: a second install rewrites the same entry.
|
||||||
pub fn install(binding: &Binding) -> Result<(), String> {
|
pub fn install(binding: &Binding) -> Result<(), String> {
|
||||||
let command = format!("{} --toggle", crate::activate::command_name());
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
|
{
|
||||||
|
let command = toggle_command();
|
||||||
match detect() {
|
match detect() {
|
||||||
Desktop::Gnome => gnome::install(binding, &command),
|
Desktop::Gnome => gnome::install(binding, &command),
|
||||||
Desktop::Kde => kde::install(binding),
|
Desktop::Kde => kde::install(binding, &command),
|
||||||
Desktop::Unsupported => Err("this desktop is not supported".to_string()),
|
Desktop::Unsupported => Err(UNSUPPORTED.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(all(unix, not(target_os = "macos"))))]
|
||||||
|
{
|
||||||
|
let _ = binding;
|
||||||
|
Err(UNSUPPORTED.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What every platform without a home for a binding answers. On Windows the
|
||||||
|
/// closed-app binding is the installer's `.lnk`, so there is nothing here to
|
||||||
|
/// write and the Settings tab shows the manual note instead.
|
||||||
|
const UNSUPPORTED: &str = "this desktop is not supported";
|
||||||
|
|
||||||
|
/// The command line the key runs, quoted for a path with spaces in it —
|
||||||
|
/// both GNOME's `command` key and a desktop file's `Exec` split on
|
||||||
|
/// whitespace and honour double quotes.
|
||||||
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
|
fn toggle_command() -> String {
|
||||||
|
let exe = crate::activate::command_name();
|
||||||
|
if exe.contains(char::is_whitespace) {
|
||||||
|
format!("\"{}\" --toggle", exe)
|
||||||
|
} else {
|
||||||
|
format!("{} --toggle", exe)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete the entry [`install`] wrote; a no-op if it is already gone.
|
/// Delete the entry [`install`] wrote; a no-op if it is already gone.
|
||||||
pub fn remove() -> Result<(), String> {
|
pub fn remove() -> Result<(), String> {
|
||||||
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
|
{
|
||||||
match detect() {
|
match detect() {
|
||||||
Desktop::Gnome => gnome::remove(),
|
Desktop::Gnome => gnome::remove(),
|
||||||
Desktop::Kde => kde::remove(),
|
Desktop::Kde => kde::remove(),
|
||||||
Desktop::Unsupported => Err("this desktop is not supported".to_string()),
|
Desktop::Unsupported => Err(UNSUPPORTED.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(all(unix, not(target_os = "macos"))))]
|
||||||
|
{
|
||||||
|
Err(UNSUPPORTED.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,89 +271,133 @@ fn format_string_list(paths: &[String]) -> String {
|
||||||
format!("[{}]", quoted.join(", "))
|
format!("[{}]", quoted.join(", "))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// KDE: the global-shortcuts entry for the `Search` action that
|
/// KDE: registration with the kglobalaccel daemon over DBus, which is the
|
||||||
/// `packaging/quicksearch.desktop` declares (`Exec=quicksearch --toggle`).
|
/// only writer `kglobalshortcutsrc` has — the daemon rewrites that file at
|
||||||
/// kglobalaccel launches desktop-file actions itself, so no command is
|
/// will and *drops* groups it did not create, so editing it directly (the
|
||||||
/// written here — only the key, in the file KDE's own Shortcuts settings
|
/// first version of this module) produced an entry that neither fired nor
|
||||||
/// page reads and edits.
|
/// survived. `setShortcut` takes effect immediately and the daemon does its
|
||||||
|
/// own persisting.
|
||||||
|
///
|
||||||
|
/// What the key *runs* is a small desktop file in
|
||||||
|
/// `~/.local/share/kglobalaccel/`, the directory Plasma itself uses for
|
||||||
|
/// custom command shortcuts: a component whose name ends in `.desktop`
|
||||||
|
/// resolves to that file, and its `_launch` action runs the `Exec` line.
|
||||||
|
/// The file's presence is also this module's "installed" marker — written
|
||||||
|
/// last on install, deleted on remove, and free of a per-frame DBus call.
|
||||||
|
///
|
||||||
|
/// Verified live against Plasma 6.6: register → the key launches a closed
|
||||||
|
/// QuickSearch; `unregister` → the daemon drops the entry from its config.
|
||||||
#[cfg(all(unix, not(target_os = "macos")))]
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
mod kde {
|
mod kde {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
const FILE: &str = "kglobalshortcutsrc";
|
/// Ends in `.desktop`: what makes the daemon treat the component as a
|
||||||
const GROUP: &str = "quicksearch.desktop";
|
/// service it can launch rather than an app that must be running.
|
||||||
|
const COMPONENT: &str = "quicksearch-search.desktop";
|
||||||
|
const ACTION: &str = "_launch";
|
||||||
|
/// KGlobalAccel's NoAutoloading flag: set the key now, not merely as a
|
||||||
|
/// default for the next load.
|
||||||
|
const SET_NOW: &str = "4";
|
||||||
|
|
||||||
/// Plasma 6's tool first; 5's second. The first present wins.
|
/// The action id every `org.kde.KGlobalAccel` call takes, in GVariant
|
||||||
fn config_tool(names: [&'static str; 2]) -> &'static str {
|
/// text: `[component, action, component friendly, action friendly]`.
|
||||||
let on_path = |name: &str| {
|
pub(super) fn action_id() -> String {
|
||||||
std::env::var_os("PATH").is_some_and(|path| {
|
format!("['{}', '{}', 'QuickSearch', 'QuickSearch']", COMPONENT, ACTION)
|
||||||
std::env::split_paths(&path).any(|dir| dir.join(name).is_file())
|
|
||||||
})
|
|
||||||
};
|
|
||||||
if on_path(names[0]) {
|
|
||||||
names[0]
|
|
||||||
} else {
|
|
||||||
names[1]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The entry format is `active,default,description`.
|
/// One `gdbus call` against the daemon. gdbus over qdbus because it
|
||||||
pub(super) fn entry(binding: &Binding) -> String {
|
/// takes GVariant text for the list arguments, which qdbus cannot spell.
|
||||||
format!("{},none,Search", binding)
|
fn call(method: &str, args: &[&str]) -> Result<String, String> {
|
||||||
|
let method = format!("org.kde.KGlobalAccel.{}", method);
|
||||||
|
let mut argv = vec![
|
||||||
|
"call",
|
||||||
|
"--session",
|
||||||
|
"--dest",
|
||||||
|
"org.kde.kglobalaccel",
|
||||||
|
"--object-path",
|
||||||
|
"/kglobalaccel",
|
||||||
|
"--method",
|
||||||
|
&method,
|
||||||
|
];
|
||||||
|
argv.extend(args);
|
||||||
|
run("gdbus", &argv)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where the launch target lives; the daemon looks here by name.
|
||||||
|
pub(super) fn desktop_file() -> PathBuf {
|
||||||
|
let data = std::env::var_os("XDG_DATA_HOME")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.filter(|p| p.is_absolute())
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
PathBuf::from(std::env::var_os("HOME").unwrap_or_default())
|
||||||
|
.join(".local/share")
|
||||||
|
});
|
||||||
|
data.join("kglobalaccel").join(COMPONENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `NoDisplay`: the entry exists to be launched by a key, not to appear
|
||||||
|
/// in menus next to the real QuickSearch entry.
|
||||||
|
pub(super) fn desktop_entry(command: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"[Desktop Entry]\nType=Application\nName=QuickSearch\nNoDisplay=true\nExec={}\n",
|
||||||
|
command
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn installed() -> bool {
|
pub(super) fn installed() -> bool {
|
||||||
let tool = config_tool(["kreadconfig6", "kreadconfig5"]);
|
desktop_file().is_file()
|
||||||
run(tool, &["--file", FILE, "--group", GROUP, "--key", "Search"])
|
|
||||||
.map(|out| {
|
|
||||||
let active = out.trim().split(',').next().unwrap_or("");
|
|
||||||
!active.is_empty() && active != "none"
|
|
||||||
})
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn install(binding: &Binding) -> Result<(), String> {
|
pub(super) fn install(binding: &Binding, command: &str) -> Result<(), String> {
|
||||||
let tool = config_tool(["kwriteconfig6", "kwriteconfig5"]);
|
let id = action_id();
|
||||||
let entry = entry(binding);
|
call("doRegister", &[&id])?;
|
||||||
for (key, value) in [("_k_friendly_name", "QuickSearch"), ("Search", &entry)] {
|
let keys = format!("[{}]", binding.qt_key_code());
|
||||||
run(
|
let reply = call("setShortcut", &[&id, &keys, SET_NOW])?;
|
||||||
tool,
|
// The daemon answers with the keys now in force; ours missing means
|
||||||
&["--file", FILE, "--group", GROUP, "--key", key, value],
|
// it kept something else — the key is taken.
|
||||||
)?;
|
if !reply_ints(&reply).contains(&binding.qt_key_code()) {
|
||||||
|
let _ = call("unregister", &[COMPONENT, ACTION]);
|
||||||
|
return Err(format!(
|
||||||
|
"your desktop refused {} — probably already in use",
|
||||||
|
binding
|
||||||
|
));
|
||||||
}
|
}
|
||||||
reload();
|
// The file last: it is the installed marker, so nothing marks this
|
||||||
|
// installed until the key is actually in force.
|
||||||
|
let path = desktop_file();
|
||||||
|
if let Some(dir) = path.parent() {
|
||||||
|
std::fs::create_dir_all(dir).map_err(|e| format!("creating {}: {}", dir.display(), e))?;
|
||||||
|
}
|
||||||
|
std::fs::write(&path, desktop_entry(command))
|
||||||
|
.map_err(|e| format!("writing {}: {}", path.display(), e))?;
|
||||||
|
// Executable, or KConfig refuses to trust the file's Exec line
|
||||||
|
// ("not owned by root and executable flag not set") — the same bit
|
||||||
|
// Plasma's own Shortcuts page sets on the files it creates here.
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755))
|
||||||
|
.map_err(|e| format!("marking {} executable: {}", path.display(), e))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn remove() -> Result<(), String> {
|
pub(super) fn remove() -> Result<(), String> {
|
||||||
let tool = config_tool(["kwriteconfig6", "kwriteconfig5"]);
|
call("unregister", &[COMPONENT, ACTION])?;
|
||||||
for key in ["Search", "_k_friendly_name"] {
|
let path = desktop_file();
|
||||||
run(
|
match std::fs::remove_file(&path) {
|
||||||
tool,
|
Ok(()) => Ok(()),
|
||||||
&["--file", FILE, "--group", GROUP, "--key", key, "--delete"],
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||||
)?;
|
Err(e) => Err(format!("deleting {}: {}", path.display(), e)),
|
||||||
}
|
}
|
||||||
reload();
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ask kglobalaccel to re-read its file. Best-effort: without it the
|
/// The integers out of a gdbus reply like `([100663366],)`. Wrong or
|
||||||
/// binding takes effect at the next login, which install's caller says.
|
/// hostile shapes yield fewer integers, never a panic.
|
||||||
fn reload() {
|
pub(super) fn reply_ints(reply: &str) -> Vec<u32> {
|
||||||
for qdbus in ["qdbus6", "qdbus"] {
|
reply
|
||||||
if run(
|
.split(|c: char| !c.is_ascii_digit())
|
||||||
qdbus,
|
.filter(|s| !s.is_empty())
|
||||||
&[
|
.filter_map(|s| s.parse().ok())
|
||||||
"org.kde.kglobalaccel",
|
.collect()
|
||||||
"/kglobalaccel",
|
|
||||||
"org.kde.KGlobalAccel.reloadConfig",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
.is_ok()
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -402,11 +495,50 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What the KDE launch key runs: a well-formed desktop entry whose Exec
|
||||||
|
/// is the toggle command, hidden from menus.
|
||||||
#[cfg(all(unix, not(target_os = "macos")))]
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
#[test]
|
#[test]
|
||||||
fn the_kde_entry_carries_the_binding_first() {
|
fn the_kde_desktop_entry_launches_the_toggle() {
|
||||||
let binding: Binding = "Ctrl+Shift+F".parse().unwrap();
|
let entry = kde::desktop_entry("/opt/qs/quicksearch --toggle");
|
||||||
assert_eq!(kde::entry(&binding), "Ctrl+Shift+F,none,Search");
|
assert!(entry.starts_with("[Desktop Entry]\n"));
|
||||||
|
assert!(entry.contains("Exec=/opt/qs/quicksearch --toggle\n"));
|
||||||
|
assert!(entry.contains("NoDisplay=true\n"));
|
||||||
|
assert!(kde::desktop_file().ends_with("kglobalaccel/quicksearch-search.desktop"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The GVariant action id every KGlobalAccel call names; `_launch` is
|
||||||
|
/// the action that runs a `.desktop` component's Exec.
|
||||||
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
|
#[test]
|
||||||
|
fn the_kde_action_id_names_the_launch_action() {
|
||||||
|
assert_eq!(
|
||||||
|
kde::action_id(),
|
||||||
|
"['quicksearch-search.desktop', '_launch', 'QuickSearch', 'QuickSearch']"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// gdbus replies, including hostile ones, must parse to integers or to
|
||||||
|
/// nothing — never panic.
|
||||||
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
|
#[test]
|
||||||
|
fn gdbus_replies_parse_to_integers_or_nothing() {
|
||||||
|
assert_eq!(kde::reply_ints("([100663366],)"), [100663366]);
|
||||||
|
assert_eq!(kde::reply_ints("([1, 2],)"), [1, 2]);
|
||||||
|
assert_eq!(kde::reply_ints("([],)"), Vec::<u32>::new());
|
||||||
|
for garbage in ["", "(true,)", "nonsense", "([99999999999999999999],)"] {
|
||||||
|
let _ = kde::reply_ints(garbage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A path with a space would otherwise split into a broken Exec line.
|
||||||
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
|
#[test]
|
||||||
|
fn the_toggle_command_survives_spaces_in_the_path() {
|
||||||
|
// `toggle_command` reads the real exe path; both shapes it can
|
||||||
|
// produce must parse back to program + one flag.
|
||||||
|
let command = toggle_command();
|
||||||
|
assert!(command.ends_with(" --toggle"), "{command}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The fixed GNOME path is load-bearing twice over: idempotence and
|
/// The fixed GNOME path is load-bearing twice over: idempotence and
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
//! Plain-language tooltips: every configuration control explains itself on hover.
|
//! Plain-language tooltips: every configuration control, and any status line
|
||||||
|
//! whose wait needs explaining, explains itself on hover.
|
||||||
|
|
||||||
const TIP_WIDTH: f32 = 420.0;
|
const TIP_WIDTH: f32 = 420.0;
|
||||||
|
|
||||||
|
|
@ -559,6 +560,18 @@ pub static CLEAR_INDEX: Tip = Tip {
|
||||||
caution: Some("This cannot be undone: the index has to be built from scratch again."),
|
caution: Some("This cannot be undone: the index has to be built from scratch again."),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Index status --------------------------------------------------------
|
||||||
|
|
||||||
|
pub static REBUILDING_FTS: Tip = Tip {
|
||||||
|
title: "Rebuilding the full-text search cache",
|
||||||
|
body: "A setting changed that affects which text is searchable, so \
|
||||||
|
QuickSearch is modifying the search index. On a \
|
||||||
|
large index this can take several minutes, and the progress may look \
|
||||||
|
frozen while it runs. You can still search during this time!",
|
||||||
|
examples: &[],
|
||||||
|
caution: None,
|
||||||
|
};
|
||||||
|
|
||||||
// --- Manage Index tab: indexed folders -----------------------------------
|
// --- Manage Index tab: indexed folders -----------------------------------
|
||||||
|
|
||||||
pub static ADD_ROOT: Tip = Tip {
|
pub static ADD_ROOT: Tip = Tip {
|
||||||
|
|
@ -711,6 +724,7 @@ mod tests {
|
||||||
&STOP_INDEXING,
|
&STOP_INDEXING,
|
||||||
&RETURN_TO_AUTO,
|
&RETURN_TO_AUTO,
|
||||||
&CLEAR_INDEX,
|
&CLEAR_INDEX,
|
||||||
|
&REBUILDING_FTS,
|
||||||
&ADD_ROOT,
|
&ADD_ROOT,
|
||||||
&REMOVE_ROOT,
|
&REMOVE_ROOT,
|
||||||
&ROOT_WORKERS,
|
&ROOT_WORKERS,
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,10 @@ Section "Search hotkey (Ctrl+Shift+F)" SecHotkey
|
||||||
; on the Settings tab rewrites this .lnk to match (per-user installs
|
; on the Settings tab rewrites this .lnk to match (per-user installs
|
||||||
; only; this all-users file needs elevation).
|
; only; this all-users file needs elevation).
|
||||||
;
|
;
|
||||||
|
; Explorer registers this key at logon, so the app's own RegisterHotKey
|
||||||
|
; for the same combination loses; either way the press works — directly,
|
||||||
|
; or by Explorer launching `--toggle`, which relays to a running window.
|
||||||
|
;
|
||||||
; Rewrites the same shortcut the section above creates: NSIS cannot add a
|
; Rewrites the same shortcut the section above creates: NSIS cannot add a
|
||||||
; hotkey to an existing .lnk, and creating it twice is harmless.
|
; hotkey to an existing .lnk, and creating it twice is harmless.
|
||||||
CreateShortcut "$SMPROGRAMS\${APP}.lnk" "$INSTDIR\quicksearch.exe" "--toggle" \
|
CreateShortcut "$SMPROGRAMS\${APP}.lnk" "$INSTDIR\quicksearch.exe" "--toggle" \
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue