commit
152402b74e
2
.github/scripts/ubuntu/Dockerfile
vendored
2
.github/scripts/ubuntu/Dockerfile
vendored
|
@ -31,7 +31,7 @@ RUN set -eux; \
|
||||||
cargo --version; \
|
cargo --version; \
|
||||||
rustc --version;
|
rustc --version;
|
||||||
|
|
||||||
RUN mkdir espanso && cargo install --force cargo-make --version 0.34.0
|
RUN mkdir espanso && cargo install rust-script --version "0.7.0" && cargo install --force cargo-make --version 0.34.0
|
||||||
|
|
||||||
COPY . espanso
|
COPY . espanso
|
||||||
|
|
||||||
|
|
9
.github/workflows/ci.yml
vendored
9
.github/workflows/ci.yml
vendored
|
@ -33,8 +33,9 @@ jobs:
|
||||||
cargo clippy -- -D warnings
|
cargo clippy -- -D warnings
|
||||||
env:
|
env:
|
||||||
MACOSX_DEPLOYMENT_TARGET: "10.13"
|
MACOSX_DEPLOYMENT_TARGET: "10.13"
|
||||||
- name: Install cargo-make
|
- name: Install rust-script and cargo-make
|
||||||
run: |
|
run: |
|
||||||
|
cargo install rust-script --version "0.7.0"
|
||||||
cargo install --force cargo-make --version 0.34.0
|
cargo install --force cargo-make --version 0.34.0
|
||||||
- name: Run test suite
|
- name: Run test suite
|
||||||
run: cargo make test-binary
|
run: cargo make test-binary
|
||||||
|
@ -58,8 +59,9 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
rustup component add clippy
|
rustup component add clippy
|
||||||
cargo clippy -p espanso --features wayland -- -D warnings
|
cargo clippy -p espanso --features wayland -- -D warnings
|
||||||
- name: Install cargo-make
|
- name: Install rust-script and cargo-make
|
||||||
run: |
|
run: |
|
||||||
|
cargo install rust-script --version "0.7.0"
|
||||||
cargo install --force cargo-make --version 0.34.0
|
cargo install --force cargo-make --version 0.34.0
|
||||||
- name: Run test suite
|
- name: Run test suite
|
||||||
run: cargo make test-binary --env NO_X11=true
|
run: cargo make test-binary --env NO_X11=true
|
||||||
|
@ -72,8 +74,9 @@ jobs:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Install target
|
- name: Install target
|
||||||
run: rustup update && rustup target add aarch64-apple-darwin
|
run: rustup update && rustup target add aarch64-apple-darwin
|
||||||
- name: Install cargo-make
|
- name: Install rust-script and cargo-make
|
||||||
run: |
|
run: |
|
||||||
|
cargo install rust-script --version "0.7.0"
|
||||||
cargo install --force cargo-make --version 0.34.0
|
cargo install --force cargo-make --version 0.34.0
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
|
|
9
.github/workflows/release.yml
vendored
9
.github/workflows/release.yml
vendored
|
@ -59,8 +59,9 @@ jobs:
|
||||||
- name: Print target version
|
- name: Print target version
|
||||||
run: |
|
run: |
|
||||||
echo Using version ${{ needs.extract-version.outputs.espanso_version }}
|
echo Using version ${{ needs.extract-version.outputs.espanso_version }}
|
||||||
- name: Install cargo-make
|
- name: Install rust-script and cargo-make
|
||||||
run: |
|
run: |
|
||||||
|
cargo install rust-script --version "0.7.0"
|
||||||
cargo install --force cargo-make --version 0.34.0
|
cargo install --force cargo-make --version 0.34.0
|
||||||
- name: Test
|
- name: Test
|
||||||
run: cargo make test-binary --profile release
|
run: cargo make test-binary --profile release
|
||||||
|
@ -126,8 +127,9 @@ jobs:
|
||||||
- name: Print target version
|
- name: Print target version
|
||||||
run: |
|
run: |
|
||||||
echo Using version ${{ needs.extract-version.outputs.espanso_version }}
|
echo Using version ${{ needs.extract-version.outputs.espanso_version }}
|
||||||
- name: Install cargo-make
|
- name: Install rust-script and cargo-make
|
||||||
run: |
|
run: |
|
||||||
|
cargo install rust-script --version "0.7.0"
|
||||||
cargo install --force cargo-make --version 0.34.0
|
cargo install --force cargo-make --version 0.34.0
|
||||||
- name: Test
|
- name: Test
|
||||||
run: cargo make test-binary --profile release
|
run: cargo make test-binary --profile release
|
||||||
|
@ -166,8 +168,9 @@ jobs:
|
||||||
echo Using version ${{ needs.extract-version.outputs.espanso_version }}
|
echo Using version ${{ needs.extract-version.outputs.espanso_version }}
|
||||||
- name: Install rust target
|
- name: Install rust target
|
||||||
run: rustup update && rustup target add aarch64-apple-darwin
|
run: rustup update && rustup target add aarch64-apple-darwin
|
||||||
- name: Install cargo-make
|
- name: Install rust-script and cargo-make
|
||||||
run: |
|
run: |
|
||||||
|
cargo install rust-script --version "0.7.0"
|
||||||
cargo install --force cargo-make --version 0.34.0
|
cargo install --force cargo-make --version 0.34.0
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo make create-bundle --profile release --env BUILD_ARCH=aarch64-apple-darwin
|
run: cargo make create-bundle --profile release --env BUILD_ARCH=aarch64-apple-darwin
|
||||||
|
|
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -572,7 +572,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "espanso"
|
name = "espanso"
|
||||||
version = "2.1.1-alpha"
|
version = "2.1.2-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"caps",
|
"caps",
|
||||||
|
|
|
@ -49,18 +49,11 @@ pub trait Clipboard {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
#[derive(Default)]
|
||||||
pub struct ClipboardOperationOptions {
|
pub struct ClipboardOperationOptions {
|
||||||
pub use_xclip_backend: bool,
|
pub use_xclip_backend: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ClipboardOperationOptions {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
use_xclip_backend: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub struct ClipboardOptions {
|
pub struct ClipboardOptions {
|
||||||
// Wayland-only
|
// Wayland-only
|
||||||
|
|
|
@ -135,7 +135,7 @@ impl Clipboard for WaylandFallbackClipboard {
|
||||||
file.read_to_end(&mut data)?;
|
file.read_to_end(&mut data)?;
|
||||||
|
|
||||||
self.invoke_command_with_timeout(
|
self.invoke_command_with_timeout(
|
||||||
&mut Command::new("wl-copy").arg("--type").arg("image/png"),
|
Command::new("wl-copy").arg("--type").arg("image/png"),
|
||||||
&data,
|
&data,
|
||||||
"wl-copy",
|
"wl-copy",
|
||||||
)
|
)
|
||||||
|
@ -148,7 +148,7 @@ impl Clipboard for WaylandFallbackClipboard {
|
||||||
_: &ClipboardOperationOptions,
|
_: &ClipboardOperationOptions,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
self.invoke_command_with_timeout(
|
self.invoke_command_with_timeout(
|
||||||
&mut Command::new("wl-copy").arg("--type").arg("text/html"),
|
Command::new("wl-copy").arg("--type").arg("text/html"),
|
||||||
html.as_bytes(),
|
html.as_bytes(),
|
||||||
"wl-copy",
|
"wl-copy",
|
||||||
)
|
)
|
||||||
|
|
|
@ -30,7 +30,7 @@ pub struct XClipClipboard {
|
||||||
|
|
||||||
impl XClipClipboard {
|
impl XClipClipboard {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let command = Command::new("xclipz").arg("-h").output();
|
let command = Command::new("xclip").arg("-h").output();
|
||||||
let is_xclip_available = command
|
let is_xclip_available = command
|
||||||
.map(|output| output.status.success())
|
.map(|output| output.status.success())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
|
@ -37,7 +37,7 @@ use thiserror::Error;
|
||||||
|
|
||||||
const STANDARD_INCLUDES: &[&str] = &["../match/**/[!_]*.yml"];
|
const STANDARD_INCLUDES: &[&str] = &["../match/**/[!_]*.yml"];
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub(crate) struct ResolvedConfig {
|
pub(crate) struct ResolvedConfig {
|
||||||
parsed: ParsedConfig,
|
parsed: ParsedConfig,
|
||||||
|
|
||||||
|
@ -52,20 +52,6 @@ pub(crate) struct ResolvedConfig {
|
||||||
filter_exec: Option<Regex>,
|
filter_exec: Option<Regex>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ResolvedConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
parsed: Default::default(),
|
|
||||||
source_path: None,
|
|
||||||
id: 0,
|
|
||||||
match_paths: Vec::new(),
|
|
||||||
filter_title: None,
|
|
||||||
filter_class: None,
|
|
||||||
filter_exec: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config for ResolvedConfig {
|
impl Config for ResolvedConfig {
|
||||||
fn id(&self) -> i32 {
|
fn id(&self) -> i32 {
|
||||||
self.id
|
self.id
|
||||||
|
@ -202,13 +188,10 @@ impl Config for ResolvedConfig {
|
||||||
Some("left_shift") => Some(ToggleKey::LeftShift),
|
Some("left_shift") => Some(ToggleKey::LeftShift),
|
||||||
Some("left_meta") | Some("left_cmd") => Some(ToggleKey::LeftMeta),
|
Some("left_meta") | Some("left_cmd") => Some(ToggleKey::LeftMeta),
|
||||||
Some("off") => None,
|
Some("off") => None,
|
||||||
None => Some(ToggleKey::Alt),
|
None => None,
|
||||||
err => {
|
err => {
|
||||||
error!(
|
error!("invalid toggle_key specified {:?}", err);
|
||||||
"invalid toggle_key specified {:?}, falling back to ALT",
|
None
|
||||||
err
|
|
||||||
);
|
|
||||||
Some(ToggleKey::Alt)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -701,18 +701,15 @@ impl LegacyConfigSet {
|
||||||
let mut sorted_triggers: Vec<String> = default
|
let mut sorted_triggers: Vec<String> = default
|
||||||
.matches
|
.matches
|
||||||
.iter()
|
.iter()
|
||||||
.flat_map(|t| triggers_for_match(t))
|
.flat_map(triggers_for_match)
|
||||||
.collect();
|
.collect();
|
||||||
sorted_triggers.sort();
|
sorted_triggers.sort();
|
||||||
|
|
||||||
let mut has_conflicts = Self::list_has_conflicts(&sorted_triggers);
|
let mut has_conflicts = Self::list_has_conflicts(&sorted_triggers);
|
||||||
|
|
||||||
for s in specific.iter() {
|
for s in specific.iter() {
|
||||||
let mut specific_triggers: Vec<String> = s
|
let mut specific_triggers: Vec<String> =
|
||||||
.matches
|
s.matches.iter().flat_map(triggers_for_match).collect();
|
||||||
.iter()
|
|
||||||
.flat_map(|t| triggers_for_match(t))
|
|
||||||
.collect();
|
|
||||||
specific_triggers.sort();
|
specific_triggers.sort();
|
||||||
has_conflicts |= Self::list_has_conflicts(&specific_triggers);
|
has_conflicts |= Self::list_has_conflicts(&specific_triggers);
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,23 +27,13 @@ use super::{Match, Variable};
|
||||||
pub(crate) mod loader;
|
pub(crate) mod loader;
|
||||||
mod path;
|
mod path;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
pub(crate) struct MatchGroup {
|
pub(crate) struct MatchGroup {
|
||||||
pub imports: Vec<String>,
|
pub imports: Vec<String>,
|
||||||
pub global_vars: Vec<Variable>,
|
pub global_vars: Vec<Variable>,
|
||||||
pub matches: Vec<Match>,
|
pub matches: Vec<Match>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for MatchGroup {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
imports: Vec::new(),
|
|
||||||
global_vars: Vec::new(),
|
|
||||||
matches: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MatchGroup {
|
impl MatchGroup {
|
||||||
// TODO: test
|
// TODO: test
|
||||||
pub fn load(group_path: &Path) -> Result<(Self, Option<NonFatalErrorSet>)> {
|
pub fn load(group_path: &Path) -> Result<(Self, Option<NonFatalErrorSet>)> {
|
||||||
|
|
|
@ -132,19 +132,11 @@ pub enum UpperCasingStyle {
|
||||||
CapitalizeWords,
|
CapitalizeWords,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
|
||||||
pub struct RegexCause {
|
pub struct RegexCause {
|
||||||
pub regex: String,
|
pub regex: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for RegexCause {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
regex: String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Effects
|
// Effects
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, EnumAsInner)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, EnumAsInner)]
|
||||||
|
@ -186,19 +178,11 @@ impl Default for TextEffect {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
|
||||||
pub struct ImageEffect {
|
pub struct ImageEffect {
|
||||||
pub path: String,
|
pub path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ImageEffect {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
path: String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct Variable {
|
pub struct Variable {
|
||||||
pub id: StructId,
|
pub id: StructId,
|
||||||
|
|
|
@ -89,6 +89,7 @@ impl Default for InjectionOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
#[derive(Default)]
|
||||||
pub struct InjectorCreationOptions {
|
pub struct InjectorCreationOptions {
|
||||||
// Only relevant in X11 Linux systems, use the EVDEV backend instead of X11.
|
// Only relevant in X11 Linux systems, use the EVDEV backend instead of X11.
|
||||||
pub use_evdev: bool,
|
pub use_evdev: bool,
|
||||||
|
@ -128,18 +129,6 @@ pub trait KeyboardStateProvider {
|
||||||
fn is_key_pressed(&self, code: u32) -> bool;
|
fn is_key_pressed(&self, code: u32) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for InjectorCreationOptions {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
use_evdev: false,
|
|
||||||
evdev_modifiers: None,
|
|
||||||
evdev_max_modifier_combination_len: None,
|
|
||||||
evdev_keyboard_rmlvo: None,
|
|
||||||
keyboard_state_provider: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub fn get_injector(_options: InjectorCreationOptions) -> Result<Box<dyn Injector>> {
|
pub fn get_injector(_options: InjectorCreationOptions) -> Result<Box<dyn Injector>> {
|
||||||
info!("using Win32Injector");
|
info!("using Win32Injector");
|
||||||
|
|
|
@ -40,19 +40,11 @@ impl<Id> RegexMatch<Id> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Default)]
|
||||||
pub struct RegexMatcherState {
|
pub struct RegexMatcherState {
|
||||||
buffer: String,
|
buffer: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for RegexMatcherState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
buffer: String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RegexMatcherOptions {
|
pub struct RegexMatcherOptions {
|
||||||
pub max_buffer_size: usize,
|
pub max_buffer_size: usize,
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,20 +50,12 @@ struct RollingMatcherStatePath<'a, Id> {
|
||||||
events: Vec<(Event, IsWordSeparator)>,
|
events: Vec<(Event, IsWordSeparator)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
pub struct RollingMatcherOptions {
|
pub struct RollingMatcherOptions {
|
||||||
pub char_word_separators: Vec<String>,
|
pub char_word_separators: Vec<String>,
|
||||||
pub key_word_separators: Vec<Key>,
|
pub key_word_separators: Vec<Key>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for RollingMatcherOptions {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
char_word_separators: Vec::new(),
|
|
||||||
key_word_separators: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RollingMatcher<Id> {
|
pub struct RollingMatcher<Id> {
|
||||||
char_word_separators: Vec<String>,
|
char_word_separators: Vec<String>,
|
||||||
key_word_separators: Vec<Key>,
|
key_word_separators: Vec<Key>,
|
||||||
|
|
|
@ -72,22 +72,13 @@ impl<Id> RollingMatch<Id> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
pub struct StringMatchOptions {
|
pub struct StringMatchOptions {
|
||||||
pub case_insensitive: bool,
|
pub case_insensitive: bool,
|
||||||
pub left_word: bool,
|
pub left_word: bool,
|
||||||
pub right_word: bool,
|
pub right_word: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for StringMatchOptions {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
case_insensitive: false,
|
|
||||||
left_word: false,
|
|
||||||
right_word: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -96,6 +96,7 @@ private:
|
||||||
void Submit();
|
void Submit();
|
||||||
void OnSubmitBtn(wxCommandEvent& event);
|
void OnSubmitBtn(wxCommandEvent& event);
|
||||||
void OnCharHook(wxKeyEvent& event);
|
void OnCharHook(wxKeyEvent& event);
|
||||||
|
void OnListBoxEvent(wxCommandEvent& event);
|
||||||
void UpdateHelpText();
|
void UpdateHelpText();
|
||||||
void HandleNormalFocus(wxFocusEvent& event);
|
void HandleNormalFocus(wxFocusEvent& event);
|
||||||
void HandleMultilineFocus(wxFocusEvent& event);
|
void HandleMultilineFocus(wxFocusEvent& event);
|
||||||
|
@ -141,7 +142,6 @@ FormFrame::FormFrame(const wxString& title, const wxPoint& pos, const wxSize& si
|
||||||
|
|
||||||
Bind(wxEVT_BUTTON, &FormFrame::OnSubmitBtn, this, ID_Submit);
|
Bind(wxEVT_BUTTON, &FormFrame::OnSubmitBtn, this, ID_Submit);
|
||||||
Bind(wxEVT_CHAR_HOOK, &FormFrame::OnCharHook, this, wxID_ANY);
|
Bind(wxEVT_CHAR_HOOK, &FormFrame::OnCharHook, this, wxID_ANY);
|
||||||
// TODO: register ESC click handler: https://forums.wxwidgets.org/viewtopic.php?t=41926
|
|
||||||
|
|
||||||
this->SetClientSize(panel->GetBestSize());
|
this->SetClientSize(panel->GetBestSize());
|
||||||
this->CentreOnScreen();
|
this->CentreOnScreen();
|
||||||
|
@ -218,6 +218,11 @@ void FormFrame::AddComponent(wxPanel *parent, wxBoxSizer *sizer, FieldMetadata m
|
||||||
}
|
}
|
||||||
|
|
||||||
((wxListBox*)choice)->Bind(wxEVT_SET_FOCUS, &FormFrame::HandleNormalFocus, this, wxID_ANY);
|
((wxListBox*)choice)->Bind(wxEVT_SET_FOCUS, &FormFrame::HandleNormalFocus, this, wxID_ANY);
|
||||||
|
// ListBoxes prevent the global CHAR_HOOK handler from handling the Return key
|
||||||
|
// correctly, so we need to handle the double click event too (which is triggered
|
||||||
|
// when the enter key is pressed).
|
||||||
|
// See: https://github.com/federico-terzi/espanso/issues/857
|
||||||
|
((wxListBox*)choice)->Bind(wxEVT_LISTBOX_DCLICK, &FormFrame::OnListBoxEvent, this, wxID_ANY);
|
||||||
|
|
||||||
// Create the field wrapper
|
// Create the field wrapper
|
||||||
std::unique_ptr<FieldWrapper> field((FieldWrapper*) new ListFieldWrapper((wxListBox*) choice));
|
std::unique_ptr<FieldWrapper> field((FieldWrapper*) new ListFieldWrapper((wxListBox*) choice));
|
||||||
|
@ -311,6 +316,10 @@ void FormFrame::OnCharHook(wxKeyEvent& event) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FormFrame::OnListBoxEvent(wxCommandEvent& event) {
|
||||||
|
Submit();
|
||||||
|
}
|
||||||
|
|
||||||
extern "C" void interop_show_form(FormMetadata * _metadata, void (*callback)(ValuePair *values, int size, void *data), void *data) {
|
extern "C" void interop_show_form(FormMetadata * _metadata, void (*callback)(ValuePair *values, int size, void *data), void *data) {
|
||||||
// Setup high DPI support on Windows
|
// Setup high DPI support on Windows
|
||||||
#ifdef __WXMSW__
|
#ifdef __WXMSW__
|
||||||
|
|
132
espanso-render/src/extension/choice.rs
Normal file
132
espanso-render/src/extension/choice.rs
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
/*
|
||||||
|
* This file is part of espanso.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* espanso is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* espanso is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use log::error;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::{Extension, ExtensionOutput, ExtensionResult, Params, Value};
|
||||||
|
|
||||||
|
pub trait ChoiceSelector {
|
||||||
|
fn show(&self, choices: &[Choice]) -> ChoiceSelectorResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Choice<'a> {
|
||||||
|
pub label: &'a str,
|
||||||
|
pub id: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum ChoiceSelectorResult {
|
||||||
|
Success(String),
|
||||||
|
Aborted,
|
||||||
|
Error(anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ChoiceExtension<'a> {
|
||||||
|
selector: &'a dyn ChoiceSelector,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::new_without_default)]
|
||||||
|
impl<'a> ChoiceExtension<'a> {
|
||||||
|
pub fn new(selector: &'a dyn ChoiceSelector) -> Self {
|
||||||
|
Self { selector }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Extension for ChoiceExtension<'a> {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"choice"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn calculate(
|
||||||
|
&self,
|
||||||
|
_: &crate::Context,
|
||||||
|
_: &crate::Scope,
|
||||||
|
params: &Params,
|
||||||
|
) -> crate::ExtensionResult {
|
||||||
|
let choices: Vec<Choice> = if let Some(Value::String(values)) = params.get("values") {
|
||||||
|
values
|
||||||
|
.lines()
|
||||||
|
.filter_map(|line| {
|
||||||
|
let trimmed_line = line.trim();
|
||||||
|
if !trimmed_line.is_empty() {
|
||||||
|
Some(trimmed_line)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(|line| Choice {
|
||||||
|
label: line,
|
||||||
|
id: line,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else if let Some(Value::Array(values)) = params.get("values") {
|
||||||
|
let choices: Result<Vec<Choice>> = values
|
||||||
|
.iter()
|
||||||
|
.map(|value| match value {
|
||||||
|
Value::String(string) => Ok(Choice {
|
||||||
|
id: string,
|
||||||
|
label: string,
|
||||||
|
}),
|
||||||
|
Value::Object(fields) => Ok(Choice {
|
||||||
|
id: fields
|
||||||
|
.get("id")
|
||||||
|
.and_then(|val| val.as_string())
|
||||||
|
.ok_or(ChoiceError::InvalidObjectValue)?,
|
||||||
|
label: fields
|
||||||
|
.get("label")
|
||||||
|
.and_then(|val| val.as_string())
|
||||||
|
.ok_or(ChoiceError::InvalidObjectValue)?,
|
||||||
|
}),
|
||||||
|
_ => Err(ChoiceError::InvalidValueType.into()),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
match choices {
|
||||||
|
Ok(choices) => choices,
|
||||||
|
Err(err) => {
|
||||||
|
return crate::ExtensionResult::Error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return crate::ExtensionResult::Error(ChoiceError::MissingValues.into());
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.selector.show(&choices) {
|
||||||
|
ChoiceSelectorResult::Success(choice_id) => {
|
||||||
|
ExtensionResult::Success(ExtensionOutput::Single(choice_id))
|
||||||
|
}
|
||||||
|
ChoiceSelectorResult::Aborted => ExtensionResult::Aborted,
|
||||||
|
ChoiceSelectorResult::Error(error) => ExtensionResult::Error(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum ChoiceError {
|
||||||
|
#[error("missing values parameter")]
|
||||||
|
MissingValues,
|
||||||
|
|
||||||
|
#[error("values contain object items, but they are missing either the 'id' or 'label' fields")]
|
||||||
|
InvalidObjectValue,
|
||||||
|
|
||||||
|
#[error("values contain an invalid item type. items can only be strings or objects")]
|
||||||
|
InvalidValueType,
|
||||||
|
}
|
|
@ -17,6 +17,7 @@
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
pub mod choice;
|
||||||
pub mod clipboard;
|
pub mod clipboard;
|
||||||
pub mod date;
|
pub mod date;
|
||||||
pub mod echo;
|
pub mod echo;
|
||||||
|
|
|
@ -42,20 +42,12 @@ pub enum RenderResult {
|
||||||
Error(anyhow::Error),
|
Error(anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
pub struct Context<'a> {
|
pub struct Context<'a> {
|
||||||
pub global_vars: Vec<&'a Variable>,
|
pub global_vars: Vec<&'a Variable>,
|
||||||
pub templates: Vec<&'a Template>,
|
pub templates: Vec<&'a Template>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Default for Context<'a> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
global_vars: Vec::new(),
|
|
||||||
templates: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct RenderOptions {
|
pub struct RenderOptions {
|
||||||
pub casing_style: CasingStyle,
|
pub casing_style: CasingStyle,
|
||||||
|
@ -77,23 +69,13 @@ pub enum CasingStyle {
|
||||||
Uppercase,
|
Uppercase,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
pub struct Template {
|
pub struct Template {
|
||||||
pub ids: Vec<String>,
|
pub ids: Vec<String>,
|
||||||
pub body: String,
|
pub body: String,
|
||||||
pub vars: Vec<Variable>,
|
pub vars: Vec<Variable>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Template {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
ids: Vec::new(),
|
|
||||||
body: "".to_string(),
|
|
||||||
vars: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct Variable {
|
pub struct Variable {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "espanso"
|
name = "espanso"
|
||||||
version = "2.1.1-alpha"
|
version = "2.1.2-alpha"
|
||||||
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
|
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
description = "Cross-platform Text Expander written in Rust"
|
description = "Cross-platform Text Expander written in Rust"
|
||||||
|
|
|
@ -74,6 +74,7 @@ pub enum LogMode {
|
||||||
CleanAndAppend,
|
CleanAndAppend,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
pub struct CliModuleArgs {
|
pub struct CliModuleArgs {
|
||||||
pub config_store: Option<Box<dyn ConfigStore>>,
|
pub config_store: Option<Box<dyn ConfigStore>>,
|
||||||
pub match_store: Option<Box<dyn MatchStore>>,
|
pub match_store: Option<Box<dyn MatchStore>>,
|
||||||
|
@ -84,20 +85,6 @@ pub struct CliModuleArgs {
|
||||||
pub cli_args: Option<ArgMatches<'static>>,
|
pub cli_args: Option<ArgMatches<'static>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for CliModuleArgs {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
config_store: None,
|
|
||||||
match_store: None,
|
|
||||||
is_legacy_config: false,
|
|
||||||
non_fatal_errors: Vec::new(),
|
|
||||||
paths: None,
|
|
||||||
paths_overrides: None,
|
|
||||||
cli_args: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PathsOverrides {
|
pub struct PathsOverrides {
|
||||||
pub config: Option<PathBuf>,
|
pub config: Option<PathBuf>,
|
||||||
pub runtime: Option<PathBuf>,
|
pub runtime: Option<PathBuf>,
|
||||||
|
|
|
@ -81,6 +81,11 @@ impl<'a> ClipboardInjectorAdapter<'a> {
|
||||||
custom_combination
|
custom_combination
|
||||||
} else if cfg!(target_os = "macos") {
|
} else if cfg!(target_os = "macos") {
|
||||||
vec![Key::Meta, Key::V]
|
vec![Key::Meta, Key::V]
|
||||||
|
} else if cfg!(target_os = "linux") && cfg!(feature = "wayland") {
|
||||||
|
// Because on Wayland we currently don't have app-specific configs (and therefore no patches)
|
||||||
|
// we switch to the more supported SHIFT+INSERT combination
|
||||||
|
// See: https://github.com/federico-terzi/espanso/issues/899
|
||||||
|
vec![Key::Shift, Key::Insert]
|
||||||
} else {
|
} else {
|
||||||
vec![Key::Control, Key::V]
|
vec![Key::Control, Key::V]
|
||||||
};
|
};
|
||||||
|
|
|
@ -58,11 +58,7 @@ fn convert_to_ui_menu_item(
|
||||||
espanso_engine::event::ui::MenuItem::Sub(sub) => {
|
espanso_engine::event::ui::MenuItem::Sub(sub) => {
|
||||||
espanso_ui::menu::MenuItem::Sub(espanso_ui::menu::SubMenuItem {
|
espanso_ui::menu::MenuItem::Sub(espanso_ui::menu::SubMenuItem {
|
||||||
label: sub.label.clone(),
|
label: sub.label.clone(),
|
||||||
items: sub
|
items: sub.items.iter().map(convert_to_ui_menu_item).collect(),
|
||||||
.items
|
|
||||||
.iter()
|
|
||||||
.map(|item| convert_to_ui_menu_item(item))
|
|
||||||
.collect(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
espanso_engine::event::ui::MenuItem::Separator => espanso_ui::menu::MenuItem::Separator,
|
espanso_engine::event::ui::MenuItem::Separator => espanso_ui::menu::MenuItem::Separator,
|
||||||
|
|
|
@ -76,18 +76,11 @@ impl KeyStateStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
struct KeyState {
|
struct KeyState {
|
||||||
keys: HashMap<u32, KeyStatus>,
|
keys: HashMap<u32, KeyStatus>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for KeyState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
keys: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct KeyStatus {
|
struct KeyStatus {
|
||||||
pressed_at: Option<Instant>,
|
pressed_at: Option<Instant>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,9 @@ use crate::{
|
||||||
},
|
},
|
||||||
multiplex::MultiplexAdapter,
|
multiplex::MultiplexAdapter,
|
||||||
render::{
|
render::{
|
||||||
extension::{clipboard::ClipboardAdapter, form::FormProviderAdapter},
|
extension::{
|
||||||
|
choice::ChoiceSelectorAdapter, clipboard::ClipboardAdapter, form::FormProviderAdapter,
|
||||||
|
},
|
||||||
RendererAdapter,
|
RendererAdapter,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -198,6 +200,9 @@ pub fn initialize_and_spawn(
|
||||||
let shell_extension = espanso_render::extension::shell::ShellExtension::new(&paths.config);
|
let shell_extension = espanso_render::extension::shell::ShellExtension::new(&paths.config);
|
||||||
let form_adapter = FormProviderAdapter::new(&modulo_form_ui);
|
let form_adapter = FormProviderAdapter::new(&modulo_form_ui);
|
||||||
let form_extension = espanso_render::extension::form::FormExtension::new(&form_adapter);
|
let form_extension = espanso_render::extension::form::FormExtension::new(&form_adapter);
|
||||||
|
let choice_adapter = ChoiceSelectorAdapter::new(&modulo_search_ui);
|
||||||
|
let choice_extension =
|
||||||
|
espanso_render::extension::choice::ChoiceExtension::new(&choice_adapter);
|
||||||
let renderer = espanso_render::create(vec![
|
let renderer = espanso_render::create(vec![
|
||||||
&clipboard_extension,
|
&clipboard_extension,
|
||||||
&date_extension,
|
&date_extension,
|
||||||
|
@ -207,6 +212,7 @@ pub fn initialize_and_spawn(
|
||||||
&script_extension,
|
&script_extension,
|
||||||
&shell_extension,
|
&shell_extension,
|
||||||
&form_extension,
|
&form_extension,
|
||||||
|
&choice_extension,
|
||||||
]);
|
]);
|
||||||
let renderer_adapter = RendererAdapter::new(&match_cache, &config_manager, &renderer);
|
let renderer_adapter = RendererAdapter::new(&match_cache, &config_manager, &renderer);
|
||||||
let path_provider = PathProviderAdapter::new(&paths);
|
let path_provider = PathProviderAdapter::new(&paths);
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
* This file is part of espanso.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* espanso is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* espanso is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use espanso_render::extension::choice::{ChoiceSelector, ChoiceSelectorResult};
|
||||||
|
|
||||||
|
use crate::gui::{SearchItem, SearchUI};
|
||||||
|
|
||||||
|
pub struct ChoiceSelectorAdapter<'a> {
|
||||||
|
search_ui: &'a dyn SearchUI,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ChoiceSelectorAdapter<'a> {
|
||||||
|
pub fn new(search_ui: &'a dyn SearchUI) -> Self {
|
||||||
|
Self { search_ui }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ChoiceSelector for ChoiceSelectorAdapter<'a> {
|
||||||
|
fn show(&self, choices: &[espanso_render::extension::choice::Choice]) -> ChoiceSelectorResult {
|
||||||
|
let items = convert_items(choices);
|
||||||
|
match self.search_ui.show(&items, None) {
|
||||||
|
Ok(Some(choice)) => ChoiceSelectorResult::Success(choice),
|
||||||
|
Ok(None) => ChoiceSelectorResult::Aborted,
|
||||||
|
Err(err) => ChoiceSelectorResult::Error(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_items(choices: &[espanso_render::extension::choice::Choice]) -> Vec<SearchItem> {
|
||||||
|
choices
|
||||||
|
.iter()
|
||||||
|
.map(|choice| SearchItem {
|
||||||
|
id: choice.id.to_string(),
|
||||||
|
label: choice.label.to_string(),
|
||||||
|
tag: None,
|
||||||
|
is_builtin: false,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
|
@ -17,5 +17,6 @@
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
pub mod choice;
|
||||||
pub mod clipboard;
|
pub mod clipboard;
|
||||||
pub mod form;
|
pub mod form;
|
||||||
|
|
|
@ -31,6 +31,7 @@ pub fn patch_store(store: Box<dyn ConfigStore>) -> Box<dyn ConfigStore> {
|
||||||
fn get_builtin_patches() -> Vec<PatchDefinition> {
|
fn get_builtin_patches() -> Vec<PatchDefinition> {
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
return vec![
|
return vec![
|
||||||
|
patches::win::brave::patch(),
|
||||||
patches::win::onenote_for_windows_10::patch(),
|
patches::win::onenote_for_windows_10::patch(),
|
||||||
patches::win::vscode_win::patch(),
|
patches::win::vscode_win::patch(),
|
||||||
];
|
];
|
||||||
|
|
41
espanso/src/patch/patches/win/brave.rs
Normal file
41
espanso/src/patch/patches/win/brave.rs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
/*
|
||||||
|
* This file is part of espanso.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* espanso is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* espanso is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::patch::patches::{PatchedConfig, Patches};
|
||||||
|
use crate::patch::PatchDefinition;
|
||||||
|
|
||||||
|
pub fn patch() -> PatchDefinition {
|
||||||
|
PatchDefinition {
|
||||||
|
name: module_path!().split(':').last().unwrap_or("unknown"),
|
||||||
|
is_enabled: || cfg!(target_os = "windows"),
|
||||||
|
should_patch: |app| app.exec.unwrap_or_default().contains("brave.exe"),
|
||||||
|
apply: |base, name| {
|
||||||
|
Arc::new(PatchedConfig::patch(
|
||||||
|
base,
|
||||||
|
name,
|
||||||
|
Patches {
|
||||||
|
pre_paste_delay: Some(400),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,5 +17,6 @@
|
||||||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
pub mod brave;
|
||||||
pub mod onenote_for_windows_10;
|
pub mod onenote_for_windows_10;
|
||||||
pub mod vscode_win;
|
pub mod vscode_win;
|
||||||
|
|
254
packager.py
254
packager.py
|
@ -1,254 +0,0 @@
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
import platform
|
|
||||||
import hashlib
|
|
||||||
import click
|
|
||||||
import shutil
|
|
||||||
import toml
|
|
||||||
import hashlib
|
|
||||||
import glob
|
|
||||||
import urllib.request
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
PACKAGER_TARGET_DIR = "target/packager"
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PackageInfo:
|
|
||||||
name: str
|
|
||||||
version: str
|
|
||||||
description: str
|
|
||||||
publisher: str
|
|
||||||
url: str
|
|
||||||
modulo_version: str
|
|
||||||
|
|
||||||
@click.group()
|
|
||||||
def cli():
|
|
||||||
pass
|
|
||||||
|
|
||||||
@cli.command()
|
|
||||||
@click.option('--skipcargo', default=False, is_flag=True, help="Skip cargo release build")
|
|
||||||
def build(skipcargo):
|
|
||||||
"""Build espanso distribution"""
|
|
||||||
# Check operating system
|
|
||||||
TARGET_OS = "macos"
|
|
||||||
if platform.system() == "Windows":
|
|
||||||
TARGET_OS = "windows"
|
|
||||||
elif platform.system() == "Linux":
|
|
||||||
TARGET_OS = "linux"
|
|
||||||
|
|
||||||
print("Detected OS:", TARGET_OS)
|
|
||||||
|
|
||||||
print("Loading info from Cargo.toml")
|
|
||||||
cargo_info = toml.load("Cargo.toml")
|
|
||||||
package_info = PackageInfo(cargo_info["package"]["name"],
|
|
||||||
cargo_info["package"]["version"],
|
|
||||||
cargo_info["package"]["description"],
|
|
||||||
cargo_info["package"]["authors"][0],
|
|
||||||
cargo_info["package"]["homepage"],
|
|
||||||
cargo_info["modulo"]["version"])
|
|
||||||
print(package_info)
|
|
||||||
|
|
||||||
if not skipcargo:
|
|
||||||
print("Building release version...")
|
|
||||||
subprocess.run(["cargo", "build", "--release"])
|
|
||||||
else:
|
|
||||||
print("Skipping build")
|
|
||||||
|
|
||||||
if TARGET_OS == "windows":
|
|
||||||
build_windows(package_info)
|
|
||||||
elif TARGET_OS == "macos":
|
|
||||||
build_mac(package_info)
|
|
||||||
|
|
||||||
def calculate_sha256(file):
|
|
||||||
with open(file, "rb") as f:
|
|
||||||
b = f.read() # read entire file as bytes
|
|
||||||
readable_hash = hashlib.sha256(b).hexdigest()
|
|
||||||
return readable_hash
|
|
||||||
|
|
||||||
def build_windows(package_info):
|
|
||||||
print("Starting packaging process for Windows...")
|
|
||||||
|
|
||||||
# Check Inno Setup
|
|
||||||
try:
|
|
||||||
subprocess.run(["iscc"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
||||||
except FileNotFoundError:
|
|
||||||
raise Exception("Could not find Inno Setup compiler. Please install it from here: http://www.jrsoftware.org/isdl.php")
|
|
||||||
|
|
||||||
print("Clearing target dirs")
|
|
||||||
|
|
||||||
# Clearing previous build directory
|
|
||||||
if os.path.isdir(PACKAGER_TARGET_DIR):
|
|
||||||
print("Cleaning packager temp directory...")
|
|
||||||
shutil.rmtree(PACKAGER_TARGET_DIR)
|
|
||||||
|
|
||||||
TARGET_DIR = os.path.join(PACKAGER_TARGET_DIR, "win")
|
|
||||||
os.makedirs(TARGET_DIR, exist_ok=True)
|
|
||||||
|
|
||||||
modulo_url = "https://github.com/federico-terzi/modulo/releases/download/v{0}/modulo-win.exe".format(package_info.modulo_version)
|
|
||||||
modulo_sha_url = "https://github.com/federico-terzi/modulo/releases/download/v{0}/modulo-win.exe.sha256.txt".format(package_info.modulo_version)
|
|
||||||
print("Pulling modulo depencency from:", modulo_url)
|
|
||||||
modulo_target_file = os.path.join(TARGET_DIR, "modulo.exe")
|
|
||||||
urllib.request.urlretrieve(modulo_url, modulo_target_file)
|
|
||||||
print("Pulling SHA signature from:", modulo_sha_url)
|
|
||||||
modulo_sha_file = os.path.join(TARGET_DIR, "modulo.sha256")
|
|
||||||
urllib.request.urlretrieve(modulo_sha_url, modulo_sha_file)
|
|
||||||
print("Checking signatures...")
|
|
||||||
expected_sha = None
|
|
||||||
with open(modulo_sha_file, "r") as sha_f:
|
|
||||||
expected_sha = sha_f.read()
|
|
||||||
actual_sha = calculate_sha256(modulo_target_file)
|
|
||||||
if actual_sha != expected_sha:
|
|
||||||
raise Exception("Modulo SHA256 is not matching")
|
|
||||||
|
|
||||||
print("Gathering CRT DLLs...")
|
|
||||||
msvc_dirs = glob.glob("C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\*\\VC\\Redist\\MSVC\\*")
|
|
||||||
print("Found Redists: ", msvc_dirs)
|
|
||||||
|
|
||||||
print("Determining best redist...")
|
|
||||||
|
|
||||||
if len(msvc_dirs) == 0:
|
|
||||||
raise Exception("Cannot find redistributable dlls")
|
|
||||||
|
|
||||||
msvc_dir = None
|
|
||||||
|
|
||||||
for curr_dir in msvc_dirs:
|
|
||||||
dll_files = glob.glob(curr_dir + "\\x64\\*CRT\\*.dll")
|
|
||||||
print("Found dlls", dll_files, "in", curr_dir)
|
|
||||||
if any("vcruntime140_1.dll" in x.lower() for x in dll_files):
|
|
||||||
msvc_dir = curr_dir
|
|
||||||
break
|
|
||||||
|
|
||||||
if msvc_dir is None:
|
|
||||||
raise Exception("Cannot find redist with VCRUNTIME140_1.dll")
|
|
||||||
|
|
||||||
print("Using: ", msvc_dir)
|
|
||||||
|
|
||||||
dll_files = glob.glob(msvc_dir + "\\x64\\*CRT\\*.dll")
|
|
||||||
|
|
||||||
print("Found DLLs:")
|
|
||||||
include_list = []
|
|
||||||
for dll in dll_files:
|
|
||||||
print("Including: "+dll)
|
|
||||||
include_list.append("Source: \""+dll+"\"; DestDir: \"{app}\"; Flags: ignoreversion")
|
|
||||||
|
|
||||||
print("Including modulo")
|
|
||||||
include_list.append("Source: \""+os.path.abspath(modulo_target_file)+"\"; DestDir: \"{app}\"; Flags: ignoreversion")
|
|
||||||
|
|
||||||
include = "\r\n".join(include_list)
|
|
||||||
|
|
||||||
INSTALLER_NAME = f"espanso-win-installer"
|
|
||||||
|
|
||||||
# Inno setup
|
|
||||||
shutil.copy("packager/win/modpath.iss", os.path.join(TARGET_DIR, "modpath.iss"))
|
|
||||||
|
|
||||||
print("Processing inno setup template")
|
|
||||||
with open("packager/win/setupscript.iss", "r") as iss_script:
|
|
||||||
content = iss_script.read()
|
|
||||||
|
|
||||||
# Replace variables
|
|
||||||
content = content.replace("{{{app_name}}}", package_info.name)
|
|
||||||
content = content.replace("{{{app_version}}}", package_info.version)
|
|
||||||
content = content.replace("{{{app_publisher}}}", package_info.publisher)
|
|
||||||
content = content.replace("{{{app_url}}}", package_info.url)
|
|
||||||
content = content.replace("{{{app_license}}}", os.path.abspath("LICENSE"))
|
|
||||||
content = content.replace("{{{app_icon}}}", os.path.abspath("packager/win/icon.ico"))
|
|
||||||
content = content.replace("{{{executable_path}}}", os.path.abspath("target/release/espanso.exe"))
|
|
||||||
content = content.replace("{{{output_dir}}}", os.path.abspath(TARGET_DIR))
|
|
||||||
content = content.replace("{{{output_name}}}", INSTALLER_NAME)
|
|
||||||
content = content.replace("{{{dll_include}}}", include)
|
|
||||||
|
|
||||||
with open(os.path.join(TARGET_DIR, "setupscript.iss"), "w") as output_script:
|
|
||||||
output_script.write(content)
|
|
||||||
|
|
||||||
print("Compiling installer with Inno setup")
|
|
||||||
subprocess.run(["iscc", os.path.abspath(os.path.join(TARGET_DIR, "setupscript.iss"))])
|
|
||||||
|
|
||||||
print("Calculating the SHA256")
|
|
||||||
sha256_hash = hashlib.sha256()
|
|
||||||
with open(os.path.abspath(os.path.join(TARGET_DIR, INSTALLER_NAME+".exe")),"rb") as f:
|
|
||||||
# Read and update hash string value in blocks of 4K
|
|
||||||
for byte_block in iter(lambda: f.read(4096),b""):
|
|
||||||
sha256_hash.update(byte_block)
|
|
||||||
|
|
||||||
hash_file = os.path.abspath(os.path.join(TARGET_DIR, "espanso-win-installer-sha256.txt"))
|
|
||||||
with open(hash_file, "w") as hf:
|
|
||||||
hf.write(sha256_hash.hexdigest())
|
|
||||||
|
|
||||||
|
|
||||||
def build_mac(package_info):
|
|
||||||
print("Starting packaging process for MacOS...")
|
|
||||||
|
|
||||||
print("Clearing target dirs")
|
|
||||||
|
|
||||||
# Clearing previous build directory
|
|
||||||
if os.path.isdir(PACKAGER_TARGET_DIR):
|
|
||||||
print("Cleaning packager temp directory...")
|
|
||||||
shutil.rmtree(PACKAGER_TARGET_DIR)
|
|
||||||
|
|
||||||
TARGET_DIR = os.path.join(PACKAGER_TARGET_DIR, "mac")
|
|
||||||
os.makedirs(TARGET_DIR, exist_ok=True)
|
|
||||||
|
|
||||||
print("Compressing release to archive...")
|
|
||||||
target_name = f"espanso-mac.tar.gz"
|
|
||||||
archive_target = os.path.abspath(os.path.join(TARGET_DIR, target_name))
|
|
||||||
subprocess.run(["tar",
|
|
||||||
"-C", os.path.abspath("target/release"),
|
|
||||||
"-cvf",
|
|
||||||
archive_target,
|
|
||||||
"espanso",
|
|
||||||
])
|
|
||||||
print(f"Created archive: {archive_target}")
|
|
||||||
|
|
||||||
print("Calculating the SHA256")
|
|
||||||
sha256_hash = hashlib.sha256()
|
|
||||||
with open(archive_target,"rb") as f:
|
|
||||||
# Read and update hash string value in blocks of 4K
|
|
||||||
for byte_block in iter(lambda: f.read(4096),b""):
|
|
||||||
sha256_hash.update(byte_block)
|
|
||||||
|
|
||||||
hash_file = os.path.abspath(os.path.join(TARGET_DIR, "espanso-mac-sha256.txt"))
|
|
||||||
with open(hash_file, "w") as hf:
|
|
||||||
hf.write(sha256_hash.hexdigest())
|
|
||||||
|
|
||||||
modulo_sha_url = "https://github.com/federico-terzi/modulo/releases/download/v{0}/modulo-mac.sha256.txt".format(package_info.modulo_version)
|
|
||||||
print("Pulling SHA signature from:", modulo_sha_url)
|
|
||||||
modulo_sha_file = os.path.join(TARGET_DIR, "modulo.sha256")
|
|
||||||
urllib.request.urlretrieve(modulo_sha_url, modulo_sha_file)
|
|
||||||
modulo_sha = None
|
|
||||||
with open(modulo_sha_file, "r") as sha_f:
|
|
||||||
modulo_sha = sha_f.read()
|
|
||||||
if modulo_sha is None:
|
|
||||||
raise Exception("Cannot determine modulo SHA")
|
|
||||||
|
|
||||||
print("Processing Homebrew formula template")
|
|
||||||
with open("packager/mac/espanso.rb", "r") as formula_template:
|
|
||||||
content = formula_template.read()
|
|
||||||
|
|
||||||
# Replace variables
|
|
||||||
content = content.replace("{{{app_desc}}}", package_info.description)
|
|
||||||
content = content.replace("{{{app_url}}}", package_info.url)
|
|
||||||
content = content.replace("{{{app_version}}}", package_info.version)
|
|
||||||
content = content.replace("{{{modulo_version}}}", package_info.modulo_version)
|
|
||||||
content = content.replace("{{{modulo_sha}}}", modulo_sha)
|
|
||||||
|
|
||||||
# Calculate hash
|
|
||||||
with open(archive_target, "rb") as f:
|
|
||||||
bytes = f.read()
|
|
||||||
readable_hash = hashlib.sha256(bytes).hexdigest()
|
|
||||||
content = content.replace("{{{release_hash}}}", readable_hash)
|
|
||||||
|
|
||||||
with open(os.path.join(TARGET_DIR, "espanso.rb"), "w") as output_script:
|
|
||||||
output_script.write(content)
|
|
||||||
|
|
||||||
print("Done!")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
print("[[ espanso packager ]]")
|
|
||||||
|
|
||||||
# Check python version 3
|
|
||||||
if sys.version_info[0] < 3:
|
|
||||||
raise Exception("Must be using Python 3")
|
|
||||||
|
|
||||||
cli()
|
|
|
@ -8,7 +8,7 @@ BASE_DIR=$(pwd)
|
||||||
|
|
||||||
mkdir -p $TOOL_DIR
|
mkdir -p $TOOL_DIR
|
||||||
|
|
||||||
if ls $TOOL_DIR/*.AppImage 1> /dev/null 2>&1; then
|
if ls $TOOL_DIR/linuxdeploy*.AppImage 1> /dev/null 2>&1; then
|
||||||
echo "Skipping download of linuxdeploy"
|
echo "Skipping download of linuxdeploy"
|
||||||
else
|
else
|
||||||
echo "Downloading linuxdeploy tool"
|
echo "Downloading linuxdeploy tool"
|
||||||
|
@ -16,12 +16,36 @@ else
|
||||||
chmod +x $TOOL_DIR/linuxdeploy*.AppImage
|
chmod +x $TOOL_DIR/linuxdeploy*.AppImage
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if ls $TOOL_DIR/appimagetool*.AppImage 1> /dev/null 2>&1; then
|
||||||
|
echo "Skipping download of appimagetool"
|
||||||
|
else
|
||||||
|
echo "Downloading appimagetool"
|
||||||
|
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -P "$TOOL_DIR"
|
||||||
|
chmod +x $TOOL_DIR/appimagetool*.AppImage
|
||||||
|
fi
|
||||||
|
|
||||||
rm -Rf "$TARGET_DIR"
|
rm -Rf "$TARGET_DIR"
|
||||||
mkdir -p $OUTPUT_DIR
|
mkdir -p $OUTPUT_DIR
|
||||||
mkdir -p $BUILD_DIR
|
mkdir -p $BUILD_DIR
|
||||||
|
|
||||||
echo Building AppImage into $OUTPUT_DIR
|
echo Building AppImage into $OUTPUT_DIR
|
||||||
pushd $OUTPUT_DIR
|
pushd $OUTPUT_DIR
|
||||||
$TOOL_DIR/linuxdeploy*.AppImage --appimage-extract-and-run -e "$BASE_DIR/$EXEC_PATH" -d "$BASE_DIR/espanso/src/res/linux/espanso.desktop" -i "$BASE_DIR/espanso/src/res/linux/icon.png" --appdir $BUILD_DIR --output appimage
|
$TOOL_DIR/linuxdeploy*.AppImage --appimage-extract-and-run -e "$BASE_DIR/$EXEC_PATH" \
|
||||||
|
-d "$BASE_DIR/espanso/src/res/linux/espanso.desktop" \
|
||||||
|
-i "$BASE_DIR/espanso/src/res/linux/icon.png" \
|
||||||
|
--appdir $BUILD_DIR \
|
||||||
|
--output appimage
|
||||||
chmod +x ./Espanso*.AppImage
|
chmod +x ./Espanso*.AppImage
|
||||||
|
|
||||||
|
# Apply a workaround to fix this issue: https://github.com/federico-terzi/espanso/issues/900
|
||||||
|
# See: https://github.com/project-slippi/Ishiiruka/issues/323#issuecomment-977415376
|
||||||
|
|
||||||
|
echo "Applying patch for libgmodule"
|
||||||
|
|
||||||
|
./Espanso*.AppImage --appimage-extract
|
||||||
|
rm -Rf ./Espanso*.AppImage
|
||||||
|
rm -Rf squashfs-root/usr/lib/libgmodule*
|
||||||
|
$TOOL_DIR/appimagetool*.AppImage --appimage-extract-and-run -v squashfs-root
|
||||||
|
rm -Rf squashfs-root
|
||||||
|
|
||||||
popd
|
popd
|
12
snap/hooks/remove
Executable file
12
snap/hooks/remove
Executable file
|
@ -0,0 +1,12 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
echo "Stopping Espanso..."
|
||||||
|
killall espanso
|
||||||
|
|
||||||
|
# Here I've also tried to unregister the Systemd service, but couldn't manage to.
|
||||||
|
# The remove hook is run as Root, but the systemd service is registered as a user.
|
||||||
|
# I couldn't find any (working) way to unregister a user systemd service as root.
|
||||||
|
# I've also tried these solutions: https://unix.stackexchange.com/questions/552922/stop-systemd-user-services-as-root-user
|
||||||
|
# but none seemed to work in this case.
|
||||||
|
# If you manage to find a way to do so, feel free to open an issue so that
|
||||||
|
# we can improve the hook! Thanks
|
|
@ -1,5 +1,5 @@
|
||||||
name: espanso
|
name: espanso
|
||||||
version: 2.1.1-alpha
|
version: 2.1.2-alpha
|
||||||
summary: A Cross-platform Text Expander written in Rust
|
summary: A Cross-platform Text Expander written in Rust
|
||||||
description: |
|
description: |
|
||||||
espanso is a Cross-platform, Text Expander written in Rust.
|
espanso is a Cross-platform, Text Expander written in Rust.
|
||||||
|
|
Loading…
Reference in New Issue
Block a user