From bf1f3fc2e0e384ef01370cf5eaf60d9d1f88f43d Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 13 Nov 2021 16:01:26 +0100 Subject: [PATCH 01/19] chore(misc): version bump --- Cargo.lock | 2 +- espanso/Cargo.toml | 2 +- snapcraft.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 244170a..0ba00ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -572,7 +572,7 @@ dependencies = [ [[package]] name = "espanso" -version = "2.1.0-alpha" +version = "2.1.1-alpha" dependencies = [ "anyhow", "caps", diff --git a/espanso/Cargo.toml b/espanso/Cargo.toml index 7248484..4716d8c 100644 --- a/espanso/Cargo.toml +++ b/espanso/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "espanso" -version = "2.1.0-alpha" +version = "2.1.1-alpha" authors = ["Federico Terzi "] license = "GPL-3.0" description = "Cross-platform Text Expander written in Rust" diff --git a/snapcraft.yaml b/snapcraft.yaml index ad623ef..22f5230 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -1,5 +1,5 @@ name: espanso -version: 2.1.0-alpha +version: 2.1.1-alpha summary: A Cross-platform Text Expander written in Rust description: | espanso is a Cross-platform, Text Expander written in Rust. From 411118b550957ce8134e720ce329fd1cdbbcd344 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 14 Nov 2021 11:17:54 +0100 Subject: [PATCH 02/19] fix(core): fix string clipping operator that crashed with some unicode chars --- .../cli/worker/engine/process/middleware/match_select.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/espanso/src/cli/worker/engine/process/middleware/match_select.rs b/espanso/src/cli/worker/engine/process/middleware/match_select.rs index b318bc2..0f0b74e 100644 --- a/espanso/src/cli/worker/engine/process/middleware/match_select.rs +++ b/espanso/src/cli/worker/engine/process/middleware/match_select.rs @@ -55,11 +55,15 @@ impl<'a> MatchSelector for MatchSelectorAdapter<'a> { let search_items: Vec = matches .into_iter() .map(|m| { - let clipped_label = &m.label[..std::cmp::min(m.label.len(), MAX_LABEL_LEN)]; + let clipped_label: String = m + .label + .chars() + .take(std::cmp::min(m.label.len(), MAX_LABEL_LEN)) + .collect(); SearchItem { id: m.id.to_string(), - label: clipped_label.to_string(), + label: clipped_label, tag: m.tag.map(String::from), is_builtin: m.is_builtin, } From 6168a2829114419a1b8997351970d39ff173ff4b Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Mon, 15 Nov 2021 20:39:10 +0100 Subject: [PATCH 03/19] fix(modulo): fix wizard pages being cut with display scaling on Windows. Fix #871 --- espanso-modulo/src/sys/wizard/wizard.fbp | 2002 ++++++++++-------- espanso-modulo/src/sys/wizard/wizard_gui.cpp | 103 +- espanso-modulo/src/sys/wizard/wizard_gui.h | 10 +- 3 files changed, 1205 insertions(+), 910 deletions(-) diff --git a/espanso-modulo/src/sys/wizard/wizard.fbp b/espanso-modulo/src/sys/wizard/wizard.fbp index 30500df..ea78a06 100644 --- a/espanso-modulo/src/sys/wizard/wizard.fbp +++ b/espanso-modulo/src/sys/wizard/wizard.fbp @@ -45,8 +45,8 @@ WizardFrame - 550,577 - wxCAPTION|wxCLOSE_BOX|wxSYSTEM_MENU + 600,577 + wxCAPTION|wxCLOSE_BOX|wxRESIZE_BORDER|wxSYSTEM_MENU ; ; forward_declare Espanso @@ -180,268 +180,333 @@ wxTAB_TRAVERSAL - bSizer2 + bSizer13 wxVERTICAL none - - 0 - wxALIGN_CENTER|wxALL - 0 - - 1 - 1 - 1 - 1 - - - - - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 1 - - 1 - - 0 - 0 - wxID_ANY - - 0 - - - 0 - 256,256 - 1 - welcome_image - 1 - - - protected - 1 - - Resizable - 1 - 256,256 - ; ; forward_declare - 0 - - - - - - - - 20 - wxALIGN_CENTER_HORIZONTAL|wxTOP - 0 - - 1 - 1 - 1 - 1 - - - - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 1 - - 1 - ,90,92,18,70,0 - 0 - 0 - wxID_ANY - Welcome to Espanso! - 0 - - 0 - - - 0 - - 1 - welcome_title_text - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - 5 - wxALIGN_CENTER_HORIZONTAL|wxALL - 0 - - 1 - 1 - 1 - 1 - - - - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 1 - - 1 - - 0 - 0 - wxID_ANY - (version 1.2.3) - 0 - - 0 - - - 0 - - 1 - welcome_version_text - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - - 0 - - 20 - protected - 0 - - - - 10 - wxALL - 0 - - 1 - 1 - 1 - 1 - - - - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 1 - - 1 - - 0 - 0 - wxID_ANY - This wizard will help you to quickly get started with espanso. Click "Start" when you are ready - 0 - - 0 - - - 0 - - 1 - welcome_description_text - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - - - 5 - wxEXPAND + wxEXPAND | wxALL 1 - - 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_scrolledWindow2 + 1 + + protected - 0 + 1 + + Resizable + 5 + 5 + 1 + + ; ; forward_declare + 0 + + + + wxHSCROLL|wxVSCROLL + + + bSizer2 + wxVERTICAL + none + + 0 + wxALIGN_CENTER|wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + 256,256 + 1 + welcome_image + 1 + + + protected + 1 + + Resizable + 1 + 256,256 + ; ; forward_declare + 0 + + + + + + + + 20 + wxALIGN_CENTER_HORIZONTAL|wxTOP + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + ,90,92,18,70,0 + 0 + 0 + wxID_ANY + Welcome to Espanso! + 0 + + 0 + + + 0 + + 1 + welcome_title_text + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxALIGN_CENTER_HORIZONTAL|wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + (version 1.2.3) + 0 + + 0 + + + 0 + + 1 + welcome_version_text + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + + 0 + + 20 + protected + 0 + + + + 10 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + This wizard will help you to quickly get started with espanso. Click "Start" when you are ready + 0 + + 0 + + + 0 + + 1 + welcome_description_text + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + wxEXPAND + 1 + + 0 + protected + 0 + + + @@ -1593,7 +1658,7 @@ wxTAB_TRAVERSAL - bSizer211 + bSizer16 wxVERTICAL none @@ -1659,19 +1724,9 @@ 5 - - 0 - - 20 - protected - 0 - - - - 10 - wxLEFT|wxRIGHT|wxTOP + wxEXPAND | wxALL 1 - + 1 1 1 @@ -1699,8 +1754,6 @@ 0 0 wxID_ANY - The new version uses a slightly different configuration format that powers some exciting new features. To ease the transition, espanso offers two possible choices: - Automatically backup the old configuration in the Documents folder and migrate to the new format (recommended). - Use compatibility mode without changing the configs. Keep in mind that: - Compatibility mode does not support all new espanso features - You can always migrate the configs later For more information, see: - 0 0 @@ -1708,7 +1761,7 @@ 0 1 - migrate_description + m_scrolledWindow4 1 @@ -1716,95 +1769,162 @@ 1 Resizable + 5 + 5 1 - ; ; forward_declare 0 - - 500 + wxVSCROLL + + + bSizer211 + wxVERTICAL + none + + 5 + + 0 + + 20 + protected + 0 + + + + 10 + wxLEFT|wxRIGHT|wxTOP + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + The new version uses a slightly different configuration format that powers some exciting new features. To ease the transition, espanso offers two possible choices: - Automatically backup the old configuration in the Documents folder and migrate to the new format (recommended). - Use compatibility mode without changing the configs. Keep in mind that: - Compatibility mode does not support all new espanso features - You can always migrate the configs later For more information, see: + 0 + + 0 + + + 0 + + 1 + migrate_description + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + 500 + + + + 10 + wxLEFT|wxRIGHT + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + + wxID_ANY + https://espanso.org/migration + + 0 + + + 0 + + 1 + migrate_link + + 1 + + + protected + 1 + + Resizable + 1 + + wxHL_DEFAULT_STYLE + ; ; forward_declare + 0 + + https://espanso.org/migration + + + + + + + - 10 - wxLEFT|wxRIGHT + 5 + wxEXPAND 0 - - 1 - 1 - 1 - 1 - - - - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 1 - - 1 - - 0 - 0 - - wxID_ANY - https://espanso.org/migration - - 0 - - - 0 - - 1 - migrate_link - - 1 - - - protected - 1 - - Resizable - 1 - - wxHL_DEFAULT_STYLE - ; ; forward_declare - 0 - - https://espanso.org/migration - - - - - - - - 5 - wxEXPAND - 10 - - 0 - protected - 0 - - - - 5 - wxEXPAND - 1 -1,-1 bSizer8 @@ -2027,274 +2147,339 @@ wxTAB_TRAVERSAL - bSizer2122 + bSizer18 wxVERTICAL none - - 20 - wxALIGN_CENTER_HORIZONTAL|wxALIGN_LEFT|wxTOP - 0 - - 1 - 1 - 1 - 1 - - - - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 1 - - 1 - ,90,92,18,70,0 - 0 - 0 - wxID_ANY - Launch on System startup - 0 - - 0 - - - 0 - - 1 - auto_start_title - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - 5 - - 0 - - 20 - protected - 0 - - - - 10 - wxLEFT|wxRIGHT|wxTOP - 0 - - 1 - 1 - 1 - 1 - - - - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 1 - - 1 - - 0 - 0 - wxID_ANY - Espanso can be launched automatically when you start your PC. Do you want to proceed? - 0 - - 0 - - - 0 - - 1 - auto_start_description - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - 500 - - - - 20 - wxALL - 0 - - 1 - 1 - 1 - 1 - - - - - - - - 1 - 0 - 1 - 1 - - 1 - 0 - Dock - 0 - Left - 1 - - 1 - - 0 - 0 - wxID_ANY - Yes, launch Espanso on system startup (recommended) - - 0 - - - 0 - - 1 - auto_start_checkbox - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 10 - wxALL - 0 - - 1 - 1 - 1 - 1 - - - - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 1 - - 1 - - 0 - 0 - wxID_ANY - Note: you can always disable this option later. - 0 - - 0 - - - 0 - - 1 - auto_start_note - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - 500 - - - - 5 - wxEXPAND + wxEXPAND | wxALL 1 - - 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_scrolledWindow6 + 1 + + protected - 0 + 1 + + Resizable + 5 + 5 + 1 + + ; ; forward_declare + 0 + + + + wxHSCROLL|wxVSCROLL + + + bSizer2122 + wxVERTICAL + none + + 20 + wxALIGN_CENTER_HORIZONTAL|wxALIGN_LEFT|wxTOP + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + ,90,92,18,70,0 + 0 + 0 + wxID_ANY + Launch on System startup + 0 + + 0 + + + 0 + + 1 + auto_start_title + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + + 0 + + 20 + protected + 0 + + + + 10 + wxLEFT|wxRIGHT|wxTOP + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + Espanso can be launched automatically when you start your PC. Do you want to proceed? + 0 + + 0 + + + 0 + + 1 + auto_start_description + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + 500 + + + + 20 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + Yes, launch Espanso on system startup (recommended) + + 0 + + + 0 + + 1 + auto_start_checkbox + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + 10 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + Note: you can always disable this option later. + 0 + + 0 + + + 0 + + 1 + auto_start_note + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + 500 + + + + 5 + wxEXPAND + 1 + + 0 + protected + 0 + + + @@ -2429,274 +2614,339 @@ wxTAB_TRAVERSAL - bSizer212 + bSizer20 wxVERTICAL none - - 20 - wxALIGN_CENTER_HORIZONTAL|wxALIGN_LEFT|wxTOP - 0 - - 1 - 1 - 1 - 1 - - - - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 1 - - 1 - ,90,92,18,70,0 - 0 - 0 - wxID_ANY - Add to PATH - 0 - - 0 - - - 0 - - 1 - add_path_title - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - -1 - - 5 - - 0 - - 20 - protected - 0 - - - - 10 - wxLEFT|wxRIGHT|wxTOP - 0 - - 1 - 1 - 1 - 1 - - - - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 1 - - 1 - - 0 - 0 - wxID_ANY - Espanso offers a rich CLI interface that enables some powerful features and comes handy when debugging configuration problems. To be easily accessed, espanso can be added to the PATH environment variable automatically. Do you want to proceed? - 0 - - 0 - - - 0 - - 1 - add_path_description - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - 500 - - - - 20 - wxALL - 0 - - 1 - 1 - 1 - 1 - - - - - - - - 1 - 0 - 1 - 1 - - 1 - 0 - Dock - 0 - Left - 1 - - 1 - - 0 - 0 - wxID_ANY - Yes, add espanso to PATH - - 0 - - - 0 - - 1 - add_path_checkbox - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - wxFILTER_NONE - wxDefaultValidator - - - - - - - - 10 - wxALL - 0 - - 1 - 1 - 1 - 1 - - - - - - - - 1 - 0 - 1 - - 1 - 0 - Dock - 0 - Left - 1 - - 1 - - 0 - 0 - wxID_ANY - Note: if you don't know what the PATH env variable is, you should probably keep this checked. - 0 - - 0 - - - 0 - - 1 - add_path_note - 1 - - - protected - 1 - - Resizable - 1 - - - ; ; forward_declare - 0 - - - - - 500 - - - - 5 - wxEXPAND + wxEXPAND | wxALL 1 - - 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + + 0 + + + 0 + + 1 + m_scrolledWindow8 + 1 + + protected - 0 + 1 + + Resizable + 5 + 5 + 1 + + ; ; forward_declare + 0 + + + + wxHSCROLL|wxVSCROLL + + + bSizer212 + wxVERTICAL + none + + 20 + wxALIGN_CENTER_HORIZONTAL|wxALIGN_LEFT|wxTOP + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + ,90,92,18,70,0 + 0 + 0 + wxID_ANY + Add to PATH + 0 + + 0 + + + 0 + + 1 + add_path_title + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + -1 + + + + 5 + + 0 + + 20 + protected + 0 + + + + 10 + wxLEFT|wxRIGHT|wxTOP + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + Espanso offers a rich CLI interface that enables some powerful features and comes handy when debugging configuration problems. To be easily accessed, espanso can be added to the PATH environment variable automatically. Do you want to proceed? + 0 + + 0 + + + 0 + + 1 + add_path_description + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + 500 + + + + 20 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + Yes, add espanso to PATH + + 0 + + + 0 + + 1 + add_path_checkbox + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + 10 + wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + + 0 + 0 + wxID_ANY + Note: if you don't know what the PATH env variable is, you should probably keep this checked. + 0 + + 0 + + + 0 + + 1 + add_path_note + 1 + + + protected + 1 + + Resizable + 1 + + + ; ; forward_declare + 0 + + + + + 500 + + + + 5 + wxEXPAND + 1 + + 0 + protected + 0 + + + diff --git a/espanso-modulo/src/sys/wizard/wizard_gui.cpp b/espanso-modulo/src/sys/wizard/wizard_gui.cpp index b5bbe8e..f9744c5 100644 --- a/espanso-modulo/src/sys/wizard/wizard_gui.cpp +++ b/espanso-modulo/src/sys/wizard/wizard_gui.cpp @@ -26,43 +26,54 @@ WizardFrame::WizardFrame( wxWindow* parent, wxWindowID id, const wxString& title welcome_panel = new wxPanel( m_simplebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); welcome_panel->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + wxBoxSizer* bSizer13; + bSizer13 = new wxBoxSizer( wxVERTICAL ); + + m_scrolledWindow2 = new wxScrolledWindow( welcome_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL|wxVSCROLL ); + m_scrolledWindow2->SetScrollRate( 5, 5 ); wxBoxSizer* bSizer2; bSizer2 = new wxBoxSizer( wxVERTICAL ); - welcome_image = new wxStaticBitmap( welcome_panel, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( 256,256 ), 0 ); + welcome_image = new wxStaticBitmap( m_scrolledWindow2, wxID_ANY, wxNullBitmap, wxDefaultPosition, wxSize( 256,256 ), 0 ); welcome_image->SetMinSize( wxSize( 256,256 ) ); bSizer2->Add( welcome_image, 0, wxALIGN_CENTER|wxALL, 0 ); - welcome_title_text = new wxStaticText( welcome_panel, wxID_ANY, wxT("Welcome to Espanso!"), wxDefaultPosition, wxDefaultSize, 0 ); + welcome_title_text = new wxStaticText( m_scrolledWindow2, wxID_ANY, wxT("Welcome to Espanso!"), wxDefaultPosition, wxDefaultSize, 0 ); welcome_title_text->Wrap( -1 ); welcome_title_text->SetFont( wxFont( 18, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); bSizer2->Add( welcome_title_text, 0, wxALIGN_CENTER_HORIZONTAL|wxTOP, 20 ); - welcome_version_text = new wxStaticText( welcome_panel, wxID_ANY, wxT("(version 1.2.3)"), wxDefaultPosition, wxDefaultSize, 0 ); + welcome_version_text = new wxStaticText( m_scrolledWindow2, wxID_ANY, wxT("(version 1.2.3)"), wxDefaultPosition, wxDefaultSize, 0 ); welcome_version_text->Wrap( -1 ); bSizer2->Add( welcome_version_text, 0, wxALIGN_CENTER_HORIZONTAL|wxALL, 5 ); bSizer2->Add( 0, 20, 0, 0, 5 ); - welcome_description_text = new wxStaticText( welcome_panel, wxID_ANY, wxT("This wizard will help you to quickly get started with espanso. \n\nClick \"Start\" when you are ready"), wxDefaultPosition, wxDefaultSize, 0 ); + welcome_description_text = new wxStaticText( m_scrolledWindow2, wxID_ANY, wxT("This wizard will help you to quickly get started with espanso. \n\nClick \"Start\" when you are ready"), wxDefaultPosition, wxDefaultSize, 0 ); welcome_description_text->Wrap( -1 ); bSizer2->Add( welcome_description_text, 0, wxALL, 10 ); bSizer2->Add( 0, 0, 1, wxEXPAND, 5 ); + + m_scrolledWindow2->SetSizer( bSizer2 ); + m_scrolledWindow2->Layout(); + bSizer2->Fit( m_scrolledWindow2 ); + bSizer13->Add( m_scrolledWindow2, 1, wxEXPAND | wxALL, 5 ); + welcome_start_button = new wxButton( welcome_panel, wxID_ANY, wxT("Start"), wxDefaultPosition, wxDefaultSize, 0 ); welcome_start_button->SetDefault(); - bSizer2->Add( welcome_start_button, 0, wxALIGN_RIGHT|wxALL, 10 ); + bSizer13->Add( welcome_start_button, 0, wxALIGN_RIGHT|wxALL, 10 ); - welcome_panel->SetSizer( bSizer2 ); + welcome_panel->SetSizer( bSizer13 ); welcome_panel->Layout(); - bSizer2->Fit( welcome_panel ); + bSizer13->Fit( welcome_panel ); m_simplebook->AddPage( welcome_panel, wxT("a page"), false ); move_bundle_panel = new wxPanel( m_simplebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); move_bundle_panel->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); @@ -175,27 +186,35 @@ WizardFrame::WizardFrame( wxWindow* parent, wxWindowID id, const wxString& title migrate_panel = new wxPanel( m_simplebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); migrate_panel->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); - wxBoxSizer* bSizer211; - bSizer211 = new wxBoxSizer( wxVERTICAL ); + wxBoxSizer* bSizer16; + bSizer16 = new wxBoxSizer( wxVERTICAL ); migrate_title = new wxStaticText( migrate_panel, wxID_ANY, wxT("Migrate configuration"), wxDefaultPosition, wxDefaultSize, 0 ); migrate_title->Wrap( -1 ); migrate_title->SetFont( wxFont( 18, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); - bSizer211->Add( migrate_title, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_LEFT|wxTOP, 20 ); + bSizer16->Add( migrate_title, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_LEFT|wxTOP, 20 ); + + m_scrolledWindow4 = new wxScrolledWindow( migrate_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL ); + m_scrolledWindow4->SetScrollRate( 5, 5 ); + wxBoxSizer* bSizer211; + bSizer211 = new wxBoxSizer( wxVERTICAL ); bSizer211->Add( 0, 20, 0, 0, 5 ); - migrate_description = new wxStaticText( migrate_panel, wxID_ANY, wxT("The new version uses a slightly different configuration format that powers some exciting new features.\n\nTo ease the transition, espanso offers two possible choices: \n\n - Automatically backup the old configuration in the Documents folder and migrate to the new format (recommended). \n - Use compatibility mode without changing the configs. \n\nKeep in mind that: \n\n - Compatibility mode does not support all new espanso features \n - You can always migrate the configs later \n\nFor more information, see: "), wxDefaultPosition, wxDefaultSize, 0 ); + migrate_description = new wxStaticText( m_scrolledWindow4, wxID_ANY, wxT("The new version uses a slightly different configuration format that powers some exciting new features.\n\nTo ease the transition, espanso offers two possible choices: \n\n - Automatically backup the old configuration in the Documents folder and migrate to the new format (recommended). \n - Use compatibility mode without changing the configs. \n\nKeep in mind that: \n\n - Compatibility mode does not support all new espanso features \n - You can always migrate the configs later \n\nFor more information, see: "), wxDefaultPosition, wxDefaultSize, 0 ); migrate_description->Wrap( 500 ); - bSizer211->Add( migrate_description, 1, wxLEFT|wxRIGHT|wxTOP, 10 ); + bSizer211->Add( migrate_description, 0, wxLEFT|wxRIGHT|wxTOP, 10 ); - migrate_link = new wxHyperlinkCtrl( migrate_panel, wxID_ANY, wxT("https://espanso.org/migration"), wxT("https://espanso.org/migration"), wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE ); + migrate_link = new wxHyperlinkCtrl( m_scrolledWindow4, wxID_ANY, wxT("https://espanso.org/migration"), wxT("https://espanso.org/migration"), wxDefaultPosition, wxDefaultSize, wxHL_DEFAULT_STYLE ); bSizer211->Add( migrate_link, 0, wxLEFT|wxRIGHT, 10 ); - bSizer211->Add( 0, 0, 10, wxEXPAND, 5 ); + m_scrolledWindow4->SetSizer( bSizer211 ); + m_scrolledWindow4->Layout(); + bSizer211->Fit( m_scrolledWindow4 ); + bSizer16->Add( m_scrolledWindow4, 1, wxEXPAND | wxALL, 5 ); wxBoxSizer* bSizer8; bSizer8 = new wxBoxSizer( wxHORIZONTAL ); @@ -212,20 +231,25 @@ WizardFrame::WizardFrame( wxWindow* parent, wxWindowID id, const wxString& title bSizer8->Add( migrate_backup_and_migrate_button, 0, wxALL, 10 ); - bSizer211->Add( bSizer8, 1, wxEXPAND, 5 ); + bSizer16->Add( bSizer8, 0, wxEXPAND, 5 ); - migrate_panel->SetSizer( bSizer211 ); + migrate_panel->SetSizer( bSizer16 ); migrate_panel->Layout(); - bSizer211->Fit( migrate_panel ); + bSizer16->Fit( migrate_panel ); m_simplebook->AddPage( migrate_panel, wxT("a page"), false ); auto_start_panel = new wxPanel( m_simplebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); auto_start_panel->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + wxBoxSizer* bSizer18; + bSizer18 = new wxBoxSizer( wxVERTICAL ); + + m_scrolledWindow6 = new wxScrolledWindow( auto_start_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL|wxVSCROLL ); + m_scrolledWindow6->SetScrollRate( 5, 5 ); wxBoxSizer* bSizer2122; bSizer2122 = new wxBoxSizer( wxVERTICAL ); - auto_start_title = new wxStaticText( auto_start_panel, wxID_ANY, wxT("Launch on System startup"), wxDefaultPosition, wxDefaultSize, 0 ); + auto_start_title = new wxStaticText( m_scrolledWindow6, wxID_ANY, wxT("Launch on System startup"), wxDefaultPosition, wxDefaultSize, 0 ); auto_start_title->Wrap( -1 ); auto_start_title->SetFont( wxFont( 18, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); @@ -234,38 +258,49 @@ WizardFrame::WizardFrame( wxWindow* parent, wxWindowID id, const wxString& title bSizer2122->Add( 0, 20, 0, 0, 5 ); - auto_start_description = new wxStaticText( auto_start_panel, wxID_ANY, wxT("Espanso can be launched automatically when you start your PC. \n\nDo you want to proceed?"), wxDefaultPosition, wxDefaultSize, 0 ); + auto_start_description = new wxStaticText( m_scrolledWindow6, wxID_ANY, wxT("Espanso can be launched automatically when you start your PC. \n\nDo you want to proceed?"), wxDefaultPosition, wxDefaultSize, 0 ); auto_start_description->Wrap( 500 ); bSizer2122->Add( auto_start_description, 0, wxLEFT|wxRIGHT|wxTOP, 10 ); - auto_start_checkbox = new wxCheckBox( auto_start_panel, wxID_ANY, wxT("Yes, launch Espanso on system startup (recommended)"), wxDefaultPosition, wxDefaultSize, 0 ); + auto_start_checkbox = new wxCheckBox( m_scrolledWindow6, wxID_ANY, wxT("Yes, launch Espanso on system startup (recommended)"), wxDefaultPosition, wxDefaultSize, 0 ); auto_start_checkbox->SetValue(true); bSizer2122->Add( auto_start_checkbox, 0, wxALL, 20 ); - auto_start_note = new wxStaticText( auto_start_panel, wxID_ANY, wxT("Note: you can always disable this option later."), wxDefaultPosition, wxDefaultSize, 0 ); + auto_start_note = new wxStaticText( m_scrolledWindow6, wxID_ANY, wxT("Note: you can always disable this option later."), wxDefaultPosition, wxDefaultSize, 0 ); auto_start_note->Wrap( 500 ); bSizer2122->Add( auto_start_note, 0, wxALL, 10 ); bSizer2122->Add( 0, 0, 1, wxEXPAND, 5 ); + + m_scrolledWindow6->SetSizer( bSizer2122 ); + m_scrolledWindow6->Layout(); + bSizer2122->Fit( m_scrolledWindow6 ); + bSizer18->Add( m_scrolledWindow6, 1, wxEXPAND | wxALL, 5 ); + auto_start_continue = new wxButton( auto_start_panel, wxID_ANY, wxT("Continue"), wxDefaultPosition, wxDefaultSize, 0 ); auto_start_continue->SetDefault(); - bSizer2122->Add( auto_start_continue, 0, wxALIGN_RIGHT|wxALL, 10 ); + bSizer18->Add( auto_start_continue, 0, wxALIGN_RIGHT|wxALL, 10 ); - auto_start_panel->SetSizer( bSizer2122 ); + auto_start_panel->SetSizer( bSizer18 ); auto_start_panel->Layout(); - bSizer2122->Fit( auto_start_panel ); + bSizer18->Fit( auto_start_panel ); m_simplebook->AddPage( auto_start_panel, wxT("a page"), false ); add_path_panel = new wxPanel( m_simplebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); add_path_panel->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + wxBoxSizer* bSizer20; + bSizer20 = new wxBoxSizer( wxVERTICAL ); + + m_scrolledWindow8 = new wxScrolledWindow( add_path_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL|wxVSCROLL ); + m_scrolledWindow8->SetScrollRate( 5, 5 ); wxBoxSizer* bSizer212; bSizer212 = new wxBoxSizer( wxVERTICAL ); - add_path_title = new wxStaticText( add_path_panel, wxID_ANY, wxT("Add to PATH"), wxDefaultPosition, wxDefaultSize, 0 ); + add_path_title = new wxStaticText( m_scrolledWindow8, wxID_ANY, wxT("Add to PATH"), wxDefaultPosition, wxDefaultSize, 0 ); add_path_title->Wrap( -1 ); add_path_title->SetFont( wxFont( 18, wxFONTFAMILY_DEFAULT, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD, false, wxEmptyString ) ); @@ -274,30 +309,36 @@ WizardFrame::WizardFrame( wxWindow* parent, wxWindowID id, const wxString& title bSizer212->Add( 0, 20, 0, 0, 5 ); - add_path_description = new wxStaticText( add_path_panel, wxID_ANY, wxT("Espanso offers a rich CLI interface that enables some powerful features and comes handy when debugging configuration problems.\n\nTo be easily accessed, espanso can be added to the PATH environment variable automatically. Do you want to proceed?\n"), wxDefaultPosition, wxDefaultSize, 0 ); + add_path_description = new wxStaticText( m_scrolledWindow8, wxID_ANY, wxT("Espanso offers a rich CLI interface that enables some powerful features and comes handy when debugging configuration problems.\n\nTo be easily accessed, espanso can be added to the PATH environment variable automatically. Do you want to proceed?\n"), wxDefaultPosition, wxDefaultSize, 0 ); add_path_description->Wrap( 500 ); bSizer212->Add( add_path_description, 0, wxLEFT|wxRIGHT|wxTOP, 10 ); - add_path_checkbox = new wxCheckBox( add_path_panel, wxID_ANY, wxT("Yes, add espanso to PATH"), wxDefaultPosition, wxDefaultSize, 0 ); + add_path_checkbox = new wxCheckBox( m_scrolledWindow8, wxID_ANY, wxT("Yes, add espanso to PATH"), wxDefaultPosition, wxDefaultSize, 0 ); add_path_checkbox->SetValue(true); bSizer212->Add( add_path_checkbox, 0, wxALL, 20 ); - add_path_note = new wxStaticText( add_path_panel, wxID_ANY, wxT("Note: if you don't know what the PATH env variable is, you should probably keep this checked."), wxDefaultPosition, wxDefaultSize, 0 ); + add_path_note = new wxStaticText( m_scrolledWindow8, wxID_ANY, wxT("Note: if you don't know what the PATH env variable is, you should probably keep this checked."), wxDefaultPosition, wxDefaultSize, 0 ); add_path_note->Wrap( 500 ); bSizer212->Add( add_path_note, 0, wxALL, 10 ); bSizer212->Add( 0, 0, 1, wxEXPAND, 5 ); + + m_scrolledWindow8->SetSizer( bSizer212 ); + m_scrolledWindow8->Layout(); + bSizer212->Fit( m_scrolledWindow8 ); + bSizer20->Add( m_scrolledWindow8, 1, wxEXPAND | wxALL, 5 ); + add_path_continue_button = new wxButton( add_path_panel, wxID_ANY, wxT("Continue"), wxDefaultPosition, wxDefaultSize, 0 ); add_path_continue_button->SetDefault(); - bSizer212->Add( add_path_continue_button, 0, wxALIGN_RIGHT|wxALL, 10 ); + bSizer20->Add( add_path_continue_button, 0, wxALIGN_RIGHT|wxALL, 10 ); - add_path_panel->SetSizer( bSizer212 ); + add_path_panel->SetSizer( bSizer20 ); add_path_panel->Layout(); - bSizer212->Fit( add_path_panel ); + bSizer20->Fit( add_path_panel ); m_simplebook->AddPage( add_path_panel, wxT("a page"), false ); accessibility_panel = new wxPanel( m_simplebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); accessibility_panel->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); diff --git a/espanso-modulo/src/sys/wizard/wizard_gui.h b/espanso-modulo/src/sys/wizard/wizard_gui.h index 1a9e24a..1ae7957 100644 --- a/espanso-modulo/src/sys/wizard/wizard_gui.h +++ b/espanso-modulo/src/sys/wizard/wizard_gui.h @@ -20,12 +20,12 @@ #include #include #include -#include #include +#include +#include #include #include #include -#include #include #include @@ -43,6 +43,7 @@ class WizardFrame : public wxFrame wxTimer check_timer; wxSimplebook* m_simplebook; wxPanel* welcome_panel; + wxScrolledWindow* m_scrolledWindow2; wxStaticBitmap* welcome_image; wxStaticText* welcome_title_text; wxStaticText* welcome_version_text; @@ -65,17 +66,20 @@ class WizardFrame : public wxFrame wxButton* wrong_edition_button; wxPanel* migrate_panel; wxStaticText* migrate_title; + wxScrolledWindow* m_scrolledWindow4; wxStaticText* migrate_description; wxHyperlinkCtrl* migrate_link; wxButton* migrate_compatibility_mode_button; wxButton* migrate_backup_and_migrate_button; wxPanel* auto_start_panel; + wxScrolledWindow* m_scrolledWindow6; wxStaticText* auto_start_title; wxStaticText* auto_start_description; wxCheckBox* auto_start_checkbox; wxStaticText* auto_start_note; wxButton* auto_start_continue; wxPanel* add_path_panel; + wxScrolledWindow* m_scrolledWindow8; wxStaticText* add_path_title; wxStaticText* add_path_description; wxCheckBox* add_path_checkbox; @@ -105,7 +109,7 @@ class WizardFrame : public wxFrame public: - WizardFrame( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = wxT("Espanso"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( 550,577 ), long style = wxCAPTION|wxCLOSE_BOX|wxSYSTEM_MENU|wxTAB_TRAVERSAL ); + WizardFrame( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = wxT("Espanso"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( 600,577 ), long style = wxCAPTION|wxCLOSE_BOX|wxRESIZE_BORDER|wxSYSTEM_MENU|wxTAB_TRAVERSAL ); ~WizardFrame(); From fff9f63f96919b8b716c272e2cfc982eb8b3215a Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Mon, 15 Nov 2021 21:50:35 +0100 Subject: [PATCH 04/19] feat(modulo): implement textview UI --- espanso-modulo/build.rs | 6 + espanso-modulo/src/lib.rs | 1 + espanso-modulo/src/sys/interop/interop.h | 8 + espanso-modulo/src/sys/interop/mod.rs | 11 + espanso-modulo/src/sys/mod.rs | 1 + espanso-modulo/src/sys/textview/mod.rs | 40 ++++ espanso-modulo/src/sys/textview/textview.cpp | 92 ++++++++ espanso-modulo/src/sys/textview/textview.fbp | 223 ++++++++++++++++++ .../src/sys/textview/textview_gui.cpp | 56 +++++ .../src/sys/textview/textview_gui.h | 50 ++++ espanso-modulo/src/textview/mod.rs | 26 ++ 11 files changed, 514 insertions(+) create mode 100644 espanso-modulo/src/sys/textview/mod.rs create mode 100644 espanso-modulo/src/sys/textview/textview.cpp create mode 100644 espanso-modulo/src/sys/textview/textview.fbp create mode 100644 espanso-modulo/src/sys/textview/textview_gui.cpp create mode 100644 espanso-modulo/src/sys/textview/textview_gui.h create mode 100644 espanso-modulo/src/textview/mod.rs diff --git a/espanso-modulo/build.rs b/espanso-modulo/build.rs index 53a995b..f3f1a21 100644 --- a/espanso-modulo/build.rs +++ b/espanso-modulo/build.rs @@ -121,6 +121,8 @@ fn build_native() { .file("src/sys/wizard/wizard_gui.cpp") .file("src/sys/welcome/welcome.cpp") .file("src/sys/welcome/welcome_gui.cpp") + .file("src/sys/textview/textview.cpp") + .file("src/sys/textview/textview_gui.cpp") .file("src/sys/troubleshooting/troubleshooting.cpp") .file("src/sys/troubleshooting/troubleshooting_gui.cpp") .flag("/EHsc") @@ -258,6 +260,8 @@ fn build_native() { .file("src/sys/wizard/wizard_gui.cpp") .file("src/sys/welcome/welcome.cpp") .file("src/sys/welcome/welcome_gui.cpp") + .file("src/sys/textview/textview.cpp") + .file("src/sys/textview/textview_gui.cpp") .file("src/sys/troubleshooting/troubleshooting.cpp") .file("src/sys/troubleshooting/troubleshooting_gui.cpp") .file("src/sys/common/mac.mm"); @@ -452,6 +456,8 @@ fn build_native() { .file("src/sys/wizard/wizard_gui.cpp") .file("src/sys/welcome/welcome.cpp") .file("src/sys/welcome/welcome_gui.cpp") + .file("src/sys/textview/textview.cpp") + .file("src/sys/textview/textview_gui.cpp") .file("src/sys/troubleshooting/troubleshooting.cpp") .file("src/sys/troubleshooting/troubleshooting_gui.cpp"); build.flag("-std=c++17"); diff --git a/espanso-modulo/src/lib.rs b/espanso-modulo/src/lib.rs index afd5396..59ce495 100644 --- a/espanso-modulo/src/lib.rs +++ b/espanso-modulo/src/lib.rs @@ -23,6 +23,7 @@ extern crate lazy_static; pub mod form; pub mod search; mod sys; +pub mod textview; pub mod troubleshooting; pub mod welcome; pub mod wizard; diff --git a/espanso-modulo/src/sys/interop/interop.h b/espanso-modulo/src/sys/interop/interop.h index 2d97c1e..a9510fe 100644 --- a/espanso-modulo/src/sys/interop/interop.h +++ b/espanso-modulo/src/sys/interop/interop.h @@ -168,3 +168,11 @@ typedef struct TroubleshootingMetadata { int (*dont_show_again_changed)(int); int (*open_file)(const char * file_name); } TroubleshootingMetadata; + +// TextView + +typedef struct TextViewMetadata { + const char *window_icon_path; + const char *title; + const char *content; +} TextViewMetadata; diff --git a/espanso-modulo/src/sys/interop/mod.rs b/espanso-modulo/src/sys/interop/mod.rs index debd2b9..e93c878 100644 --- a/espanso-modulo/src/sys/interop/mod.rs +++ b/espanso-modulo/src/sys/interop/mod.rs @@ -189,6 +189,14 @@ pub struct ErrorMetadata { pub message: *const c_char, } +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct TextViewMetadata { + pub window_icon_path: *const c_char, + pub title: *const c_char, + pub content: *const c_char, +} + // Native bindings #[allow(improper_ctypes)] @@ -220,4 +228,7 @@ extern "C" { // TROUBLESHOOTING pub(crate) fn interop_show_troubleshooting(metadata: *const TroubleshootingMetadata); + + // TEXTVIEW + pub(crate) fn interop_show_text_view(metadata: *const TextViewMetadata); } diff --git a/espanso-modulo/src/sys/mod.rs b/espanso-modulo/src/sys/mod.rs index d69b485..490f3ff 100644 --- a/espanso-modulo/src/sys/mod.rs +++ b/espanso-modulo/src/sys/mod.rs @@ -19,6 +19,7 @@ pub mod form; pub mod search; +pub mod textview; pub mod troubleshooting; pub mod welcome; pub mod wizard; diff --git a/espanso-modulo/src/sys/textview/mod.rs b/espanso-modulo/src/sys/textview/mod.rs new file mode 100644 index 0000000..a3213a1 --- /dev/null +++ b/espanso-modulo/src/sys/textview/mod.rs @@ -0,0 +1,40 @@ +/* + * This file is part of modulo. + * + * Copyright (C) 2020-2021 Federico Terzi + * + * modulo 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. + * + * modulo 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 modulo. If not, see . + */ + +use std::ffi::CString; + +use crate::sys::util::convert_to_cstring_or_null; +use crate::{sys::interop::TextViewMetadata, textview::TextViewOptions}; + +pub fn show(options: TextViewOptions) { + let (_c_window_icon_path, c_window_icon_path_ptr) = + convert_to_cstring_or_null(options.window_icon_path); + let c_title = CString::new(options.title).expect("unable to convert title to CString"); + let c_content = CString::new(options.content).expect("unable to convert content to CString"); + + let textview_metadata = TextViewMetadata { + window_icon_path: c_window_icon_path_ptr, + title: c_title.as_ptr(), + content: c_content.as_ptr(), + }; + + unsafe { + super::interop::interop_show_text_view(&textview_metadata); + } +} diff --git a/espanso-modulo/src/sys/textview/textview.cpp b/espanso-modulo/src/sys/textview/textview.cpp new file mode 100644 index 0000000..7134fbf --- /dev/null +++ b/espanso-modulo/src/sys/textview/textview.cpp @@ -0,0 +1,92 @@ +/* + * This file is part of modulo. + * + * Copyright (C) 2020-2021 Federico Terzi + * + * modulo 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. + * + * modulo 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 modulo. If not, see . + */ + +#define _UNICODE + +#include "../common/common.h" +#include "../interop/interop.h" +#include "./textview_gui.h" + +#include +#include +#include +#include + +TextViewMetadata *text_view_metadata = nullptr; + +// App Code + +class TextViewApp : public wxApp +{ +public: + virtual bool OnInit(); +}; + +class DerivedTextViewFrame : public TextViewFrame +{ +protected: + void on_copy_to_clipboard( wxCommandEvent& event ); + +public: + DerivedTextViewFrame(wxWindow *parent); +}; + +DerivedTextViewFrame::DerivedTextViewFrame(wxWindow *parent) + : TextViewFrame(parent) +{ + this->text_content->SetValue(wxString::FromUTF8(text_view_metadata->content)); + this->SetTitle(wxString::FromUTF8(text_view_metadata->title)); +} + +void DerivedTextViewFrame::on_copy_to_clipboard( wxCommandEvent& event ) { + if (wxTheClipboard->Open()) + { + wxTheClipboard->SetData( new wxTextDataObject(wxString::FromUTF8(text_view_metadata->content)) ); + wxTheClipboard->Close(); + } +} + +bool TextViewApp::OnInit() +{ + DerivedTextViewFrame *frame = new DerivedTextViewFrame(NULL); + + if (text_view_metadata->window_icon_path) + { + setFrameIcon(wxString::FromUTF8(text_view_metadata->window_icon_path), frame); + } + + frame->Show(true); + Activate(frame); + + return true; +} + +extern "C" void interop_show_text_view(TextViewMetadata *_metadata) +{ +// Setup high DPI support on Windows +#ifdef __WXMSW__ + SetProcessDPIAware(); +#endif + + text_view_metadata = _metadata; + + wxApp::SetInstance(new TextViewApp()); + int argc = 0; + wxEntry(argc, (char **)nullptr); +} \ No newline at end of file diff --git a/espanso-modulo/src/sys/textview/textview.fbp b/espanso-modulo/src/sys/textview/textview.fbp new file mode 100644 index 0000000..72bdce5 --- /dev/null +++ b/espanso-modulo/src/sys/textview/textview.fbp @@ -0,0 +1,223 @@ + + + + + ; + C++ + 1 + source_name + 0 + 0 + res + UTF-8 + connect + textview_gui + 1000 + none + + 0 + TextView + + . + #define _UNICODE + 1 + 1 + 1 + 1 + UI + 0 + 0 + + 0 + wxAUI_MGR_DEFAULT + wxSYS_COLOUR_WINDOW + wxBOTH + + 1 + 1 + impl_virtual + + + + 0 + wxID_ANY + + + TextViewFrame + + 895,545 + wxDEFAULT_FRAME_STYLE + ; ; forward_declare + TextView + + + + wxTAB_TRAVERSAL + 1 + + + bSizer1 + wxVERTICAL + none + + 5 + wxALL|wxEXPAND + 1 + + 1 + 1 + 1 + 1 + + + + + + + + 1 + 0 + 1 + + 1 + 0 + Dock + 0 + Left + 1 + + 1 + ,90,90,-1,76,0 + 0 + 0 + wxID_ANY + + 0 + + + + 0 + + 1 + text_content + 1 + + + protected + 1 + + Resizable + 1 + + wxTE_MULTILINE|wxTE_READONLY + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + + + + + 10 + wxEXPAND + 0 + + + bSizer2 + wxHORIZONTAL + none + + 5 + wxEXPAND + 1 + + 0 + protected + 0 + + + + 10 + wxALIGN_CENTER_VERTICAL|wxALL + 0 + + 1 + 1 + 1 + 1 + + + + + + + + + 1 + 0 + 1 + + 1 + + 1 + 0 + + Dock + 0 + Left + 1 + + 1 + + + 0 + 0 + wxID_ANY + Copy to Clipboard + + 0 + + 0 + + + 0 + + 1 + copy_to_clipboard_btn + 1 + + + protected + 1 + + + + Resizable + 1 + + + ; ; forward_declare + 0 + + + wxFILTER_NONE + wxDefaultValidator + + + + + on_copy_to_clipboard + + + + + + + + diff --git a/espanso-modulo/src/sys/textview/textview_gui.cpp b/espanso-modulo/src/sys/textview/textview_gui.cpp new file mode 100644 index 0000000..60d71ea --- /dev/null +++ b/espanso-modulo/src/sys/textview/textview_gui.cpp @@ -0,0 +1,56 @@ +/////////////////////////////////////////////////////////////////////////// +// C++ code generated with wxFormBuilder (version Oct 26 2018) +// http://www.wxformbuilder.org/ +// +// PLEASE DO *NOT* EDIT THIS FILE! +/////////////////////////////////////////////////////////////////////////// + +#define _UNICODE + +#include "textview_gui.h" + +/////////////////////////////////////////////////////////////////////////// + +TextViewFrame::TextViewFrame( wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style ) : wxFrame( parent, id, title, pos, size, style ) +{ + this->SetSizeHints( wxDefaultSize, wxDefaultSize ); + this->SetBackgroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_WINDOW ) ); + + wxBoxSizer* bSizer1; + bSizer1 = new wxBoxSizer( wxVERTICAL ); + + text_content = new wxTextCtrl( this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE|wxTE_READONLY ); + text_content->SetFont( wxFont( wxNORMAL_FONT->GetPointSize(), wxFONTFAMILY_TELETYPE, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL, false, wxEmptyString ) ); + + bSizer1->Add( text_content, 1, wxALL|wxEXPAND, 5 ); + + wxBoxSizer* bSizer2; + bSizer2 = new wxBoxSizer( wxHORIZONTAL ); + + + bSizer2->Add( 0, 0, 1, wxEXPAND, 5 ); + + copy_to_clipboard_btn = new wxButton( this, wxID_ANY, wxT("Copy to Clipboard"), wxDefaultPosition, wxDefaultSize, 0 ); + + copy_to_clipboard_btn->SetDefault(); + bSizer2->Add( copy_to_clipboard_btn, 0, wxALIGN_CENTER_VERTICAL|wxALL, 10 ); + + + bSizer1->Add( bSizer2, 0, wxEXPAND, 10 ); + + + this->SetSizer( bSizer1 ); + this->Layout(); + + this->Centre( wxBOTH ); + + // Connect Events + copy_to_clipboard_btn->Connect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( TextViewFrame::on_copy_to_clipboard ), NULL, this ); +} + +TextViewFrame::~TextViewFrame() +{ + // Disconnect Events + copy_to_clipboard_btn->Disconnect( wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler( TextViewFrame::on_copy_to_clipboard ), NULL, this ); + +} diff --git a/espanso-modulo/src/sys/textview/textview_gui.h b/espanso-modulo/src/sys/textview/textview_gui.h new file mode 100644 index 0000000..7c6bfc4 --- /dev/null +++ b/espanso-modulo/src/sys/textview/textview_gui.h @@ -0,0 +1,50 @@ +/////////////////////////////////////////////////////////////////////////// +// C++ code generated with wxFormBuilder (version Oct 26 2018) +// http://www.wxformbuilder.org/ +// +// PLEASE DO *NOT* EDIT THIS FILE! +/////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/////////////////////////////////////////////////////////////////////////// + + +/////////////////////////////////////////////////////////////////////////////// +/// Class TextViewFrame +/////////////////////////////////////////////////////////////////////////////// +class TextViewFrame : public wxFrame +{ + private: + + protected: + wxTextCtrl* text_content; + wxButton* copy_to_clipboard_btn; + + // Virtual event handlers, overide them in your derived class + virtual void on_copy_to_clipboard( wxCommandEvent& event ) { event.Skip(); } + + + public: + + TextViewFrame( wxWindow* parent, wxWindowID id = wxID_ANY, const wxString& title = wxT("TextView"), const wxPoint& pos = wxDefaultPosition, const wxSize& size = wxSize( 895,545 ), long style = wxDEFAULT_FRAME_STYLE|wxTAB_TRAVERSAL ); + + ~TextViewFrame(); + +}; + diff --git a/espanso-modulo/src/textview/mod.rs b/espanso-modulo/src/textview/mod.rs new file mode 100644 index 0000000..664ba01 --- /dev/null +++ b/espanso-modulo/src/textview/mod.rs @@ -0,0 +1,26 @@ +/* + * This file is part of modulo. + * + * Copyright (C) 2020-2021 Federico Terzi + * + * modulo 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. + * + * modulo 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 modulo. If not, see . + */ + +pub use crate::sys::textview::show; + +pub struct TextViewOptions { + pub window_icon_path: Option, + pub title: String, + pub content: String, +} From 02ec804604d2227eca1b8a7faf211dff87198dcc Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Mon, 15 Nov 2021 21:50:58 +0100 Subject: [PATCH 05/19] feat(core): wire up textview UI --- espanso/src/cli/modulo/mod.rs | 6 ++++ espanso/src/cli/modulo/textview.rs | 51 ++++++++++++++++++++++++++++++ espanso/src/main.rs | 17 ++++++++++ 3 files changed, 74 insertions(+) create mode 100644 espanso/src/cli/modulo/textview.rs diff --git a/espanso/src/cli/modulo/mod.rs b/espanso/src/cli/modulo/mod.rs index 7954d50..ffdbb6e 100644 --- a/espanso/src/cli/modulo/mod.rs +++ b/espanso/src/cli/modulo/mod.rs @@ -24,6 +24,8 @@ mod form; #[cfg(feature = "modulo")] mod search; #[cfg(feature = "modulo")] +mod textview; +#[cfg(feature = "modulo")] mod troubleshoot; #[cfg(feature = "modulo")] mod welcome; @@ -57,6 +59,10 @@ fn modulo_main(args: CliModuleArgs) -> i32 { return welcome::welcome_main(matches, &paths, &icon_paths); } + if let Some(matches) = cli_args.subcommand_matches("textview") { + return textview::textview_main(matches, &icon_paths); + } + if cli_args.subcommand_matches("troubleshoot").is_some() { return troubleshoot::troubleshoot_main(&paths, &icon_paths); } diff --git a/espanso/src/cli/modulo/textview.rs b/espanso/src/cli/modulo/textview.rs new file mode 100644 index 0000000..229668a --- /dev/null +++ b/espanso/src/cli/modulo/textview.rs @@ -0,0 +1,51 @@ +/* + * 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 . + */ + +use crate::icon::IconPaths; +use clap::ArgMatches; +use espanso_modulo::textview::TextViewOptions; + +pub fn textview_main(matches: &ArgMatches, icon_paths: &IconPaths) -> i32 { + let title = matches.value_of("title").unwrap_or("Espanso"); + + let input_file = matches + .value_of("input_file") + .expect("missing input, please specify the -i option"); + let data = if input_file == "-" { + use std::io::Read; + let mut buffer = String::new(); + std::io::stdin() + .read_to_string(&mut buffer) + .expect("unable to obtain input from stdin"); + buffer + } else { + std::fs::read_to_string(input_file).expect("unable to read input file") + }; + + espanso_modulo::textview::show(TextViewOptions { + window_icon_path: icon_paths + .wizard_icon + .as_ref() + .map(|path| path.to_string_lossy().to_string()), + title: title.to_string(), + content: data, + }); + + 0 +} diff --git a/espanso/src/main.rs b/espanso/src/main.rs index 58cf84f..97d42d9 100644 --- a/espanso/src/main.rs +++ b/espanso/src/main.rs @@ -292,6 +292,23 @@ For example, specifying 'email' is equivalent to 'match/email.yml'."#)) .help("Interpret the input data as JSON"), ), ) + .subcommand( + SubCommand::with_name("textview") + .about("Display a Text View") + .arg( + Arg::with_name("input_file") + .short("i") + .takes_value(true) + .help("Input file or - for stdin"), + ) + .arg( + Arg::with_name("title") + .long("title") + .required(true) + .takes_value(true) + .help("Window title to display"), + ), + ) .subcommand(SubCommand::with_name("troubleshoot").about("Display the troubleshooting GUI")) .subcommand( SubCommand::with_name("welcome") From b2452ecca7a9416c04686c9bcec8cab74a6af638 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Mon, 15 Nov 2021 22:23:29 +0100 Subject: [PATCH 06/19] feat(modulo): add Esc handling in textview UI --- espanso-modulo/src/sys/textview/textview.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/espanso-modulo/src/sys/textview/textview.cpp b/espanso-modulo/src/sys/textview/textview.cpp index 7134fbf..47a65ce 100644 --- a/espanso-modulo/src/sys/textview/textview.cpp +++ b/espanso-modulo/src/sys/textview/textview.cpp @@ -42,6 +42,8 @@ class DerivedTextViewFrame : public TextViewFrame { protected: void on_copy_to_clipboard( wxCommandEvent& event ); + + void on_char_event(wxKeyEvent &event); public: DerivedTextViewFrame(wxWindow *parent); @@ -52,6 +54,16 @@ DerivedTextViewFrame::DerivedTextViewFrame(wxWindow *parent) { this->text_content->SetValue(wxString::FromUTF8(text_view_metadata->content)); this->SetTitle(wxString::FromUTF8(text_view_metadata->title)); + + + Bind(wxEVT_CHAR_HOOK, &DerivedTextViewFrame::on_char_event, this, wxID_ANY); +} + +void DerivedTextViewFrame::on_char_event(wxKeyEvent &event) { + if (event.GetKeyCode() == WXK_ESCAPE) + { + Close(true); + } } void DerivedTextViewFrame::on_copy_to_clipboard( wxCommandEvent& event ) { From 84fd39a9527a794694619301503280c5426d9449 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Mon, 15 Nov 2021 22:24:30 +0100 Subject: [PATCH 07/19] feat(engine): implement ShowText event --- espanso-engine/src/dispatch/default.rs | 8 ++- espanso-engine/src/dispatch/executor/mod.rs | 1 + .../src/dispatch/executor/text_ui.rs | 56 +++++++++++++++++++ espanso-engine/src/dispatch/mod.rs | 3 + espanso-engine/src/event/mod.rs | 1 + espanso-engine/src/event/ui.rs | 6 ++ 6 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 espanso-engine/src/dispatch/executor/text_ui.rs diff --git a/espanso-engine/src/dispatch/default.rs b/espanso-engine/src/dispatch/default.rs index bb14a77..146b2c2 100644 --- a/espanso-engine/src/dispatch/default.rs +++ b/espanso-engine/src/dispatch/default.rs @@ -17,7 +17,9 @@ * along with espanso. If not, see . */ -use super::{ContextMenuHandler, Event, IconHandler, ImageInjector, SecureInputManager}; +use super::{ + ContextMenuHandler, Event, IconHandler, ImageInjector, SecureInputManager, TextUIHandler, +}; use super::{Dispatcher, Executor, HtmlInjector, KeyInjector, ModeProvider, TextInjector}; pub struct DefaultDispatcher<'a> { @@ -36,6 +38,7 @@ impl<'a> DefaultDispatcher<'a> { context_menu_handler: &'a dyn ContextMenuHandler, icon_handler: &'a dyn IconHandler, secure_input_manager: &'a dyn SecureInputManager, + text_ui_handler: &'a dyn TextUIHandler, ) -> Self { Self { executors: vec![ @@ -62,6 +65,9 @@ impl<'a> DefaultDispatcher<'a> { Box::new(super::executor::secure_input::SecureInputExecutor::new( secure_input_manager, )), + Box::new(super::executor::text_ui::TextUIExecutor::new( + text_ui_handler, + )), ], } } diff --git a/espanso-engine/src/dispatch/executor/mod.rs b/espanso-engine/src/dispatch/executor/mod.rs index 31bb6d0..e9ac066 100644 --- a/espanso-engine/src/dispatch/executor/mod.rs +++ b/espanso-engine/src/dispatch/executor/mod.rs @@ -24,3 +24,4 @@ pub mod image_inject; pub mod key_inject; pub mod secure_input; pub mod text_inject; +pub mod text_ui; diff --git a/espanso-engine/src/dispatch/executor/text_ui.rs b/espanso-engine/src/dispatch/executor/text_ui.rs new file mode 100644 index 0000000..4e80900 --- /dev/null +++ b/espanso-engine/src/dispatch/executor/text_ui.rs @@ -0,0 +1,56 @@ +/* + * 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 . + */ + +use crate::event::EventType; +use crate::{dispatch::Executor, event::Event}; +use anyhow::Result; +use log::error; + +pub trait TextUIHandler { + fn show_text(&self, title: &str, text: &str) -> Result<()>; +} + +pub struct TextUIExecutor<'a> { + handler: &'a dyn TextUIHandler, +} + +impl<'a> TextUIExecutor<'a> { + pub fn new(handler: &'a dyn TextUIHandler) -> Self { + Self { handler } + } +} + +impl<'a> Executor for TextUIExecutor<'a> { + fn execute(&self, event: &Event) -> bool { + if let EventType::ShowText(show_text_event) = &event.etype { + if let Err(error) = self + .handler + .show_text(&show_text_event.title, &show_text_event.text) + { + error!("text UI handler reported an error: {:?}", error); + } + + return true; + } + + false + } +} + +// TODO: test diff --git a/espanso-engine/src/dispatch/mod.rs b/espanso-engine/src/dispatch/mod.rs index d2959ff..753fffc 100644 --- a/espanso-engine/src/dispatch/mod.rs +++ b/espanso-engine/src/dispatch/mod.rs @@ -38,6 +38,7 @@ pub use executor::image_inject::ImageInjector; pub use executor::key_inject::KeyInjector; pub use executor::secure_input::SecureInputManager; pub use executor::text_inject::{Mode, ModeProvider, TextInjector}; +pub use executor::text_ui::{TextUIExecutor, TextUIHandler}; #[allow(clippy::too_many_arguments)] pub fn default<'a>( @@ -50,6 +51,7 @@ pub fn default<'a>( context_menu_handler: &'a dyn ContextMenuHandler, icon_handler: &'a dyn IconHandler, secure_input_manager: &'a dyn SecureInputManager, + text_ui_handler: &'a dyn TextUIHandler, ) -> impl Dispatcher + 'a { default::DefaultDispatcher::new( event_injector, @@ -61,5 +63,6 @@ pub fn default<'a>( context_menu_handler, icon_handler, secure_input_manager, + text_ui_handler, ) } diff --git a/espanso-engine/src/event/mod.rs b/espanso-engine/src/event/mod.rs index 2042588..7e6551c 100644 --- a/espanso-engine/src/event/mod.rs +++ b/espanso-engine/src/event/mod.rs @@ -101,6 +101,7 @@ pub enum EventType { IconStatusChange(ui::IconStatusChangeEvent), DisplaySecureInputTroubleshoot, ShowSearchBar, + ShowText(ui::ShowTextEvent), // Other LaunchSecureInputAutoFix, diff --git a/espanso-engine/src/event/ui.rs b/espanso-engine/src/event/ui.rs index b172c45..f23bbb9 100644 --- a/espanso-engine/src/event/ui.rs +++ b/espanso-engine/src/event/ui.rs @@ -52,3 +52,9 @@ pub enum IconStatus { Disabled, SecureInputDisabled, } + +#[derive(Debug, Clone, PartialEq)] +pub struct ShowTextEvent { + pub title: String, + pub text: String, +} From 9081ca76e709cbde1fe594ee5db3f8b7bfd03b97 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Mon, 15 Nov 2021 22:25:30 +0100 Subject: [PATCH 08/19] feat(core): wire up textview UI events --- .../worker/engine/dispatch/executor/mod.rs | 1 + .../engine/dispatch/executor/text_ui.rs | 39 ++++++++++++++ espanso/src/cli/worker/engine/mod.rs | 4 ++ espanso/src/gui/mod.rs | 7 ++- espanso/src/gui/modulo/manager.rs | 49 ++++++++++++++++++ espanso/src/gui/modulo/mod.rs | 1 + espanso/src/gui/modulo/textview.rs | 51 +++++++++++++++++++ 7 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 espanso/src/cli/worker/engine/dispatch/executor/text_ui.rs create mode 100644 espanso/src/gui/modulo/textview.rs diff --git a/espanso/src/cli/worker/engine/dispatch/executor/mod.rs b/espanso/src/cli/worker/engine/dispatch/executor/mod.rs index 7fee77a..ff3dc19 100644 --- a/espanso/src/cli/worker/engine/dispatch/executor/mod.rs +++ b/espanso/src/cli/worker/engine/dispatch/executor/mod.rs @@ -23,6 +23,7 @@ pub mod event_injector; pub mod icon; pub mod key_injector; pub mod secure_input; +pub mod text_ui; pub trait InjectParamsProvider { fn get(&self) -> InjectParams; diff --git a/espanso/src/cli/worker/engine/dispatch/executor/text_ui.rs b/espanso/src/cli/worker/engine/dispatch/executor/text_ui.rs new file mode 100644 index 0000000..5d73932 --- /dev/null +++ b/espanso/src/cli/worker/engine/dispatch/executor/text_ui.rs @@ -0,0 +1,39 @@ +/* + * 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 . + */ + +use espanso_engine::dispatch::TextUIHandler; + +use crate::gui::TextUI; + +pub struct TextUIHandlerAdapter<'a> { + text_ui: &'a dyn TextUI, +} + +impl<'a> TextUIHandlerAdapter<'a> { + pub fn new(text_ui: &'a dyn TextUI) -> Self { + Self { text_ui } + } +} + +impl<'a> TextUIHandler for TextUIHandlerAdapter<'a> { + fn show_text(&self, title: &str, text: &str) -> anyhow::Result<()> { + self.text_ui.show_text(title, text)?; + Ok(()) + } +} diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs index 07c653a..0cd74b2 100644 --- a/espanso/src/cli/worker/engine/mod.rs +++ b/espanso/src/cli/worker/engine/mod.rs @@ -37,6 +37,7 @@ use crate::{ clipboard_injector::ClipboardInjectorAdapter, context_menu::ContextMenuHandlerAdapter, event_injector::EventInjectorAdapter, icon::IconHandlerAdapter, key_injector::KeyInjectorAdapter, secure_input::SecureInputManagerAdapter, + text_ui::TextUIHandlerAdapter, }, process::middleware::{ image_resolve::PathProviderAdapter, @@ -106,6 +107,7 @@ pub fn initialize_and_spawn( let modulo_manager = crate::gui::modulo::manager::ModuloManager::new(); let modulo_form_ui = crate::gui::modulo::form::ModuloFormUI::new(&modulo_manager); let modulo_search_ui = crate::gui::modulo::search::ModuloSearchUI::new(&modulo_manager); + let modulo_text_ui = crate::gui::modulo::textview::ModuloTextUI::new(&modulo_manager); let context: Box = Box::new(super::context::DefaultContext::new( &config_manager, @@ -241,6 +243,7 @@ pub fn initialize_and_spawn( let context_menu_adapter = ContextMenuHandlerAdapter::new(&*ui_remote); let icon_adapter = IconHandlerAdapter::new(&*ui_remote); let secure_input_adapter = SecureInputManagerAdapter::new(); + let text_ui_adapter = TextUIHandlerAdapter::new(&modulo_text_ui); let dispatcher = espanso_engine::dispatch::default( &event_injector, &clipboard_injector, @@ -251,6 +254,7 @@ pub fn initialize_and_spawn( &context_menu_adapter, &icon_adapter, &secure_input_adapter, + &text_ui_adapter, ); // Disable previously granted linux capabilities if not needed anymore diff --git a/espanso/src/gui/mod.rs b/espanso/src/gui/mod.rs index bcd26e7..86ae5db 100644 --- a/espanso/src/gui/mod.rs +++ b/espanso/src/gui/mod.rs @@ -17,7 +17,7 @@ * along with espanso. If not, see . */ -use std::collections::HashMap; +use std::{collections::HashMap, path::Path}; use anyhow::Result; @@ -58,3 +58,8 @@ pub enum FormField { values: Vec, }, } + +pub trait TextUI { + fn show_text(&self, title: &str, text: &str) -> Result<()>; + fn show_file(&self, title: &str, path: &Path) -> Result<()>; +} diff --git a/espanso/src/gui/modulo/manager.rs b/espanso/src/gui/modulo/manager.rs index 0cc3184..e371db9 100644 --- a/espanso/src/gui/modulo/manager.rs +++ b/espanso/src/gui/modulo/manager.rs @@ -39,6 +39,52 @@ impl ModuloManager { Self { is_support_enabled } } + pub fn invoke_no_output(&self, args: &[&str], body: &str) -> Result<()> { + if self.is_support_enabled { + let exec_path = std::env::current_exe().expect("unable to obtain current exec path"); + let mut command = Command::new(exec_path); + let mut full_args = vec!["modulo"]; + full_args.extend(args); + command + .args(full_args) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()); + + crate::util::set_command_flags(&mut command); + + let child = command.spawn(); + + match child { + Ok(mut child) => { + if let Some(stdin) = child.stdin.as_mut() { + match stdin.write_all(body.as_bytes()) { + Ok(_) => { + // Get the output + match child.wait_with_output() { + Ok(child_output) => { + if child_output.status.success() { + Ok(()) + } else { + Err(ModuloError::NonZeroExit.into()) + } + } + Err(error) => Err(ModuloError::Error(error).into()), + } + } + Err(error) => Err(ModuloError::Error(error).into()), + } + } else { + Err(ModuloError::StdinError.into()) + } + } + Err(error) => Err(ModuloError::Error(error).into()), + } + } else { + Err(ModuloError::MissingModulo.into()) + } + } + pub fn invoke(&self, args: &[&str], body: &str) -> Result { if self.is_support_enabled { let exec_path = std::env::current_exe().expect("unable to obtain current exec path"); @@ -101,6 +147,9 @@ pub enum ModuloError { )] MissingModulo, + #[error("modulo returned a non-zero exit code")] + NonZeroExit, + #[error("modulo returned an empty output")] EmptyOutput, diff --git a/espanso/src/gui/modulo/mod.rs b/espanso/src/gui/modulo/mod.rs index 9e27396..e065ab4 100644 --- a/espanso/src/gui/modulo/mod.rs +++ b/espanso/src/gui/modulo/mod.rs @@ -20,3 +20,4 @@ pub mod form; pub mod manager; pub mod search; +pub mod textview; diff --git a/espanso/src/gui/modulo/textview.rs b/espanso/src/gui/modulo/textview.rs new file mode 100644 index 0000000..66d0834 --- /dev/null +++ b/espanso/src/gui/modulo/textview.rs @@ -0,0 +1,51 @@ +/* + * 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 . + */ + +use crate::gui::TextUI; + +use super::manager::ModuloManager; + +pub struct ModuloTextUI<'a> { + manager: &'a ModuloManager, +} + +impl<'a> ModuloTextUI<'a> { + pub fn new(manager: &'a ModuloManager) -> Self { + Self { manager } + } +} + +impl<'a> TextUI for ModuloTextUI<'a> { + fn show_text(&self, title: &str, text: &str) -> anyhow::Result<()> { + self + .manager + .invoke_no_output(&["textview", "--title", title, "-i", "-"], text)?; + + Ok(()) + } + + fn show_file(&self, title: &str, path: &std::path::Path) -> anyhow::Result<()> { + let path_str = path.to_string_lossy().to_string(); + self + .manager + .invoke_no_output(&["textview", "--title", title, "-i", &path_str], "")?; + + Ok(()) + } +} From 334e99b3438de59e50a422bcecd0425b8e41dd85 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Mon, 15 Nov 2021 22:25:52 +0100 Subject: [PATCH 09/19] feat(core): implement builtins to show active config and app --- espanso/src/cli/worker/builtin/debug.rs | 47 +++++++++++++++++++++++-- espanso/src/cli/worker/builtin/mod.rs | 2 ++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/espanso/src/cli/worker/builtin/debug.rs b/espanso/src/cli/worker/builtin/debug.rs index 6ab41df..c548090 100644 --- a/espanso/src/cli/worker/builtin/debug.rs +++ b/espanso/src/cli/worker/builtin/debug.rs @@ -17,7 +17,7 @@ * along with espanso. If not, see . */ -use espanso_engine::event::{effect::TextInjectRequest, EventType}; +use espanso_engine::event::{effect::TextInjectRequest, ui::ShowTextEvent, EventType}; use crate::cli::worker::builtin::generate_next_builtin_id; @@ -29,7 +29,7 @@ pub fn create_match_paste_active_config_info() -> BuiltInMatch { BuiltInMatch { id: generate_next_builtin_id(), label: "Paste active config information", - triggers: vec!["#acfg#".to_string()], + triggers: vec!["#pacfg#".to_string()], action: |context| { let dump = context.get_active_config().pretty_dump(); @@ -46,7 +46,7 @@ pub fn create_match_paste_active_app_info() -> BuiltInMatch { BuiltInMatch { id: generate_next_builtin_id(), label: "Paste active application information (detect)", - triggers: vec!["#detect#".to_string()], + triggers: vec!["#pdetect#".to_string()], action: |context| { let info = context.get_active_app_info(); @@ -65,3 +65,44 @@ pub fn create_match_paste_active_app_info() -> BuiltInMatch { ..Default::default() } } + +pub fn create_match_show_active_config_info() -> BuiltInMatch { + BuiltInMatch { + id: generate_next_builtin_id(), + label: "Show active config information", + triggers: vec!["#acfg#".to_string()], + action: |context| { + let dump = context.get_active_config().pretty_dump(); + + EventType::ShowText(ShowTextEvent { + text: dump, + title: "Active configuration".to_string(), + }) + }, + ..Default::default() + } +} + +pub fn create_match_show_active_app_info() -> BuiltInMatch { + BuiltInMatch { + id: generate_next_builtin_id(), + label: "Show active application information (detect)", + triggers: vec!["#detect#".to_string()], + action: |context| { + let info = context.get_active_app_info(); + + let dump = format!( + "title: '{}'\nexec: '{}'\nclass: '{}'", + info.title.unwrap_or_default(), + info.exec.unwrap_or_default(), + info.class.unwrap_or_default() + ); + + EventType::ShowText(ShowTextEvent { + text: dump, + title: "Active application information (detect)".to_string(), + }) + }, + ..Default::default() + } +} diff --git a/espanso/src/cli/worker/builtin/mod.rs b/espanso/src/cli/worker/builtin/mod.rs index 1189f1b..b8e84d2 100644 --- a/espanso/src/cli/worker/builtin/mod.rs +++ b/espanso/src/cli/worker/builtin/mod.rs @@ -55,6 +55,8 @@ pub fn get_builtin_matches(config: &dyn Config) -> Vec { let mut matches = vec![ debug::create_match_paste_active_config_info(), debug::create_match_paste_active_app_info(), + debug::create_match_show_active_config_info(), + debug::create_match_show_active_app_info(), process::create_match_exit(), process::create_match_restart(), ]; From c69544c1e2a8842a09373034be22f5f4b59469a3 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Mon, 15 Nov 2021 22:31:26 +0100 Subject: [PATCH 10/19] feat(engine): implement ShowLogs event --- espanso-engine/src/dispatch/executor/text_ui.rs | 7 +++++++ espanso-engine/src/event/mod.rs | 1 + 2 files changed, 8 insertions(+) diff --git a/espanso-engine/src/dispatch/executor/text_ui.rs b/espanso-engine/src/dispatch/executor/text_ui.rs index 4e80900..e08ba86 100644 --- a/espanso-engine/src/dispatch/executor/text_ui.rs +++ b/espanso-engine/src/dispatch/executor/text_ui.rs @@ -24,6 +24,7 @@ use log::error; pub trait TextUIHandler { fn show_text(&self, title: &str, text: &str) -> Result<()>; + fn show_logs(&self) -> Result<()>; } pub struct TextUIExecutor<'a> { @@ -46,6 +47,12 @@ impl<'a> Executor for TextUIExecutor<'a> { error!("text UI handler reported an error: {:?}", error); } + return true; + } else if let EventType::ShowLogs = &event.etype { + if let Err(error) = self.handler.show_logs() { + error!("text UI handler reported an error: {:?}", error); + } + return true; } diff --git a/espanso-engine/src/event/mod.rs b/espanso-engine/src/event/mod.rs index 7e6551c..2100cba 100644 --- a/espanso-engine/src/event/mod.rs +++ b/espanso-engine/src/event/mod.rs @@ -102,6 +102,7 @@ pub enum EventType { DisplaySecureInputTroubleshoot, ShowSearchBar, ShowText(ui::ShowTextEvent), + ShowLogs, // Other LaunchSecureInputAutoFix, From e2b6dcba38632ea169dadc2f97da26c473f7c3b5 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Mon, 15 Nov 2021 22:31:57 +0100 Subject: [PATCH 11/19] feat(core): implement builtin to show logs --- espanso/src/cli/worker/builtin/debug.rs | 10 ++++++++++ espanso/src/cli/worker/builtin/mod.rs | 1 + .../cli/worker/engine/dispatch/executor/text_ui.rs | 13 +++++++++++-- espanso/src/cli/worker/engine/mod.rs | 2 +- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/espanso/src/cli/worker/builtin/debug.rs b/espanso/src/cli/worker/builtin/debug.rs index c548090..3120dd0 100644 --- a/espanso/src/cli/worker/builtin/debug.rs +++ b/espanso/src/cli/worker/builtin/debug.rs @@ -106,3 +106,13 @@ pub fn create_match_show_active_app_info() -> BuiltInMatch { ..Default::default() } } + +pub fn create_match_show_logs() -> BuiltInMatch { + BuiltInMatch { + id: generate_next_builtin_id(), + label: "Show Espanso's logs", + triggers: vec!["#log#".to_string()], + action: |_| EventType::ShowLogs, + ..Default::default() + } +} diff --git a/espanso/src/cli/worker/builtin/mod.rs b/espanso/src/cli/worker/builtin/mod.rs index b8e84d2..ee8f7f8 100644 --- a/espanso/src/cli/worker/builtin/mod.rs +++ b/espanso/src/cli/worker/builtin/mod.rs @@ -57,6 +57,7 @@ pub fn get_builtin_matches(config: &dyn Config) -> Vec { debug::create_match_paste_active_app_info(), debug::create_match_show_active_config_info(), debug::create_match_show_active_app_info(), + debug::create_match_show_logs(), process::create_match_exit(), process::create_match_restart(), ]; diff --git a/espanso/src/cli/worker/engine/dispatch/executor/text_ui.rs b/espanso/src/cli/worker/engine/dispatch/executor/text_ui.rs index 5d73932..488d964 100644 --- a/espanso/src/cli/worker/engine/dispatch/executor/text_ui.rs +++ b/espanso/src/cli/worker/engine/dispatch/executor/text_ui.rs @@ -18,16 +18,18 @@ */ use espanso_engine::dispatch::TextUIHandler; +use espanso_path::Paths; use crate::gui::TextUI; pub struct TextUIHandlerAdapter<'a> { text_ui: &'a dyn TextUI, + paths: &'a Paths, } impl<'a> TextUIHandlerAdapter<'a> { - pub fn new(text_ui: &'a dyn TextUI) -> Self { - Self { text_ui } + pub fn new(text_ui: &'a dyn TextUI, paths: &'a Paths) -> Self { + Self { text_ui, paths } } } @@ -36,4 +38,11 @@ impl<'a> TextUIHandler for TextUIHandlerAdapter<'a> { self.text_ui.show_text(title, text)?; Ok(()) } + + fn show_logs(&self) -> anyhow::Result<()> { + self + .text_ui + .show_file("Espanso Logs", &self.paths.runtime.join("espanso.log"))?; + Ok(()) + } } diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs index 0cd74b2..7449bda 100644 --- a/espanso/src/cli/worker/engine/mod.rs +++ b/espanso/src/cli/worker/engine/mod.rs @@ -243,7 +243,7 @@ pub fn initialize_and_spawn( let context_menu_adapter = ContextMenuHandlerAdapter::new(&*ui_remote); let icon_adapter = IconHandlerAdapter::new(&*ui_remote); let secure_input_adapter = SecureInputManagerAdapter::new(); - let text_ui_adapter = TextUIHandlerAdapter::new(&modulo_text_ui); + let text_ui_adapter = TextUIHandlerAdapter::new(&modulo_text_ui, &paths); let dispatcher = espanso_engine::dispatch::default( &event_injector, &clipboard_injector, From 8909ccdb4d46d5ec3921e07d180f2ab6e6e4a411 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 16 Nov 2021 22:27:14 +0100 Subject: [PATCH 12/19] feat(engine): add show logs entry in context menu --- espanso-engine/src/process/middleware/context_menu.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/espanso-engine/src/process/middleware/context_menu.rs b/espanso-engine/src/process/middleware/context_menu.rs index 79de2e3..9ee53fd 100644 --- a/espanso-engine/src/process/middleware/context_menu.rs +++ b/espanso-engine/src/process/middleware/context_menu.rs @@ -32,6 +32,7 @@ const CONTEXT_ITEM_DISABLE: u32 = 3; const CONTEXT_ITEM_SECURE_INPUT_EXPLAIN: u32 = 4; const CONTEXT_ITEM_SECURE_INPUT_TRIGGER_WORKAROUND: u32 = 5; const CONTEXT_ITEM_OPEN_SEARCH: u32 = 6; +const CONTEXT_ITEM_SHOW_LOGS: u32 = 7; pub struct ContextMenuMiddleware { is_enabled: RefCell, @@ -82,6 +83,10 @@ impl Middleware for ContextMenuMiddleware { id: CONTEXT_ITEM_RELOAD, label: "Reload config".to_string(), }), + MenuItem::Simple(SimpleMenuItem { + id: CONTEXT_ITEM_SHOW_LOGS, + label: "Show logs".to_string(), + }), MenuItem::Separator, MenuItem::Simple(SimpleMenuItem { id: CONTEXT_ITEM_EXIT, @@ -155,6 +160,10 @@ impl Middleware for ContextMenuMiddleware { dispatch(Event::caused_by(event.source_id, EventType::ShowSearchBar)); Event::caused_by(event.source_id, EventType::NOOP) } + CONTEXT_ITEM_SHOW_LOGS => { + dispatch(Event::caused_by(event.source_id, EventType::ShowLogs)); + Event::caused_by(event.source_id, EventType::NOOP) + } _ => { // TODO: handle dynamic items todo!() From c4f4f438d3066ec419ef8f8e1836431d754f8ea3 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 16 Nov 2021 22:27:43 +0100 Subject: [PATCH 13/19] fix(core): prevent blocking when spawning the textview UI --- espanso/src/gui/modulo/manager.rs | 19 ++----------------- espanso/src/gui/modulo/textview.rs | 4 ++-- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/espanso/src/gui/modulo/manager.rs b/espanso/src/gui/modulo/manager.rs index e371db9..9f7ed51 100644 --- a/espanso/src/gui/modulo/manager.rs +++ b/espanso/src/gui/modulo/manager.rs @@ -39,7 +39,7 @@ impl ModuloManager { Self { is_support_enabled } } - pub fn invoke_no_output(&self, args: &[&str], body: &str) -> Result<()> { + pub fn spawn(&self, args: &[&str], body: &str) -> Result<()> { if self.is_support_enabled { let exec_path = std::env::current_exe().expect("unable to obtain current exec path"); let mut command = Command::new(exec_path); @@ -59,19 +59,7 @@ impl ModuloManager { Ok(mut child) => { if let Some(stdin) = child.stdin.as_mut() { match stdin.write_all(body.as_bytes()) { - Ok(_) => { - // Get the output - match child.wait_with_output() { - Ok(child_output) => { - if child_output.status.success() { - Ok(()) - } else { - Err(ModuloError::NonZeroExit.into()) - } - } - Err(error) => Err(ModuloError::Error(error).into()), - } - } + Ok(_) => Ok(()), Err(error) => Err(ModuloError::Error(error).into()), } } else { @@ -147,9 +135,6 @@ pub enum ModuloError { )] MissingModulo, - #[error("modulo returned a non-zero exit code")] - NonZeroExit, - #[error("modulo returned an empty output")] EmptyOutput, diff --git a/espanso/src/gui/modulo/textview.rs b/espanso/src/gui/modulo/textview.rs index 66d0834..57c31ba 100644 --- a/espanso/src/gui/modulo/textview.rs +++ b/espanso/src/gui/modulo/textview.rs @@ -35,7 +35,7 @@ impl<'a> TextUI for ModuloTextUI<'a> { fn show_text(&self, title: &str, text: &str) -> anyhow::Result<()> { self .manager - .invoke_no_output(&["textview", "--title", title, "-i", "-"], text)?; + .spawn(&["textview", "--title", title, "-i", "-"], text)?; Ok(()) } @@ -44,7 +44,7 @@ impl<'a> TextUI for ModuloTextUI<'a> { let path_str = path.to_string_lossy().to_string(); self .manager - .invoke_no_output(&["textview", "--title", title, "-i", &path_str], "")?; + .spawn(&["textview", "--title", title, "-i", &path_str], "")?; Ok(()) } From 41b72acdf1d733331fad72d62f91df68f097418f Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 21 Nov 2021 19:38:43 +0100 Subject: [PATCH 14/19] feat(clipboard): add clipboard operation options and alternative x11 xclip backend. #882 --- espanso-clipboard/src/cocoa/mod.rs | 19 ++- espanso-clipboard/src/lib.rs | 30 +++- espanso-clipboard/src/wayland/fallback/mod.rs | 19 ++- espanso-clipboard/src/win32/mod.rs | 19 ++- espanso-clipboard/src/x11/mod.rs | 64 +++++++- espanso-clipboard/src/x11/native/mod.rs | 19 ++- espanso-clipboard/src/x11/xclip/mod.rs | 139 ++++++++++++++++++ 7 files changed, 282 insertions(+), 27 deletions(-) create mode 100644 espanso-clipboard/src/x11/xclip/mod.rs diff --git a/espanso-clipboard/src/cocoa/mod.rs b/espanso-clipboard/src/cocoa/mod.rs index d842592..6802851 100644 --- a/espanso-clipboard/src/cocoa/mod.rs +++ b/espanso-clipboard/src/cocoa/mod.rs @@ -24,7 +24,7 @@ use std::{ path::PathBuf, }; -use crate::Clipboard; +use crate::{Clipboard, ClipboardOperationOptions}; use anyhow::Result; use log::error; use thiserror::Error; @@ -38,7 +38,7 @@ impl CocoaClipboard { } impl Clipboard for CocoaClipboard { - fn get_text(&self) -> Option { + fn get_text(&self, _: &ClipboardOperationOptions) -> Option { let mut buffer: [i8; 2048] = [0; 2048]; let native_result = unsafe { ffi::clipboard_get_text(buffer.as_mut_ptr(), (buffer.len() - 1) as i32) }; @@ -50,7 +50,7 @@ impl Clipboard for CocoaClipboard { } } - fn set_text(&self, text: &str) -> anyhow::Result<()> { + fn set_text(&self, text: &str, _: &ClipboardOperationOptions) -> anyhow::Result<()> { let string = CString::new(text)?; let native_result = unsafe { ffi::clipboard_set_text(string.as_ptr()) }; if native_result > 0 { @@ -60,7 +60,11 @@ impl Clipboard for CocoaClipboard { } } - fn set_image(&self, image_path: &std::path::Path) -> anyhow::Result<()> { + fn set_image( + &self, + image_path: &std::path::Path, + _: &ClipboardOperationOptions, + ) -> anyhow::Result<()> { if !image_path.exists() || !image_path.is_file() { return Err(CocoaClipboardError::ImageNotFound(image_path.to_path_buf()).into()); } @@ -75,7 +79,12 @@ impl Clipboard for CocoaClipboard { } } - fn set_html(&self, html: &str, fallback_text: Option<&str>) -> anyhow::Result<()> { + fn set_html( + &self, + html: &str, + fallback_text: Option<&str>, + _: &ClipboardOperationOptions, + ) -> anyhow::Result<()> { let html_string = CString::new(html)?; let fallback_string = CString::new(fallback_text.unwrap_or_default())?; let fallback_ptr = if fallback_text.is_some() { diff --git a/espanso-clipboard/src/lib.rs b/espanso-clipboard/src/lib.rs index 077728b..7416ce4 100644 --- a/espanso-clipboard/src/lib.rs +++ b/espanso-clipboard/src/lib.rs @@ -37,10 +37,28 @@ mod wayland; mod cocoa; pub trait Clipboard { - fn get_text(&self) -> Option; - fn set_text(&self, text: &str) -> Result<()>; - fn set_image(&self, image_path: &Path) -> Result<()>; - fn set_html(&self, html: &str, fallback_text: Option<&str>) -> Result<()>; + fn get_text(&self, options: &ClipboardOperationOptions) -> Option; + fn set_text(&self, text: &str, options: &ClipboardOperationOptions) -> Result<()>; + fn set_image(&self, image_path: &Path, options: &ClipboardOperationOptions) -> Result<()>; + fn set_html( + &self, + html: &str, + fallback_text: Option<&str>, + options: &ClipboardOperationOptions, + ) -> Result<()>; +} + +#[allow(dead_code)] +pub struct ClipboardOperationOptions { + pub use_xclip_backend: bool, +} + +impl Default for ClipboardOperationOptions { + fn default() -> Self { + Self { + use_xclip_backend: false, + } + } } #[allow(dead_code)] @@ -74,8 +92,8 @@ pub fn get_clipboard(_: ClipboardOptions) -> Result> { #[cfg(target_os = "linux")] #[cfg(not(feature = "wayland"))] pub fn get_clipboard(_: ClipboardOptions) -> Result> { - info!("using X11NativeClipboard"); - Ok(Box::new(x11::native::X11NativeClipboard::new()?)) + info!("using X11Clipboard"); + Ok(Box::new(x11::X11Clipboard::new()?)) } #[cfg(target_os = "linux")] diff --git a/espanso-clipboard/src/wayland/fallback/mod.rs b/espanso-clipboard/src/wayland/fallback/mod.rs index c916dac..f566a41 100644 --- a/espanso-clipboard/src/wayland/fallback/mod.rs +++ b/espanso-clipboard/src/wayland/fallback/mod.rs @@ -24,7 +24,7 @@ use std::{ process::Stdio, }; -use crate::{Clipboard, ClipboardOptions}; +use crate::{Clipboard, ClipboardOperationOptions, ClipboardOptions}; use anyhow::Result; use log::{error, warn}; use std::process::Command; @@ -74,7 +74,7 @@ impl WaylandFallbackClipboard { } impl Clipboard for WaylandFallbackClipboard { - fn get_text(&self) -> Option { + fn get_text(&self, _: &ClipboardOperationOptions) -> Option { let timeout = std::time::Duration::from_millis(self.command_timeout); match Command::new("wl-paste") .arg("--no-newline") @@ -116,11 +116,15 @@ impl Clipboard for WaylandFallbackClipboard { } } - fn set_text(&self, text: &str) -> anyhow::Result<()> { + fn set_text(&self, text: &str, _: &ClipboardOperationOptions) -> anyhow::Result<()> { self.invoke_command_with_timeout(&mut Command::new("wl-copy"), text.as_bytes(), "wl-copy") } - fn set_image(&self, image_path: &std::path::Path) -> anyhow::Result<()> { + fn set_image( + &self, + image_path: &std::path::Path, + _: &ClipboardOperationOptions, + ) -> anyhow::Result<()> { if !image_path.exists() || !image_path.is_file() { return Err(WaylandFallbackClipboardError::ImageNotFound(image_path.to_path_buf()).into()); } @@ -137,7 +141,12 @@ impl Clipboard for WaylandFallbackClipboard { ) } - fn set_html(&self, html: &str, _fallback_text: Option<&str>) -> anyhow::Result<()> { + fn set_html( + &self, + html: &str, + _fallback_text: Option<&str>, + _: &ClipboardOperationOptions, + ) -> anyhow::Result<()> { self.invoke_command_with_timeout( &mut Command::new("wl-copy").arg("--type").arg("text/html"), html.as_bytes(), diff --git a/espanso-clipboard/src/win32/mod.rs b/espanso-clipboard/src/win32/mod.rs index c4a3143..5d8023d 100644 --- a/espanso-clipboard/src/win32/mod.rs +++ b/espanso-clipboard/src/win32/mod.rs @@ -21,7 +21,7 @@ mod ffi; use std::{ffi::CString, path::PathBuf}; -use crate::Clipboard; +use crate::{Clipboard, ClipboardOperationOptions}; use anyhow::Result; use log::error; use thiserror::Error; @@ -36,7 +36,7 @@ impl Win32Clipboard { } impl Clipboard for Win32Clipboard { - fn get_text(&self) -> Option { + fn get_text(&self, _: &ClipboardOperationOptions) -> Option { let mut buffer: [u16; 2048] = [0; 2048]; let native_result = unsafe { ffi::clipboard_get_text(buffer.as_mut_ptr(), (buffer.len() - 1) as i32) }; @@ -48,7 +48,7 @@ impl Clipboard for Win32Clipboard { } } - fn set_text(&self, text: &str) -> anyhow::Result<()> { + fn set_text(&self, text: &str, _: &ClipboardOperationOptions) -> anyhow::Result<()> { let string = U16CString::from_str(text)?; let native_result = unsafe { ffi::clipboard_set_text(string.as_ptr()) }; if native_result > 0 { @@ -58,7 +58,11 @@ impl Clipboard for Win32Clipboard { } } - fn set_image(&self, image_path: &std::path::Path) -> anyhow::Result<()> { + fn set_image( + &self, + image_path: &std::path::Path, + _: &ClipboardOperationOptions, + ) -> anyhow::Result<()> { if !image_path.exists() || !image_path.is_file() { return Err(Win32ClipboardError::ImageNotFound(image_path.to_path_buf()).into()); } @@ -73,7 +77,12 @@ impl Clipboard for Win32Clipboard { } } - fn set_html(&self, html: &str, fallback_text: Option<&str>) -> anyhow::Result<()> { + fn set_html( + &self, + html: &str, + fallback_text: Option<&str>, + _: &ClipboardOperationOptions, + ) -> anyhow::Result<()> { let html_descriptor = generate_html_descriptor(html); let html_string = CString::new(html_descriptor)?; let fallback_string = U16CString::from_str(fallback_text.unwrap_or_default())?; diff --git a/espanso-clipboard/src/x11/mod.rs b/espanso-clipboard/src/x11/mod.rs index cccb648..9e658e3 100644 --- a/espanso-clipboard/src/x11/mod.rs +++ b/espanso-clipboard/src/x11/mod.rs @@ -17,4 +17,66 @@ * along with espanso. If not, see . */ -pub(crate) mod native; +use anyhow::Result; + +use crate::{Clipboard, ClipboardOperationOptions}; + +mod native; +mod xclip; + +pub(crate) struct X11Clipboard { + native_backend: native::X11NativeClipboard, + xclip_backend: xclip::XClipClipboard, +} + +impl X11Clipboard { + pub fn new() -> Result { + Ok(Self { + native_backend: native::X11NativeClipboard::new()?, + xclip_backend: xclip::XClipClipboard::new(), + }) + } +} + +impl Clipboard for X11Clipboard { + fn get_text(&self, options: &ClipboardOperationOptions) -> Option { + if options.use_xclip_backend { + self.xclip_backend.get_text(options) + } else { + self.native_backend.get_text(options) + } + } + + fn set_text(&self, text: &str, options: &ClipboardOperationOptions) -> anyhow::Result<()> { + if options.use_xclip_backend { + self.xclip_backend.set_text(text, options) + } else { + self.native_backend.set_text(text, options) + } + } + + fn set_image( + &self, + image_path: &std::path::Path, + options: &ClipboardOperationOptions, + ) -> anyhow::Result<()> { + if options.use_xclip_backend { + self.xclip_backend.set_image(image_path, options) + } else { + self.native_backend.set_image(image_path, options) + } + } + + fn set_html( + &self, + html: &str, + fallback_text: Option<&str>, + options: &ClipboardOperationOptions, + ) -> anyhow::Result<()> { + if options.use_xclip_backend { + self.xclip_backend.set_html(html, fallback_text, options) + } else { + self.native_backend.set_html(html, fallback_text, options) + } + } +} diff --git a/espanso-clipboard/src/x11/native/mod.rs b/espanso-clipboard/src/x11/native/mod.rs index de5a1f1..6cba718 100644 --- a/espanso-clipboard/src/x11/native/mod.rs +++ b/espanso-clipboard/src/x11/native/mod.rs @@ -23,7 +23,7 @@ use std::{ path::PathBuf, }; -use crate::Clipboard; +use crate::{Clipboard, ClipboardOperationOptions}; use anyhow::Result; use std::os::raw::c_char; use thiserror::Error; @@ -39,7 +39,7 @@ impl X11NativeClipboard { } impl Clipboard for X11NativeClipboard { - fn get_text(&self) -> Option { + fn get_text(&self, _: &ClipboardOperationOptions) -> Option { let mut buffer: [c_char; 2048] = [0; 2048]; let native_result = unsafe { ffi::clipboard_x11_get_text(buffer.as_mut_ptr(), (buffer.len() - 1) as i32) }; @@ -51,7 +51,7 @@ impl Clipboard for X11NativeClipboard { } } - fn set_text(&self, text: &str) -> anyhow::Result<()> { + fn set_text(&self, text: &str, _: &ClipboardOperationOptions) -> anyhow::Result<()> { let string = CString::new(text)?; let native_result = unsafe { ffi::clipboard_x11_set_text(string.as_ptr()) }; if native_result > 0 { @@ -61,7 +61,11 @@ impl Clipboard for X11NativeClipboard { } } - fn set_image(&self, image_path: &std::path::Path) -> anyhow::Result<()> { + fn set_image( + &self, + image_path: &std::path::Path, + _: &ClipboardOperationOptions, + ) -> anyhow::Result<()> { if !image_path.exists() || !image_path.is_file() { return Err(X11NativeClipboardError::ImageNotFound(image_path.to_path_buf()).into()); } @@ -80,7 +84,12 @@ impl Clipboard for X11NativeClipboard { } } - fn set_html(&self, html: &str, fallback_text: Option<&str>) -> anyhow::Result<()> { + fn set_html( + &self, + html: &str, + fallback_text: Option<&str>, + _: &ClipboardOperationOptions, + ) -> anyhow::Result<()> { let html_string = CString::new(html)?; let fallback_string = CString::new(fallback_text.unwrap_or_default())?; let fallback_ptr = if fallback_text.is_some() { diff --git a/espanso-clipboard/src/x11/xclip/mod.rs b/espanso-clipboard/src/x11/xclip/mod.rs new file mode 100644 index 0000000..1136dff --- /dev/null +++ b/espanso-clipboard/src/x11/xclip/mod.rs @@ -0,0 +1,139 @@ +/* + * 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 . + */ + +use anyhow::bail; +use log::error; +use std::io::Write; +use std::process::{Command, Stdio}; + +use crate::{Clipboard, ClipboardOperationOptions}; + +pub struct XClipClipboard { + is_xclip_available: bool, +} + +impl XClipClipboard { + pub fn new() -> Self { + let command = Command::new("xclipz").arg("-h").output(); + let is_xclip_available = command + .map(|output| output.status.success()) + .unwrap_or(false); + + Self { is_xclip_available } + } +} + +impl Clipboard for XClipClipboard { + fn get_text(&self, _: &ClipboardOperationOptions) -> Option { + if !self.is_xclip_available { + error!("attempted to use XClipClipboard, but `xclip` command can't be called"); + return None; + } + + match Command::new("xclip").args(&["-o", "-sel", "clip"]).output() { + Ok(output) => { + if output.status.success() { + let s = String::from_utf8_lossy(&output.stdout); + return Some(s.to_string()); + } + } + Err(error) => { + error!("xclip reported an error: {}", error); + } + } + + None + } + + fn set_text(&self, text: &str, _: &ClipboardOperationOptions) -> anyhow::Result<()> { + if !self.is_xclip_available { + bail!("attempted to use XClipClipboard, but `xclip` command can't be called"); + } + + let mut child = Command::new("xclip") + .args(&["-sel", "clip"]) + .stdin(Stdio::piped()) + .spawn()?; + + let stdin = child.stdin.as_mut(); + if let Some(input) = stdin { + input.write_all(text.as_bytes())?; + child.wait()?; + } + + Ok(()) + } + + fn set_image( + &self, + image_path: &std::path::Path, + _: &ClipboardOperationOptions, + ) -> anyhow::Result<()> { + if !self.is_xclip_available { + bail!("attempted to use XClipClipboard, but `xclip` command can't be called"); + } + + let extension = image_path.extension(); + let mime = match extension { + Some(ext) => { + let ext = ext.to_string_lossy().to_lowercase(); + match ext.as_ref() { + "png" => "image/png", + "jpg" | "jpeg" => "image/jpeg", + "gif" => "image/gif", + "svg" => "image/svg", + _ => "image/png", + } + } + None => "image/png", + }; + + let image_path = image_path.to_string_lossy(); + + Command::new("xclip") + .args(&["-selection", "clipboard", "-t", mime, "-i", &image_path]) + .spawn()?; + + Ok(()) + } + + fn set_html( + &self, + html: &str, + _: Option<&str>, + _: &ClipboardOperationOptions, + ) -> anyhow::Result<()> { + if !self.is_xclip_available { + bail!("attempted to use XClipClipboard, but `xclip` command can't be called"); + } + + let mut child = Command::new("xclip") + .args(&["-sel", "clip", "-t", "text/html"]) + .stdin(Stdio::piped()) + .spawn()?; + + let stdin = child.stdin.as_mut(); + if let Some(input) = stdin { + input.write_all(html.as_bytes())?; + child.wait()?; + } + + Ok(()) + } +} From 42cbb6e3de0e632687c929717400df1a403d0013 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 21 Nov 2021 19:39:35 +0100 Subject: [PATCH 15/19] feat(config): create config option for alternative x11 xclip backend --- espanso-config/src/config/mod.rs | 6 ++++++ espanso-config/src/config/parse/mod.rs | 1 + espanso-config/src/config/parse/yaml.rs | 6 ++++++ espanso-config/src/config/resolve.rs | 5 +++++ espanso-config/src/legacy/mod.rs | 4 ++++ 5 files changed, 22 insertions(+) diff --git a/espanso-config/src/config/mod.rs b/espanso-config/src/config/mod.rs index dd88929..0992372 100644 --- a/espanso-config/src/config/mod.rs +++ b/espanso-config/src/config/mod.rs @@ -150,6 +150,10 @@ pub trait Config: Send + Sync { // If false, avoid showing the SecureInput notification on macOS fn secure_input_notification(&self) -> bool; + // If true, use the `xclip` command to implement the clipboard instead of + // the built-in native module on X11. + fn x11_use_xclip_backend(&self) -> bool; + // If true, filter out keyboard events without an explicit HID device source on Windows. // This is needed to filter out the software-generated events, including // those from espanso, but might need to be disabled when using some software-level keyboards. @@ -193,6 +197,7 @@ pub trait Config: Send + Sync { show_notifications: {:?} secure_input_notification: {:?} + x11_use_xclip_backend: {:?} win32_exclude_orphan_events: {:?} win32_keyboard_layout_cache_interval: {:?} @@ -224,6 +229,7 @@ pub trait Config: Send + Sync { self.show_notifications(), self.secure_input_notification(), + self.x11_use_xclip_backend(), self.win32_exclude_orphan_events(), self.win32_keyboard_layout_cache_interval(), diff --git a/espanso-config/src/config/parse/mod.rs b/espanso-config/src/config/parse/mod.rs index bd39ad4..94f174d 100644 --- a/espanso-config/src/config/parse/mod.rs +++ b/espanso-config/src/config/parse/mod.rs @@ -46,6 +46,7 @@ pub(crate) struct ParsedConfig { pub secure_input_notification: Option, pub win32_exclude_orphan_events: Option, pub win32_keyboard_layout_cache_interval: Option, + pub x11_use_xclip_backend: Option, pub pre_paste_delay: Option, pub restore_clipboard_delay: Option, diff --git a/espanso-config/src/config/parse/yaml.rs b/espanso-config/src/config/parse/yaml.rs index 824429c..d666662 100644 --- a/espanso-config/src/config/parse/yaml.rs +++ b/espanso-config/src/config/parse/yaml.rs @@ -112,6 +112,9 @@ pub(crate) struct YAMLConfig { #[serde(default)] pub win32_keyboard_layout_cache_interval: Option, + #[serde(default)] + pub x11_use_xclip_backend: Option, + // Include/Exclude #[serde(default)] pub includes: Option>, @@ -201,6 +204,7 @@ impl TryFrom for ParsedConfig { win32_exclude_orphan_events: yaml_config.win32_exclude_orphan_events, win32_keyboard_layout_cache_interval: yaml_config.win32_keyboard_layout_cache_interval, + x11_use_xclip_backend: yaml_config.x11_use_xclip_backend, use_standard_includes: yaml_config.use_standard_includes, includes: yaml_config.includes, @@ -258,6 +262,7 @@ mod tests { secure_input_notification: false win32_exclude_orphan_events: false win32_keyboard_layout_cache_interval: 300 + x11_use_xclip_backend: true use_standard_includes: true includes: ["test1"] @@ -311,6 +316,7 @@ mod tests { secure_input_notification: Some(false), win32_exclude_orphan_events: Some(false), win32_keyboard_layout_cache_interval: Some(300), + x11_use_xclip_backend: Some(true), pre_paste_delay: Some(300), evdev_modifier_delay: Some(40), diff --git a/espanso-config/src/config/resolve.rs b/espanso-config/src/config/resolve.rs index afcad9b..8738085 100644 --- a/espanso-config/src/config/resolve.rs +++ b/espanso-config/src/config/resolve.rs @@ -330,6 +330,10 @@ impl Config for ResolvedConfig { .win32_keyboard_layout_cache_interval .unwrap_or(2000) } + + fn x11_use_xclip_backend(&self) -> bool { + self.parsed.x11_use_xclip_backend.unwrap_or(false) + } } impl ResolvedConfig { @@ -413,6 +417,7 @@ impl ResolvedConfig { secure_input_notification, win32_exclude_orphan_events, win32_keyboard_layout_cache_interval, + x11_use_xclip_backend, includes, excludes, extra_includes, diff --git a/espanso-config/src/legacy/mod.rs b/espanso-config/src/legacy/mod.rs index 8347c8e..b67e0f7 100644 --- a/espanso-config/src/legacy/mod.rs +++ b/espanso-config/src/legacy/mod.rs @@ -398,6 +398,10 @@ impl Config for LegacyInteropConfig { fn win32_keyboard_layout_cache_interval(&self) -> i64 { 2000 } + + fn x11_use_xclip_backend(&self) -> bool { + false + } } struct LegacyMatchGroup { From 08853451c0c705e42322067c71f5a6859d251d94 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 21 Nov 2021 19:40:53 +0100 Subject: [PATCH 16/19] feat(core): wire up alternative x11 xclip clipboard backend and create patch for gedit. Fix #882 --- espanso/src/cli/worker/config.rs | 15 ++++++- .../dispatch/executor/clipboard_injector.rs | 38 +++++++++++++---- espanso/src/cli/worker/engine/mod.rs | 2 +- .../middleware/render/extension/clipboard.rs | 23 +++++++++-- espanso/src/patch/mod.rs | 1 + espanso/src/patch/patches/linux/gedit_x11.rs | 41 +++++++++++++++++++ espanso/src/patch/patches/linux/mod.rs | 1 + espanso/src/patch/patches/mod.rs | 1 + 8 files changed, 109 insertions(+), 13 deletions(-) create mode 100644 espanso/src/patch/patches/linux/gedit_x11.rs diff --git a/espanso/src/cli/worker/config.rs b/espanso/src/cli/worker/config.rs index 408782a..dd69176 100644 --- a/espanso/src/cli/worker/config.rs +++ b/espanso/src/cli/worker/config.rs @@ -25,7 +25,10 @@ use espanso_config::{ }; use espanso_info::{AppInfo, AppInfoProvider}; -use super::builtin::is_builtin_match; +use super::{ + builtin::is_builtin_match, + engine::process::middleware::render::extension::clipboard::ClipboardOperationOptionsProvider, +}; pub struct ConfigManager<'a> { config_store: &'a dyn ConfigStore, @@ -139,6 +142,16 @@ impl<'a> super::engine::dispatch::executor::clipboard_injector::ClipboardParamsP disable_x11_fast_inject: active.disable_x11_fast_inject(), restore_clipboard: active.preserve_clipboard(), restore_clipboard_delay: active.restore_clipboard_delay(), + x11_use_xclip_backend: active.x11_use_xclip_backend(), + } + } +} + +impl<'a> ClipboardOperationOptionsProvider for ConfigManager<'a> { + fn get_operation_options(&self) -> espanso_clipboard::ClipboardOperationOptions { + let active = self.active(); + espanso_clipboard::ClipboardOperationOptions { + use_xclip_backend: active.x11_use_xclip_backend(), } } } diff --git a/espanso/src/cli/worker/engine/dispatch/executor/clipboard_injector.rs b/espanso/src/cli/worker/engine/dispatch/executor/clipboard_injector.rs index 8ebfd54..163245b 100644 --- a/espanso/src/cli/worker/engine/dispatch/executor/clipboard_injector.rs +++ b/espanso/src/cli/worker/engine/dispatch/executor/clipboard_injector.rs @@ -19,7 +19,7 @@ use std::{convert::TryInto, path::PathBuf}; -use espanso_clipboard::Clipboard; +use espanso_clipboard::{Clipboard, ClipboardOperationOptions}; use espanso_inject::{keys::Key, InjectionOptions, Injector}; use log::error; @@ -39,6 +39,7 @@ pub struct ClipboardParams { pub disable_x11_fast_inject: bool, pub restore_clipboard: bool, pub restore_clipboard_delay: usize, + pub x11_use_xclip_backend: bool, } pub struct ClipboardInjectorAdapter<'a> { @@ -103,11 +104,19 @@ impl<'a> ClipboardInjectorAdapter<'a> { Some(ClipboardRestoreGuard::lock( self.clipboard, params.restore_clipboard_delay.try_into().unwrap(), + self.get_operation_options(), )) } else { None } } + + fn get_operation_options(&self) -> ClipboardOperationOptions { + let params = self.params_provider.get(); + ClipboardOperationOptions { + use_xclip_backend: params.x11_use_xclip_backend, + } + } } impl<'a> TextInjector for ClipboardInjectorAdapter<'a> { @@ -118,7 +127,9 @@ impl<'a> TextInjector for ClipboardInjectorAdapter<'a> { fn inject_text(&self, text: &str) -> anyhow::Result<()> { let _guard = self.restore_clipboard_guard(); - self.clipboard.set_text(text)?; + self + .clipboard + .set_text(text, &self.get_operation_options())?; self.send_paste_combination()?; @@ -130,7 +141,9 @@ impl<'a> HtmlInjector for ClipboardInjectorAdapter<'a> { fn inject_html(&self, html: &str, fallback_text: &str) -> anyhow::Result<()> { let _guard = self.restore_clipboard_guard(); - self.clipboard.set_html(html, Some(fallback_text))?; + self + .clipboard + .set_html(html, Some(fallback_text), &self.get_operation_options())?; self.send_paste_combination()?; @@ -153,7 +166,9 @@ impl<'a> ImageInjector for ClipboardInjectorAdapter<'a> { let _guard = self.restore_clipboard_guard(); - self.clipboard.set_image(&path)?; + self + .clipboard + .set_image(&path, &self.get_operation_options())?; self.send_paste_combination()?; @@ -165,16 +180,22 @@ struct ClipboardRestoreGuard<'a> { clipboard: &'a dyn Clipboard, content: Option, restore_delay: u64, + clipboard_operation_options: ClipboardOperationOptions, } impl<'a> ClipboardRestoreGuard<'a> { - pub fn lock(clipboard: &'a dyn Clipboard, restore_delay: u64) -> Self { - let clipboard_content = clipboard.get_text(); + pub fn lock( + clipboard: &'a dyn Clipboard, + restore_delay: u64, + clipboard_operation_options: ClipboardOperationOptions, + ) -> Self { + let clipboard_content = clipboard.get_text(&clipboard_operation_options); Self { clipboard, content: clipboard_content, restore_delay, + clipboard_operation_options, } } } @@ -186,7 +207,10 @@ impl<'a> Drop for ClipboardRestoreGuard<'a> { // A delay is needed to mitigate the problem std::thread::sleep(std::time::Duration::from_millis(self.restore_delay)); - if let Err(error) = self.clipboard.set_text(&content) { + if let Err(error) = self + .clipboard + .set_text(&content, &self.clipboard_operation_options) + { error!( "unable to restore clipboard content after expansion: {}", error diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs index 7449bda..513bff5 100644 --- a/espanso/src/cli/worker/engine/mod.rs +++ b/espanso/src/cli/worker/engine/mod.rs @@ -181,7 +181,7 @@ pub fn initialize_and_spawn( let clipboard = espanso_clipboard::get_clipboard(Default::default()) .expect("failed to initialize clipboard module"); // TODO: handle options - let clipboard_adapter = ClipboardAdapter::new(&*clipboard); + let clipboard_adapter = ClipboardAdapter::new(&*clipboard, &config_manager); let clipboard_extension = espanso_render::extension::clipboard::ClipboardExtension::new(&clipboard_adapter); let date_extension = espanso_render::extension::date::DateExtension::new(); diff --git a/espanso/src/cli/worker/engine/process/middleware/render/extension/clipboard.rs b/espanso/src/cli/worker/engine/process/middleware/render/extension/clipboard.rs index 57b2c91..999b308 100644 --- a/espanso/src/cli/worker/engine/process/middleware/render/extension/clipboard.rs +++ b/espanso/src/cli/worker/engine/process/middleware/render/extension/clipboard.rs @@ -17,21 +17,36 @@ * along with espanso. If not, see . */ -use espanso_clipboard::Clipboard; +use espanso_clipboard::{Clipboard, ClipboardOperationOptions}; use espanso_render::extension::clipboard::ClipboardProvider; +pub trait ClipboardOperationOptionsProvider { + fn get_operation_options(&self) -> ClipboardOperationOptions; +} + pub struct ClipboardAdapter<'a> { clipboard: &'a dyn Clipboard, + clipboard_operation_options_provider: &'a dyn ClipboardOperationOptionsProvider, } impl<'a> ClipboardAdapter<'a> { - pub fn new(clipboard: &'a dyn Clipboard) -> Self { - Self { clipboard } + pub fn new( + clipboard: &'a dyn Clipboard, + clipboard_operation_options_provider: &'a dyn ClipboardOperationOptionsProvider, + ) -> Self { + Self { + clipboard, + clipboard_operation_options_provider, + } } } impl<'a> ClipboardProvider for ClipboardAdapter<'a> { fn get_text(&self) -> Option { - self.clipboard.get_text() + self.clipboard.get_text( + &self + .clipboard_operation_options_provider + .get_operation_options(), + ) } } diff --git a/espanso/src/patch/mod.rs b/espanso/src/patch/mod.rs index f1ce634..fa6d065 100644 --- a/espanso/src/patch/mod.rs +++ b/espanso/src/patch/mod.rs @@ -42,6 +42,7 @@ fn get_builtin_patches() -> Vec { return vec![ patches::linux::alacritty_terminal_x11::patch(), patches::linux::emacs_x11::patch(), + patches::linux::gedit_x11::patch(), patches::linux::generic_terminal_x11::patch(), patches::linux::kitty_terminal_x11::patch(), patches::linux::konsole_terminal_x11::patch(), diff --git a/espanso/src/patch/patches/linux/gedit_x11.rs b/espanso/src/patch/patches/linux/gedit_x11.rs new file mode 100644 index 0000000..458f9c9 --- /dev/null +++ b/espanso/src/patch/patches/linux/gedit_x11.rs @@ -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 . + */ + +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 = "linux") && !super::util::is_wayland(), + should_patch: |app| app.class.unwrap_or_default().contains("Gedit"), + apply: |base, name| { + Arc::new(PatchedConfig::patch( + base, + name, + Patches { + x11_use_xclip_backend: Some(true), + ..Default::default() + }, + )) + }, + } +} diff --git a/espanso/src/patch/patches/linux/mod.rs b/espanso/src/patch/patches/linux/mod.rs index 3d42011..1e0bc94 100644 --- a/espanso/src/patch/patches/linux/mod.rs +++ b/espanso/src/patch/patches/linux/mod.rs @@ -19,6 +19,7 @@ pub mod alacritty_terminal_x11; pub mod emacs_x11; +pub mod gedit_x11; pub mod generic_terminal_x11; pub mod kitty_terminal_x11; pub mod konsole_terminal_x11; diff --git a/espanso/src/patch/patches/mod.rs b/espanso/src/patch/patches/mod.rs index c6dd4f7..e45567d 100644 --- a/espanso/src/patch/patches/mod.rs +++ b/espanso/src/patch/patches/mod.rs @@ -50,5 +50,6 @@ generate_patchable_config!( undo_backspace -> bool, win32_exclude_orphan_events -> bool, win32_keyboard_layout_cache_interval -> i64, + x11_use_xclip_backend -> bool, keyboard_layout -> Option ); From 20cfb9cb3bbabe3330bc6411456353010d76d9e9 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 21 Nov 2021 19:42:08 +0100 Subject: [PATCH 17/19] fix(misc): add xclip to snap packages to support alternative backend. #882 --- snapcraft.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/snapcraft.yaml b/snapcraft.yaml index 22f5230..8f5437c 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -80,6 +80,7 @@ parts: - libwayland-cursor0 - libwayland-egl1 - libwxgtk3.0-gtk3-0v5 + - xclip apps: espanso: From 57b2f194e5464884fd703cd9c2a9b0b83910c3c4 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 21 Nov 2021 20:57:05 +0100 Subject: [PATCH 18/19] fix(inject): attempt setting explicit coregraphics dependency to fix compilation on macOS 11.6 --- espanso-inject/build.rs | 1 + espanso-inject/src/mac/native.mm | 1 + 2 files changed, 2 insertions(+) diff --git a/espanso-inject/build.rs b/espanso-inject/build.rs index 16bd1a4..702678d 100644 --- a/espanso-inject/build.rs +++ b/espanso-inject/build.rs @@ -64,6 +64,7 @@ fn cc_config() { println!("cargo:rustc-link-lib=dylib=c++"); println!("cargo:rustc-link-lib=static=espansoinject"); println!("cargo:rustc-link-lib=framework=Cocoa"); + println!("cargo:rustc-link-lib=framework=CoreGraphics"); } fn main() { diff --git a/espanso-inject/src/mac/native.mm b/espanso-inject/src/mac/native.mm index 7a8e27f..cecbb56 100644 --- a/espanso-inject/src/mac/native.mm +++ b/espanso-inject/src/mac/native.mm @@ -20,6 +20,7 @@ #include "native.h" #include #import +#import #include // Events dispatched by espanso are "marked" with a custom location From 73214cb59aac7d87f0bd32f521a46cf20e8dec56 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Mon, 22 Nov 2021 22:54:44 +0100 Subject: [PATCH 19/19] fix(inject): improve X11 injector to handle dead keys. Fix #881 --- espanso-inject/src/x11/ffi.rs | 55 ++++++- espanso-inject/src/x11/mod.rs | 277 +++++++++++++++++++++++++++------- 2 files changed, 270 insertions(+), 62 deletions(-) diff --git a/espanso-inject/src/x11/ffi.rs b/espanso-inject/src/x11/ffi.rs index d90d333..e456b3a 100644 --- a/espanso-inject/src/x11/ffi.rs +++ b/espanso-inject/src/x11/ffi.rs @@ -47,17 +47,31 @@ pub struct XModifierKeymap { pub modifiermap: *mut KeyCode, } +// XCreateIC values +#[allow(non_upper_case_globals)] +pub const XIMPreeditNothing: c_int = 0x0008; +#[allow(non_upper_case_globals)] +pub const XIMStatusNothing: c_int = 0x0400; + +#[allow(non_upper_case_globals)] +pub const XNClientWindow_0: &[u8] = b"clientWindow\0"; +#[allow(non_upper_case_globals)] +pub const XNInputStyle_0: &[u8] = b"inputStyle\0"; + +pub enum _XIC {} +pub enum _XIM {} +pub enum _XrmHashBucketRec {} + +#[allow(clippy::upper_case_acronyms)] +pub type XIC = *mut _XIC; +#[allow(clippy::upper_case_acronyms)] +pub type XIM = *mut _XIM; +pub type XrmDatabase = *mut _XrmHashBucketRec; + #[link(name = "X11")] extern "C" { pub fn XOpenDisplay(name: *const c_char) -> *mut Display; pub fn XCloseDisplay(display: *mut Display); - pub fn XLookupString( - event: *const XKeyEvent, - buffer_return: *mut c_char, - bytes_buffer: c_int, - keysym_return: *mut KeySym, - status_in_out: *const c_void, - ) -> c_int; pub fn XDefaultRootWindow(display: *mut Display) -> Window; pub fn XGetInputFocus( display: *mut Display, @@ -82,4 +96,31 @@ extern "C" { ) -> c_int; pub fn XSync(display: *mut Display, discard: c_int) -> c_int; pub fn XQueryKeymap(display: *mut Display, keys_return: *mut u8); + pub fn XOpenIM( + display: *mut Display, + db: XrmDatabase, + res_name: *mut c_char, + res_class: *mut c_char, + ) -> XIM; + pub fn XCreateIC( + input_method: XIM, + p2: *const u8, + p3: c_int, + p4: *const u8, + p5: c_int, + p6: *const c_void, + ) -> XIC; + pub fn XDestroyIC(input_context: XIC); + pub fn XmbResetIC(input_context: XIC) -> *mut c_char; + pub fn Xutf8LookupString( + input_context: XIC, + event: *mut XKeyEvent, + buffer: *mut c_char, + buff_size: c_int, + keysym_return: *mut c_ulong, + status_return: *mut c_int, + ) -> c_int; + pub fn XFilterEvent(event: *mut XKeyEvent, window: c_ulong) -> c_int; + pub fn XCloseIM(input_method: XIM) -> c_int; + pub fn XFree(data: *mut c_void) -> c_int; } diff --git a/espanso-inject/src/x11/mod.rs b/espanso-inject/src/x11/mod.rs index 7878fe5..84d4a6c 100644 --- a/espanso-inject/src/x11/mod.rs +++ b/espanso-inject/src/x11/mod.rs @@ -20,7 +20,7 @@ mod ffi; use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, ffi::{CStr, CString}, os::raw::c_char, slice, @@ -28,23 +28,29 @@ use std::{ use ffi::{ Display, KeyCode, KeyPress, KeyRelease, KeySym, Window, XCloseDisplay, XDefaultRootWindow, - XFlush, XFreeModifiermap, XGetInputFocus, XGetModifierMapping, XKeyEvent, XLookupString, - XQueryKeymap, XSendEvent, XSync, XTestFakeKeyEvent, + XFlush, XFreeModifiermap, XGetInputFocus, XGetModifierMapping, XKeyEvent, XQueryKeymap, + XSendEvent, XSync, XTestFakeKeyEvent, }; -use log::error; +use libc::c_void; +use log::{debug, error}; -use crate::linux::raw_keys::convert_to_sym_array; -use anyhow::Result; +use crate::{linux::raw_keys::convert_to_sym_array, x11::ffi::Xutf8LookupString}; +use anyhow::{bail, Result}; use thiserror::Error; use crate::{keys, InjectionOptions, Injector}; +use self::ffi::{ + XCloseIM, XCreateIC, XDestroyIC, XFilterEvent, XFree, XIMPreeditNothing, XIMStatusNothing, + XNClientWindow_0, XNInputStyle_0, XOpenIM, XmbResetIC, XIC, +}; + // Offset between evdev keycodes (where KEY_ESCAPE is 1), and the evdev XKB // keycode set (where ESC is 9). const EVDEV_OFFSET: u32 = 8; #[derive(Clone, Copy, Debug)] -struct KeyRecord { +struct KeyPair { // Keycode code: u32, // Modifier state which combined with the code produces the char @@ -52,6 +58,15 @@ struct KeyRecord { state: u32, } +#[derive(Clone, Copy, Debug)] +struct KeyRecord { + main: KeyPair, + + // Under some keyboard layouts (de, es), a deadkey + // press might be needed to generate the right char + preceding_dead_key: Option, +} + type CharMap = HashMap; type SymMap = HashMap; @@ -76,7 +91,7 @@ impl X11Injector { return Err(X11InjectorError::Init().into()); } - let (char_map, sym_map) = Self::generate_maps(display); + let (char_map, sym_map) = Self::generate_maps(display)?; Ok(Self { display, @@ -85,27 +100,169 @@ impl X11Injector { }) } - fn generate_maps(display: *mut Display) -> (CharMap, SymMap) { + fn generate_maps(display: *mut Display) -> Result<(CharMap, SymMap)> { + debug!("generating key maps"); + let mut char_map = HashMap::new(); let mut sym_map = HashMap::new(); - let root_window = unsafe { XDefaultRootWindow(display) }; + let input_method = unsafe { + XOpenIM( + display, + std::ptr::null_mut(), + std::ptr::null_mut(), + std::ptr::null_mut(), + ) + }; + if input_method.is_null() { + bail!("could not open input method"); + } + let _im_guard = scopeguard::guard((), |_| { + unsafe { XCloseIM(input_method) }; + }); + + let input_context = unsafe { + XCreateIC( + input_method, + XNInputStyle_0.as_ptr(), + XIMPreeditNothing | XIMStatusNothing, + XNClientWindow_0.as_ptr(), + 0, + std::ptr::null_mut(), + ) + }; + if input_context.is_null() { + bail!("could not open input context"); + } + let _ic_guard = scopeguard::guard((), |_| { + unsafe { XDestroyIC(input_context) }; + }); + + let deadkeys = Self::find_deadkeys(display, &input_context)?; + + // Cycle through all state/code combinations to populate the reverse lookup tables + for key_code in 0..256u32 { + for modifier_state in 0..256u32 { + for dead_key in deadkeys.iter() { + let code_with_offset = key_code + EVDEV_OFFSET; + + let preceding_dead_key = if let Some(dead_key) = dead_key { + let mut dead_key_event = XKeyEvent { + display, + keycode: dead_key.code, + state: dead_key.state, + + // These might not even need to be filled + window: 0, + root: 0, + same_screen: 1, + time: 0, + type_: KeyPress, + x_root: 1, + y_root: 1, + x: 1, + y: 1, + subwindow: 0, + serial: 0, + send_event: 0, + }; + + unsafe { XFilterEvent(&mut dead_key_event, 0) }; + + Some(*dead_key) + } else { + None + }; + + let mut key_event = XKeyEvent { + display, + keycode: code_with_offset, + state: modifier_state, + + // These might not even need to be filled + window: 0, + root: 0, + same_screen: 1, + time: 0, + type_: KeyPress, + x_root: 1, + y_root: 1, + x: 1, + y: 1, + subwindow: 0, + serial: 0, + send_event: 0, + }; + + unsafe { XFilterEvent(&mut key_event, 0) }; + let mut sym: KeySym = 0; + let mut buffer: [c_char; 10] = [0; 10]; + + let result = unsafe { + Xutf8LookupString( + input_context, + &mut key_event, + buffer.as_mut_ptr(), + (buffer.len() - 1) as i32, + &mut sym, + std::ptr::null_mut(), + ) + }; + + let key_record = KeyRecord { + main: KeyPair { + code: code_with_offset, + state: modifier_state, + }, + preceding_dead_key, + }; + + // Keysym was found + if sym != 0 { + sym_map.entry(sym).or_insert(key_record); + }; + + // Char was found + if result > 0 { + let raw_string = unsafe { CStr::from_ptr(buffer.as_ptr()) }; + let string = raw_string.to_string_lossy().to_string(); + char_map.entry(string).or_insert(key_record); + }; + + // We need to reset the context state to prevent + // deadkeys effect to propagate to the next combination + let _reset = unsafe { XmbResetIC(input_context) }; + unsafe { XFree(_reset as *mut c_void) }; + } + } + } + + debug!("Populated char_map with {} symbols", char_map.len()); + debug!("Populated sym_map with {} symbols", sym_map.len()); + debug!("Detected {} dead key combinations", deadkeys.len()); + + Ok((char_map, sym_map)) + } + + fn find_deadkeys(display: *mut Display, input_context: &XIC) -> Result>> { + let mut deadkeys = vec![None]; + let mut seen_keysyms: HashSet = HashSet::new(); // Cycle through all state/code combinations to populate the reverse lookup tables for key_code in 0..256u32 { for modifier_state in 0..256u32 { let code_with_offset = key_code + EVDEV_OFFSET; - let event = XKeyEvent { + let mut event = XKeyEvent { display, keycode: code_with_offset, state: modifier_state, // These might not even need to be filled - window: root_window, - root: root_window, + window: 0, + root: 0, same_screen: 1, time: 0, - type_: KeyRelease, + type_: KeyPress, x_root: 1, y_root: 1, x: 1, @@ -115,38 +272,38 @@ impl X11Injector { send_event: 0, }; - let mut sym: KeySym = 0; - let mut buffer: [c_char; 10] = [0; 10]; - let result = unsafe { - XLookupString( - &event, - buffer.as_mut_ptr(), - (buffer.len() - 1) as i32, - &mut sym, - std::ptr::null(), - ) - }; + let filter = unsafe { XFilterEvent(&mut event, 0) }; + if filter == 1 { + let mut sym: KeySym = 0; + let mut buffer: [c_char; 10] = [0; 10]; - let key_record = KeyRecord { - code: code_with_offset, - state: modifier_state, - }; + unsafe { + Xutf8LookupString( + *input_context, + &mut event, + buffer.as_mut_ptr(), + (buffer.len() - 1) as i32, + &mut sym, + std::ptr::null_mut(), + ) + }; - // Keysym was found - if sym != 0 { - sym_map.entry(sym).or_insert(key_record); + if sym != 0 && !seen_keysyms.contains(&sym) { + let key_record = KeyPair { + code: code_with_offset, + state: modifier_state, + }; + deadkeys.push(Some(key_record)); + seen_keysyms.insert(sym); + } } - // Char was found - if result > 0 { - let raw_string = unsafe { CStr::from_ptr(buffer.as_ptr()) }; - let string = raw_string.to_string_lossy().to_string(); - char_map.entry(string).or_insert(key_record); - } + let _reset = unsafe { XmbResetIC(*input_context) }; + unsafe { XFree(_reset as *mut c_void) }; } } - (char_map, sym_map) + Ok(deadkeys) } fn convert_to_record_array(&self, syms: &[KeySym]) -> Result> { @@ -202,12 +359,12 @@ impl X11Injector { // Render the state by applying the modifiers for (mod_index, modifier) in modifiers_codes.iter().enumerate() { - if modifier.contains(&(record.code as u8)) { + if modifier.contains(&(record.main.code as u8)) { current_state |= 1 << mod_index; } } - current_record.state = current_state; + current_record.main.state = current_state; records.push(current_record); } @@ -223,7 +380,7 @@ impl X11Injector { focused_window } - fn send_key(&self, window: Window, record: &KeyRecord, pressed: bool, delay_us: u32) { + fn send_key(&self, window: Window, record: &KeyPair, pressed: bool, delay_us: u32) { let root_window = unsafe { XDefaultRootWindow(self.display) }; let mut event = XKeyEvent { display: self.display, @@ -269,7 +426,7 @@ impl X11Injector { } } - fn xtest_send_key(&self, record: &KeyRecord, pressed: bool, delay_us: u32) { + fn xtest_send_key(&self, record: &KeyPair, pressed: bool, delay_us: u32) { // If the key requires any modifier, we need to send those events if record.state != 0 { self.xtest_send_modifiers(record.state, pressed); @@ -345,11 +502,21 @@ impl Injector for X11Injector { for record in records? { if options.disable_fast_inject { - self.xtest_send_key(&record, true, delay_us); - self.xtest_send_key(&record, false, delay_us); + if let Some(deadkey) = &record.preceding_dead_key { + self.xtest_send_key(deadkey, true, delay_us); + self.xtest_send_key(deadkey, false, delay_us); + } + + self.xtest_send_key(&record.main, true, delay_us); + self.xtest_send_key(&record.main, false, delay_us); } else { - self.send_key(focused_window, &record, true, delay_us); - self.send_key(focused_window, &record, false, delay_us); + if let Some(deadkey) = &record.preceding_dead_key { + self.send_key(focused_window, deadkey, true, delay_us); + self.send_key(focused_window, deadkey, false, delay_us); + } + + self.send_key(focused_window, &record.main, true, delay_us); + self.send_key(focused_window, &record.main, false, delay_us); } } @@ -371,11 +538,11 @@ impl Injector for X11Injector { for record in records { if options.disable_fast_inject { - self.xtest_send_key(&record, true, delay_us); - self.xtest_send_key(&record, false, delay_us); + self.xtest_send_key(&record.main, true, delay_us); + self.xtest_send_key(&record.main, false, delay_us); } else { - self.send_key(focused_window, &record, true, delay_us); - self.send_key(focused_window, &record, false, delay_us); + self.send_key(focused_window, &record.main, true, delay_us); + self.send_key(focused_window, &record.main, false, delay_us); } } @@ -401,18 +568,18 @@ impl Injector for X11Injector { // First press the keys for record in records.iter() { if options.disable_fast_inject { - self.xtest_send_key(record, true, delay_us); + self.xtest_send_key(&record.main, true, delay_us); } else { - self.send_key(focused_window, record, true, delay_us); + self.send_key(focused_window, &record.main, true, delay_us); } } // Then release them for record in records.iter().rev() { if options.disable_fast_inject { - self.xtest_send_key(record, false, delay_us); + self.xtest_send_key(&record.main, false, delay_us); } else { - self.send_key(focused_window, record, false, delay_us); + self.send_key(focused_window, &record.main, false, delay_us); } }