Repository: espanso/espanso (language: rust)

Issue:
Froms window size is excceding the screen
Hi,

I noticed that if you want to replace a trigger with a From consisting of a long sentence, then the size of the Form Window would exceed the size of the screen.

I tried to add the following to the default.yml, but it didn't work out:
    form_size:
      width: 400
      height: 300

Is there any feature to wrap the text within the Form without creating new lines.

Thank you for your time.

Best,
- George


Discussion / hints:
The only workaround I could find was to completely detach what's shown on the Espanso form window from what's pasted by Espanso when I'm done filling out a form.

I was reading the form documentation looking for answers when I got to [this section about controls with verbose syntax](https://espanso.org/docs/matches/forms/#controls-with-the-verbose-syntax). It uses `replace` instead of `form` right after the `trigger` parameter, and it controls what's displayed on the Espanso window through a `layout` parameter.

I was able to use `\n` to add line breaks on the text displayed by Espanso, and that decreased the window size significantly. It isn't exactly what you want (automatic text wrap—that's exactly what I wanted too), but it's an okay-ish short to medium term solution while text wrap isn't implemented.
I added some information about [Form sizes](https://espanso.org/docs/matches/forms/#form-sizes) recently, but I agree, some direct control of the size would be useful.

Fix patch:
diff --git a/espanso-config/src/config/mod.rs b/espanso-config/src/config/mod.rs
index 0b4890b44..ba0ed3a22 100644
--- a/espanso-config/src/config/mod.rs
+++ b/espanso-config/src/config/mod.rs
@@ -156,6 +156,12 @@ pub trait Config: Send + Sync {
   // not be targeted to the right application.
   fn post_form_delay(&self) -> usize;
 
+  // The maximum width that a form window can take.
+  fn max_form_width(&self) -> usize;
+
+  // The maximum height that a form window can take.
+  fn max_form_height(&self) -> usize;
+
   // The number of milliseconds to wait after the search bar has been closed.
   // This is useful to let the target application regain focus
   // after the search bar has been closed, otherwise the injection might
@@ -204,7 +210,7 @@ pub trait Config: Send + Sync {
         key_delay: {:?}
         apply_patch: {:?}
         word_separators: {:?}
-        
+
         preserve_clipboard: {:?}
         clipboard_threshold: {:?}
         disable_x11_fast_inject: {}
@@ -212,9 +218,11 @@ pub trait Config: Send + Sync {
         paste_shortcut_event_delay: {}
         toggle_key: {:?}
         auto_restart: {:?}
-        restore_clipboard_delay: {:?} 
-        post_form_delay: {:?} 
-        post_search_delay: {:?} 
+        restore_clipboard_delay: {:?}
+        post_form_delay: {:?}
+        max_form_width: {:?}
+        max_form_height: {:?}
+        post_search_delay: {:?}
         backspace_limit: {}
         search_trigger: {:?}
         search_shortcut: {:?}
@@ -230,7 +238,7 @@ pub trait Config: Send + Sync {
         win32_keyboard_layout_cache_interval: {:?}
 
         match_paths: {:#?}
-      ", 
+      ",
       self.label(),
       self.backend(),
       self.enable(),
@@ -249,6 +257,8 @@ pub trait Config: Send + Sync {
       self.auto_restart(),
       self.restore_clipboard_delay(),
       self.post_form_delay(),
+      self.max_form_width(),
+      self.max_form_height(),
       self.post_search_delay(),
       self.backspace_limit(),
       self.search_trigger(),
diff --git a/espanso-config/src/config/parse/mod.rs b/espanso-config/src/config/parse/mod.rs
index c641684f2..e2d7791d0 100644
--- a/espanso-config/src/config/parse/mod.rs
+++ b/espanso-config/src/config/parse/mod.rs
@@ -45,6 +45,8 @@ pub(crate) struct ParsedConfig {
   pub show_icon: Option<bool>,
   pub secure_input_notification: Option<bool>,
   pub post_form_delay: Option<usize>,
+  pub max_form_width: Option<usize>,
+  pub max_form_height: Option<usize>,
   pub post_search_delay: Option<usize>,
   pub emulate_alt_codes: Option<bool>,
   pub win32_exclude_orphan_events: Option<bool>,
diff --git a/espanso-config/src/config/parse/yaml.rs b/espanso-config/src/config/parse/yaml.rs
index 9beb82fdf..c2e728952 100644
--- a/espanso-config/src/config/parse/yaml.rs
+++ b/espanso-config/src/config/parse/yaml.rs
@@ -106,6 +106,12 @@ pub(crate) struct YAMLConfig {
   #[serde(default)]
   pub post_form_delay: Option<usize>,
 
+  #[serde(default)]
+  pub max_form_width: Option<usize>,
+
+  #[serde(default)]
+  pub max_form_height: Option<usize>,
+
   #[serde(default)]
   pub post_search_delay: Option<usize>,
 
@@ -214,6 +220,8 @@ impl TryFrom<YAMLConfig> for ParsedConfig {
       restore_clipboard_delay: yaml_config.restore_clipboard_delay,
       paste_shortcut_event_delay: yaml_config.paste_shortcut_event_delay,
       post_form_delay: yaml_config.post_form_delay,
+      max_form_width: yaml_config.max_form_width,
+      max_form_height: yaml_config.max_form_height,
       post_search_delay: yaml_config.post_search_delay,
 
       emulate_alt_codes: yaml_config.emulate_alt_codes,
@@ -278,19 +286,21 @@ mod tests {
     show_notifications: false
     secure_input_notification: false
     post_form_delay: 300
+    max_form_width: 700
+    max_form_height: 500
     post_search_delay: 400
     emulate_alt_codes: true
     win32_exclude_orphan_events: false
     win32_keyboard_layout_cache_interval: 300
     x11_use_xclip_backend: true
     x11_use_xdotool_backend: true
-      
+
     use_standard_includes: true
     includes: ["test1"]
     extra_includes: ["test2"]
     excludes: ["test3"]
     extra_excludes: ["test4"]
-    
+
     filter_class: "test5"
     filter_exec: "test6"
     filter_os: "test7"
@@ -337,6 +347,8 @@ mod tests {
         secure_input_notification: Some(false),
         emulate_alt_codes: Some(true),
         post_form_delay: Some(300),
+        max_form_width: Some(700),
+        max_form_height: Some(500),
         post_search_delay: Some(400),
         win32_exclude_orphan_events: Some(false),
         win32_keyboard_layout_cache_interval: Some(300),
diff --git a/espanso-config/src/config/resolve.rs b/espanso-config/src/config/resolve.rs
index 2ff937b62..22c780218 100644
--- a/espanso-config/src/config/resolve.rs
+++ b/espanso-config/src/config/resolve.rs
@@ -326,6 +326,14 @@ impl Config for ResolvedConfig {
       .unwrap_or(DEFAULT_POST_FORM_DELAY)
   }
 
+  fn max_form_width(&self) -> usize {
+    self.parsed.max_form_width.unwrap_or(700)
+  }
+
+  fn max_form_height(&self) -> usize {
+    self.parsed.max_form_height.unwrap_or(500)
+  }
+
   fn post_search_delay(&self) -> usize {
     self
       .parsed
@@ -438,6 +446,8 @@ impl ResolvedConfig {
       secure_input_notification,
       emulate_alt_codes,
       post_form_delay,
+      max_form_width,
+      max_form_height,
       post_search_delay,
       win32_exclude_orphan_events,
       win32_keyboard_layout_cache_interval,
diff --git a/espanso-config/src/legacy/mod.rs b/espanso-config/src/legacy/mod.rs
index e1156c9da..01f5923c0 100644
--- a/espanso-config/src/legacy/mod.rs
+++ b/espanso-config/src/legacy/mod.rs
@@ -391,6 +391,14 @@ impl Config for LegacyInteropConfig {
     crate::config::default::DEFAULT_POST_FORM_DELAY
   }
 
+  fn max_form_width(&self) -> usize {
+    700
+  }
+
+  fn max_form_height(&self) -> usize {
+    500
+  }
+
   fn post_search_delay(&self) -> usize {
     crate::config::default::DEFAULT_POST_SEARCH_DELAY
   }
@@ -604,7 +612,7 @@ mod tests {
       matches:
         - trigger: "hello"
           replace: "world"
-        
+
         - trigger: "withvars"
           replace: "{{output}}"
           vars:
@@ -687,7 +695,7 @@ mod tests {
       std::fs::write(
         packages.join("test-package").join("package.yml"),
         r#"
-      name: test-package 
+      name: test-package
       parent: default
 
       matches:
diff --git a/espanso-modulo/src/form/config.rs b/espanso-modulo/src/form/config.rs
index 2fd101191..461a58ddc 100644
--- a/espanso-modulo/src/form/config.rs
+++ b/espanso-modulo/src/form/config.rs
@@ -32,6 +32,14 @@ fn default_fields() -> HashMap<String, FieldConfig> {
   HashMap::new()
 }
 
+fn default_max_form_width() -> i32 {
+  700
+}
+
+fn default_max_form_height() -> i32 {
+  500
+}
+
 #[derive(Debug, Serialize, Deserialize, Clone)]
 pub struct FormConfig {
   #[serde(default = "default_title")]
@@ -44,6 +52,12 @@ pub struct FormConfig {
 
   #[serde(default = "default_fields")]
   pub fields: HashMap<String, FieldConfig>,
+
+  #[serde(default = "default_max_form_width")]
+  pub max_form_width: i32,
+
+  #[serde(default = "default_max_form_height")]
+  pub max_form_height: i32,
 }
 
 #[derive(Debug, Serialize, Clone)]
diff --git a/espanso-modulo/src/form/generator.rs b/espanso-modulo/src/form/generator.rs
index 7003d5ccb..e0a670cc0 100644
--- a/espanso-modulo/src/form/generator.rs
+++ b/espanso-modulo/src/form/generator.rs
@@ -97,5 +97,7 @@ fn build_form(form: FormConfig, structure: Vec<Vec<Token>>) -> Form {
     title: form.title,
     icon: form.icon,
     fields,
+    max_form_width: form.max_form_width,
+    max_form_height: form.max_form_height,
   }
 }
diff --git a/espanso-modulo/src/sys/form/form.cpp b/espanso-modulo/src/sys/form/form.cpp
index fecf807e5..0efe3d71a 100644
--- a/espanso-modulo/src/sys/form/form.cpp
+++ b/espanso-modulo/src/sys/form/form.cpp
@@ -108,10 +108,16 @@ enum
 
 bool FormApp::OnInit()
 {
-    FormFrame *frame = new FormFrame(wxString::FromUTF8(formMetadata->windowTitle), wxPoint(50, 50), wxSize(450, 340) );
+    const wxSize& maxFormSize = wxSize(formMetadata->maxWindowWidth, formMetadata->maxWindowHeight);
+    FormFrame *frame = new FormFrame(
+        wxString::FromUTF8(formMetadata->windowTitle),
+        wxPoint(50, 50),
+        maxFormSize
+    );
+    frame->SetMaxSize(maxFormSize);
     setFrameIcon(wxString::FromUTF8(formMetadata->iconPath), frame);
     frame->Show( true );
-    
+
     Activate(frame);
 
     return true;
@@ -154,7 +160,10 @@ void FormFrame::AddComponent(wxPanel *parent, wxBoxSizer *sizer, FieldMetadata m
         case FieldType::LABEL:
         {
             const LabelMetadata *labelMeta = static_cast<const LabelMetadata*>(meta.specific);
-            auto label = new wxStaticText(parent, wxID_ANY, wxString::FromUTF8(labelMeta->text), wxDefaultPosition, wxDefaultSize);
+            const long style = wxST_ELLIPSIZE_END;
+            auto label = new wxStaticText(parent, wxID_ANY, wxString::FromUTF8(labelMeta->text), wxDefaultPosition, wxDefaultSize, style);
+
+            label->Wrap(this->GetClientSize().GetWidth());
             control = label;
             fields.push_back(label);
             break;
@@ -168,7 +177,7 @@ void FormFrame::AddComponent(wxPanel *parent, wxBoxSizer *sizer, FieldMetadata m
             }
 
             auto textControl = new wxTextCtrl(parent, NewControlId(), wxString::FromUTF8(textMeta->defaultText), wxDefaultPosition, wxDefaultSize, style);
-            
+
             if (textMeta->multiline) {
                 textControl->SetMinSize(wxSize(MULTILINE_MIN_WIDTH, MULTILINE_MIN_HEIGHT));
                 textControl->Bind(wxEVT_SET_FOCUS, &FormFrame::HandleMultilineFocus, this, wxID_ANY);
@@ -212,7 +221,7 @@ void FormFrame::AddComponent(wxPanel *parent, wxBoxSizer *sizer, FieldMetadata m
                 idMap[meta.id] = std::move(field);
             }else {
                 choice = (void*) new wxListBox(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, choices);
-                
+
                 if (selectedItem >= 0) {
                     ((wxListBox*)choice)->SetSelection(selectedItem);
                 }
@@ -223,12 +232,12 @@ void FormFrame::AddComponent(wxPanel *parent, wxBoxSizer *sizer, FieldMetadata m
                 // when the enter key is pressed).
                 // See: https://github.com/espanso/espanso/issues/857
                 ((wxListBox*)choice)->Bind(wxEVT_LISTBOX_DCLICK, &FormFrame::OnListBoxEvent, this, wxID_ANY);
-                
+
                 // Create the field wrapper
                 std::unique_ptr<FieldWrapper> field((FieldWrapper*) new ListFieldWrapper((wxListBox*) choice));
                 idMap[meta.id] = std::move(field);
             }
-            
+
             control = choice;
             fields.push_back(choice);
             break;
@@ -325,7 +334,7 @@ extern "C" void interop_show_form(FormMetadata * _metadata, void (*callback)(Val
     #ifdef __WXMSW__
         SetProcessDPIAware();
     #endif
-    
+
     formMetadata = _metadata;
 
     wxApp::SetInstance(new FormApp());
@@ -339,4 +348,4 @@ extern "C" void interop_show_form(FormMetadata * _metadata, void (*callback)(Val
         free((void*) pair.id);
         free((void*) pair.value);
     }
-}
\ No newline at end of file
+}
diff --git a/espanso-modulo/src/sys/form/mod.rs b/espanso-modulo/src/sys/form/mod.rs
index 04af33c35..cfcae8641 100644
--- a/espanso-modulo/src/sys/form/mod.rs
+++ b/espanso-modulo/src/sys/form/mod.rs
@@ -29,6 +29,8 @@ pub mod types {
     pub title: String,
     pub icon: Option<String>,
     pub fields: Vec<Field>,
+    pub max_form_width: i32,
+    pub max_form_height: i32,
   }
 
   #[derive(Debug)]
@@ -136,11 +138,16 @@ mod interop {
         std::ptr::null()
       };
 
+      let max_form_width = form.max_form_width;
+      let max_form_height = form.max_form_height;
+
       let interop = Box::new(FormMetadata {
         windowTitle: title.as_ptr(),
         iconPath: icon_path_ptr,
         fields: metadata.as_ptr(),
         fieldSize: fields.len() as c_int,
+        maxWindowWidth: max_form_width,
+        maxWindowHeight: max_form_height,
       });
 
       Self {
diff --git a/espanso-modulo/src/sys/interop/interop.h b/espanso-modulo/src/sys/interop/interop.h
index a9510fe66..7ace6858f 100644
--- a/espanso-modulo/src/sys/interop/interop.h
+++ b/espanso-modulo/src/sys/interop/interop.h
@@ -64,6 +64,8 @@ typedef struct FormMetadata {
   const char *iconPath;
   const FieldMetadata *fields;
   const int fieldSize;
+  const int maxWindowWidth;
+  const int maxWindowHeight;
 } FormMetadata;
 
 typedef struct ValuePair {
@@ -104,14 +106,14 @@ const int DETECTED_OS_WAYLAND = 2;
 typedef struct WizardMetadata {
   const char *version;
 
-  const int is_welcome_page_enabled; 
-  const int is_move_bundle_page_enabled; 
-  const int is_legacy_version_page_enabled; 
-  const int is_wrong_edition_page_enabled; 
-  const int is_migrate_page_enabled; 
-  const int is_auto_start_page_enabled; 
-  const int is_add_path_page_enabled; 
-  const int is_accessibility_page_enabled; 
+  const int is_welcome_page_enabled;
+  const int is_move_bundle_page_enabled;
+  const int is_legacy_version_page_enabled;
+  const int is_wrong_edition_page_enabled;
+  const int is_migrate_page_enabled;
+  const int is_auto_start_page_enabled;
+  const int is_add_path_page_enabled;
+  const int is_accessibility_page_enabled;
 
   const char *window_icon_path;
   const char *welcome_image_path;
diff --git a/espanso-modulo/src/sys/interop/mod.rs b/espanso-modulo/src/sys/interop/mod.rs
index e93c8783c..c6a61cdb3 100644
--- a/espanso-modulo/src/sys/interop/mod.rs
+++ b/espanso-modulo/src/sys/interop/mod.rs
@@ -76,6 +76,8 @@ pub struct FormMetadata {
   pub iconPath: *const ::std::os::raw::c_char,
   pub fields: *const FieldMetadata,
   pub fieldSize: ::std::os::raw::c_int,
+  pub maxWindowWidth: ::std::os::raw::c_int,
+  pub maxWindowHeight: ::std::os::raw::c_int,
 }
 
 #[repr(C)]
diff --git a/espanso/src/cli/worker/config.rs b/espanso/src/cli/worker/config.rs
index 8960173d0..7ee4c9ef7 100644
--- a/espanso/src/cli/worker/config.rs
+++ b/espanso/src/cli/worker/config.rs
@@ -204,6 +204,14 @@ impl<'a> crate::gui::modulo::form::ModuloFormUIOptionProvider for ConfigManager<
   fn get_post_form_delay(&self) -> usize {
     self.active().post_form_delay()
   }
+
+  fn get_max_form_width(&self) -> usize {
+    self.active().max_form_width()
+  }
+
+  fn get_max_form_height(&self) -> usize {
+    self.active().max_form_height()
+  }
 }
 
 impl<'a> crate::gui::modulo::search::ModuloSearchUIOptionProvider for ConfigManager<'a> {
diff --git a/espanso/src/gui/modulo/form.rs b/espanso/src/gui/modulo/form.rs
index c483a1353..213203e5f 100644
--- a/espanso/src/gui/modulo/form.rs
+++ b/espanso/src/gui/modulo/form.rs
@@ -28,6 +28,8 @@ use super::manager::ModuloManager;
 
 pub trait ModuloFormUIOptionProvider {
   fn get_post_form_delay(&self) -> usize;
+  fn get_max_form_width(&self) -> usize;
+  fn get_max_form_height(&self) -> usize;
 }
 
 pub struct ModuloFormUI<'a> {
@@ -57,6 +59,8 @@ impl<'a> FormUI for ModuloFormUI<'a> {
       title: "espanso",
       layout,
       fields: convert_fields_into_object(fields),
+      max_form_width: self.option_provider.get_max_form_width(),
+      max_form_height: self.option_provider.get_max_form_height(),
     };
 
     let json_config = serde_json::to_string(&modulo_form_config)?;
@@ -91,6 +95,8 @@ struct ModuloFormConfig<'a> {
   title: &'a str,
   layout: &'a str,
   fields: Map<String, Value>,
+  max_form_width: usize,
+  max_form_height: usize,
 }
 
 // TODO: test
diff --git a/espanso/src/patch/patches/mod.rs b/espanso/src/patch/patches/mod.rs
index 30c29eb50..66f2bd199 100644
--- a/espanso/src/patch/patches/mod.rs
+++ b/espanso/src/patch/patches/mod.rs
@@ -49,6 +49,8 @@ generate_patchable_config!(
   apply_patch -> bool,
   undo_backspace -> bool,
   post_form_delay -> usize,
+  max_form_width -> usize,
+  max_form_height -> usize,
   post_search_delay -> usize,
   emulate_alt_codes -> bool,
   win32_exclude_orphan_events -> bool,
diff --git a/schemas/config.schema.json b/schemas/config.schema.json
index d02c8231d..7624ee0fc 100644
--- a/schemas/config.schema.json
+++ b/schemas/config.schema.json
@@ -210,6 +210,16 @@
             "default": 200,
             "description": "Delay (in ms) returning text after closing a form, to allow the target application regain focus."
         },
+        "max_form_width": {
+            "type": "number",
+            "default": 700,
+            "description": "Maximum width of a form window."
+        },
+        "max_form_height": {
+            "type": "number",
+            "default": 500,
+            "description": "Maximum height of a form window."
+        },
         "post_search_delay": {
             "type": "number",
             "default": 200,
@@ -238,4 +248,4 @@
     },
     "additionalProperties": false,
     "type": "object"
-}
\ No newline at end of file
+}


Test patch: