feat(ui): refactor windows notification system away from WinToast
This commit is contained in:
		
							parent
							
								
									f8b7300d31
								
							
						
					
					
						commit
						a8799c3d9a
					
				| 
						 | 
				
			
			@ -20,6 +20,8 @@ thiserror = "1.0.23"
 | 
			
		|||
[target.'cfg(windows)'.dependencies]
 | 
			
		||||
widestring = "0.4.3"
 | 
			
		||||
lazycell = "1.3.0"
 | 
			
		||||
winrt-notification = "0.3.1"
 | 
			
		||||
lazy_static = "1.4.0"
 | 
			
		||||
 | 
			
		||||
[target.'cfg(target_os="macos")'.dependencies]
 | 
			
		||||
lazycell = "1.3.0"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,10 +24,8 @@ fn cc_config() {
 | 
			
		|||
  cc::Build::new()
 | 
			
		||||
    .cpp(true)
 | 
			
		||||
    .include("src/win32/native.h")
 | 
			
		||||
    .include("src/win32/WinToast/wintoastlib.h")
 | 
			
		||||
    .include("src/win32/json/json.hpp")
 | 
			
		||||
    .file("src/win32/native.cpp")
 | 
			
		||||
    .file("src/win32/WinToast/wintoastlib.cpp")
 | 
			
		||||
    .compile("espansoui");
 | 
			
		||||
 | 
			
		||||
  println!("cargo:rustc-link-lib=static=espansoui");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +0,0 @@
 | 
			
		|||
In order to support native notifications on Windows, espanso wraps
 | 
			
		||||
WinToast, a great and lightweight C++ library.
 | 
			
		||||
More info: https://github.com/mohabouje/WinToast
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
				
			
			@ -1,217 +0,0 @@
 | 
			
		|||
/* * Copyright (C) 2016-2019 Mohammed Boujemaoui <mohabouje@gmail.com>
 | 
			
		||||
 *
 | 
			
		||||
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 | 
			
		||||
 * this software and associated documentation files (the "Software"), to deal in
 | 
			
		||||
 * the Software without restriction, including without limitation the rights to
 | 
			
		||||
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 | 
			
		||||
 * the Software, and to permit persons to whom the Software is furnished to do so,
 | 
			
		||||
 * subject to the following conditions:
 | 
			
		||||
 *
 | 
			
		||||
 * The above copyright notice and this permission notice shall be included in all
 | 
			
		||||
 * copies or substantial portions of the Software.
 | 
			
		||||
 *
 | 
			
		||||
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
			
		||||
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 | 
			
		||||
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 | 
			
		||||
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 | 
			
		||||
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 | 
			
		||||
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
#ifndef WINTOASTLIB_H
 | 
			
		||||
#define WINTOASTLIB_H
 | 
			
		||||
#include <Windows.h>
 | 
			
		||||
#include <sdkddkver.h>
 | 
			
		||||
#include <WinUser.h>
 | 
			
		||||
#include <ShObjIdl.h>
 | 
			
		||||
#include <wrl/implements.h>
 | 
			
		||||
#include <wrl/event.h>
 | 
			
		||||
#include <windows.ui.notifications.h>
 | 
			
		||||
#include <strsafe.h>
 | 
			
		||||
#include <Psapi.h>
 | 
			
		||||
#include <ShlObj.h>
 | 
			
		||||
#include <roapi.h>
 | 
			
		||||
#include <propvarutil.h>
 | 
			
		||||
#include <functiondiscoverykeys.h>
 | 
			
		||||
#include <iostream>
 | 
			
		||||
#include <winstring.h>
 | 
			
		||||
#include <string.h>
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include <map>
 | 
			
		||||
using namespace Microsoft::WRL;
 | 
			
		||||
using namespace ABI::Windows::Data::Xml::Dom;
 | 
			
		||||
using namespace ABI::Windows::Foundation;
 | 
			
		||||
using namespace ABI::Windows::UI::Notifications;
 | 
			
		||||
using namespace Windows::Foundation;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
namespace WinToastLib {
 | 
			
		||||
 | 
			
		||||
    class IWinToastHandler {
 | 
			
		||||
    public:
 | 
			
		||||
        enum WinToastDismissalReason {
 | 
			
		||||
            UserCanceled = ToastDismissalReason::ToastDismissalReason_UserCanceled,
 | 
			
		||||
            ApplicationHidden = ToastDismissalReason::ToastDismissalReason_ApplicationHidden,
 | 
			
		||||
            TimedOut = ToastDismissalReason::ToastDismissalReason_TimedOut
 | 
			
		||||
        };
 | 
			
		||||
        virtual ~IWinToastHandler() = default;
 | 
			
		||||
        virtual void toastActivated() const = 0;
 | 
			
		||||
        virtual void toastActivated(int actionIndex) const = 0;
 | 
			
		||||
        virtual void toastDismissed(WinToastDismissalReason state) const = 0;
 | 
			
		||||
        virtual void toastFailed() const = 0;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    class WinToastTemplate {
 | 
			
		||||
    public:
 | 
			
		||||
        enum Duration { System, Short, Long };
 | 
			
		||||
        enum AudioOption { Default = 0, Silent, Loop };
 | 
			
		||||
        enum TextField { FirstLine = 0, SecondLine, ThirdLine };
 | 
			
		||||
        enum WinToastTemplateType {
 | 
			
		||||
            ImageAndText01 = ToastTemplateType::ToastTemplateType_ToastImageAndText01,
 | 
			
		||||
            ImageAndText02 = ToastTemplateType::ToastTemplateType_ToastImageAndText02,
 | 
			
		||||
            ImageAndText03 = ToastTemplateType::ToastTemplateType_ToastImageAndText03,
 | 
			
		||||
            ImageAndText04 = ToastTemplateType::ToastTemplateType_ToastImageAndText04,
 | 
			
		||||
            Text01 = ToastTemplateType::ToastTemplateType_ToastText01,
 | 
			
		||||
            Text02 = ToastTemplateType::ToastTemplateType_ToastText02,
 | 
			
		||||
            Text03 = ToastTemplateType::ToastTemplateType_ToastText03,
 | 
			
		||||
            Text04 = ToastTemplateType::ToastTemplateType_ToastText04,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        enum AudioSystemFile {
 | 
			
		||||
            DefaultSound,
 | 
			
		||||
            IM, 
 | 
			
		||||
            Mail,
 | 
			
		||||
            Reminder, 
 | 
			
		||||
            SMS, 
 | 
			
		||||
            Alarm,
 | 
			
		||||
            Alarm2,
 | 
			
		||||
            Alarm3,
 | 
			
		||||
            Alarm4,
 | 
			
		||||
            Alarm5,
 | 
			
		||||
            Alarm6,
 | 
			
		||||
            Alarm7,
 | 
			
		||||
            Alarm8,
 | 
			
		||||
            Alarm9,
 | 
			
		||||
            Alarm10,
 | 
			
		||||
            Call,
 | 
			
		||||
            Call1,
 | 
			
		||||
            Call2,
 | 
			
		||||
            Call3,
 | 
			
		||||
            Call4,
 | 
			
		||||
            Call5,
 | 
			
		||||
            Call6,
 | 
			
		||||
            Call7,
 | 
			
		||||
            Call8,
 | 
			
		||||
            Call9,
 | 
			
		||||
            Call10,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        WinToastTemplate(_In_ WinToastTemplateType type = WinToastTemplateType::ImageAndText02);
 | 
			
		||||
        ~WinToastTemplate();
 | 
			
		||||
 | 
			
		||||
        void setFirstLine(_In_ const std::wstring& text);
 | 
			
		||||
        void setSecondLine(_In_ const std::wstring& text);
 | 
			
		||||
        void setThirdLine(_In_ const std::wstring& text);
 | 
			
		||||
        void setTextField(_In_ const std::wstring& txt, _In_ TextField pos);
 | 
			
		||||
        void setAttributionText(_In_ const std::wstring & attributionText);
 | 
			
		||||
        void setImagePath(_In_ const std::wstring& imgPath);
 | 
			
		||||
        void setAudioPath(_In_ WinToastTemplate::AudioSystemFile audio);
 | 
			
		||||
        void setAudioPath(_In_ const std::wstring& audioPath);
 | 
			
		||||
        void setAudioOption(_In_ WinToastTemplate::AudioOption audioOption);
 | 
			
		||||
        void setDuration(_In_ Duration duration);
 | 
			
		||||
        void setExpiration(_In_ INT64 millisecondsFromNow);
 | 
			
		||||
        void addAction(_In_ const std::wstring& label);
 | 
			
		||||
 | 
			
		||||
        std::size_t textFieldsCount() const;
 | 
			
		||||
        std::size_t actionsCount() const;
 | 
			
		||||
        bool hasImage() const;
 | 
			
		||||
        const std::vector<std::wstring>& textFields() const;
 | 
			
		||||
        const std::wstring& textField(_In_ TextField pos) const;
 | 
			
		||||
        const std::wstring& actionLabel(_In_ std::size_t pos) const;
 | 
			
		||||
        const std::wstring& imagePath() const;
 | 
			
		||||
        const std::wstring& audioPath() const;
 | 
			
		||||
        const std::wstring& attributionText() const;
 | 
			
		||||
        INT64 expiration() const;
 | 
			
		||||
        WinToastTemplateType type() const;
 | 
			
		||||
        WinToastTemplate::AudioOption audioOption() const;
 | 
			
		||||
        Duration duration() const;
 | 
			
		||||
    private:
 | 
			
		||||
        std::vector<std::wstring>			_textFields{};
 | 
			
		||||
        std::vector<std::wstring>           _actions{};
 | 
			
		||||
        std::wstring                        _imagePath{};
 | 
			
		||||
        std::wstring                        _audioPath{};
 | 
			
		||||
        std::wstring                        _attributionText{};
 | 
			
		||||
        INT64                               _expiration{0};
 | 
			
		||||
        AudioOption                         _audioOption{WinToastTemplate::AudioOption::Default};
 | 
			
		||||
        WinToastTemplateType                _type{WinToastTemplateType::Text01};
 | 
			
		||||
        Duration                            _duration{Duration::System};
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    class WinToast {
 | 
			
		||||
    public:
 | 
			
		||||
        enum WinToastError {
 | 
			
		||||
            NoError = 0,
 | 
			
		||||
            NotInitialized,
 | 
			
		||||
            SystemNotSupported,
 | 
			
		||||
            ShellLinkNotCreated,
 | 
			
		||||
            InvalidAppUserModelID,
 | 
			
		||||
            InvalidParameters,
 | 
			
		||||
            InvalidHandler,
 | 
			
		||||
            NotDisplayed,
 | 
			
		||||
            UnknownError
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        enum ShortcutResult {
 | 
			
		||||
            SHORTCUT_UNCHANGED = 0,
 | 
			
		||||
            SHORTCUT_WAS_CHANGED = 1,
 | 
			
		||||
            SHORTCUT_WAS_CREATED = 2,
 | 
			
		||||
 | 
			
		||||
            SHORTCUT_MISSING_PARAMETERS = -1,
 | 
			
		||||
            SHORTCUT_INCOMPATIBLE_OS = -2,
 | 
			
		||||
            SHORTCUT_COM_INIT_FAILURE = -3,
 | 
			
		||||
            SHORTCUT_CREATE_FAILED = -4
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        WinToast(void);
 | 
			
		||||
        virtual ~WinToast();
 | 
			
		||||
        static WinToast* instance();
 | 
			
		||||
        static bool isCompatible();
 | 
			
		||||
		static bool	isSupportingModernFeatures();
 | 
			
		||||
		static std::wstring configureAUMI(_In_ const std::wstring& companyName,
 | 
			
		||||
                                          _In_ const std::wstring& productName,
 | 
			
		||||
                                          _In_ const std::wstring& subProduct = std::wstring(),
 | 
			
		||||
                                          _In_ const std::wstring& versionInformation = std::wstring());
 | 
			
		||||
        static const std::wstring& strerror(_In_ WinToastError error);
 | 
			
		||||
        virtual bool initialize(_Out_ WinToastError* error = nullptr);
 | 
			
		||||
        virtual bool isInitialized() const;
 | 
			
		||||
        virtual bool hideToast(_In_ INT64 id);
 | 
			
		||||
        virtual INT64 showToast(_In_ const WinToastTemplate& toast, _In_ IWinToastHandler* handler, _Out_ WinToastError* error = nullptr);
 | 
			
		||||
        virtual void clear();
 | 
			
		||||
        virtual enum ShortcutResult createShortcut();
 | 
			
		||||
 | 
			
		||||
        const std::wstring& appName() const;
 | 
			
		||||
        const std::wstring& appUserModelId() const;
 | 
			
		||||
        void setAppUserModelId(_In_ const std::wstring& aumi);
 | 
			
		||||
        void setAppName(_In_ const std::wstring& appName);
 | 
			
		||||
 | 
			
		||||
    protected:
 | 
			
		||||
        bool											_isInitialized{false};
 | 
			
		||||
        bool                                            _hasCoInitialized{false};
 | 
			
		||||
        std::wstring                                    _appName{};
 | 
			
		||||
        std::wstring                                    _aumi{};
 | 
			
		||||
        std::map<INT64, ComPtr<IToastNotification>>     _buffer{};
 | 
			
		||||
 | 
			
		||||
        HRESULT validateShellLinkHelper(_Out_ bool& wasChanged);
 | 
			
		||||
        HRESULT createShellLinkHelper();
 | 
			
		||||
        HRESULT setImageFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& path);
 | 
			
		||||
        HRESULT setAudioFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& path, _In_opt_ WinToastTemplate::AudioOption option = WinToastTemplate::AudioOption::Default);
 | 
			
		||||
        HRESULT setTextFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& text, _In_ UINT32 pos);
 | 
			
		||||
        HRESULT setAttributionTextFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& text);
 | 
			
		||||
        HRESULT addActionHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& action, _In_ const std::wstring& arguments);
 | 
			
		||||
        HRESULT addDurationHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& duration);
 | 
			
		||||
        ComPtr<IToastNotifier> notifier(_In_ bool* succeded) const;
 | 
			
		||||
        void setError(_Out_ WinToastError* error, _In_ WinToastError value);
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
#endif // WINTOASTLIB_H
 | 
			
		||||
| 
						 | 
				
			
			@ -22,6 +22,7 @@ use std::{
 | 
			
		|||
  collections::HashMap,
 | 
			
		||||
  ffi::{c_void, CString},
 | 
			
		||||
  os::raw::c_char,
 | 
			
		||||
  path::PathBuf,
 | 
			
		||||
  sync::{
 | 
			
		||||
    atomic::{AtomicPtr, Ordering},
 | 
			
		||||
    Arc,
 | 
			
		||||
| 
						 | 
				
			
			@ -29,6 +30,8 @@ use std::{
 | 
			
		|||
  thread::ThreadId,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
mod notification;
 | 
			
		||||
 | 
			
		||||
use anyhow::Result;
 | 
			
		||||
use lazycell::LazyCell;
 | 
			
		||||
use log::{error, trace};
 | 
			
		||||
| 
						 | 
				
			
			@ -51,8 +54,6 @@ pub struct RawUIOptions {
 | 
			
		|||
 | 
			
		||||
  pub icon_paths: [[u16; MAX_FILE_PATH]; MAX_ICON_COUNT],
 | 
			
		||||
  pub icon_paths_count: i32,
 | 
			
		||||
 | 
			
		||||
  pub notification_icon_path: [u16; MAX_FILE_PATH],
 | 
			
		||||
}
 | 
			
		||||
// Take a look at the native.h header file for an explanation of the fields
 | 
			
		||||
#[repr(C)]
 | 
			
		||||
| 
						 | 
				
			
			@ -76,7 +77,6 @@ extern "C" {
 | 
			
		|||
  pub fn ui_destroy(window_handle: *const c_void) -> i32;
 | 
			
		||||
  pub fn ui_exit(window_handle: *const c_void) -> i32;
 | 
			
		||||
  pub fn ui_update_tray_icon(window_handle: *const c_void, index: i32);
 | 
			
		||||
  pub fn ui_show_notification(window_handle: *const c_void, message: *const u16);
 | 
			
		||||
  pub fn ui_show_context_menu(window_handle: *const c_void, payload: *const c_char);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -102,13 +102,12 @@ pub fn create(options: Win32UIOptions) -> Result<(Win32Remote, Win32EventLoop)>
 | 
			
		|||
    icons.push(path.clone());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let eventloop = Win32EventLoop::new(
 | 
			
		||||
    handle.clone(),
 | 
			
		||||
    icons,
 | 
			
		||||
    options.show_icon,
 | 
			
		||||
    options.notification_icon_path,
 | 
			
		||||
  let eventloop = Win32EventLoop::new(handle.clone(), icons, options.show_icon);
 | 
			
		||||
  let remote = Win32Remote::new(
 | 
			
		||||
    handle,
 | 
			
		||||
    icon_indexes,
 | 
			
		||||
    PathBuf::from(options.notification_icon_path),
 | 
			
		||||
  );
 | 
			
		||||
  let remote = Win32Remote::new(handle, icon_indexes);
 | 
			
		||||
 | 
			
		||||
  Ok((remote, eventloop))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -118,7 +117,6 @@ pub struct Win32EventLoop {
 | 
			
		|||
 | 
			
		||||
  show_icon: bool,
 | 
			
		||||
  icons: Vec<String>,
 | 
			
		||||
  notification_icon_path: String,
 | 
			
		||||
 | 
			
		||||
  // Internal
 | 
			
		||||
  _event_callback: LazyCell<UIEventCallback>,
 | 
			
		||||
| 
						 | 
				
			
			@ -126,17 +124,11 @@ pub struct Win32EventLoop {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
impl Win32EventLoop {
 | 
			
		||||
  pub(crate) fn new(
 | 
			
		||||
    handle: Arc<AtomicPtr<c_void>>,
 | 
			
		||||
    icons: Vec<String>,
 | 
			
		||||
    show_icon: bool,
 | 
			
		||||
    notification_icon_path: String,
 | 
			
		||||
  ) -> Self {
 | 
			
		||||
  pub(crate) fn new(handle: Arc<AtomicPtr<c_void>>, icons: Vec<String>, show_icon: bool) -> Self {
 | 
			
		||||
    Self {
 | 
			
		||||
      handle,
 | 
			
		||||
      icons,
 | 
			
		||||
      show_icon,
 | 
			
		||||
      notification_icon_path,
 | 
			
		||||
      _event_callback: LazyCell::new(),
 | 
			
		||||
      _init_thread_id: LazyCell::new(),
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -160,17 +152,10 @@ impl UIEventLoop for Win32EventLoop {
 | 
			
		|||
      icon_path[0..len].clone_from_slice(&wide_path.as_slice()[..len]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let wide_notification_icon_path =
 | 
			
		||||
      widestring::WideCString::from_str(&self.notification_icon_path)?;
 | 
			
		||||
    let mut wide_notification_icon_path_buffer: [u16; MAX_FILE_PATH] = [0; MAX_FILE_PATH];
 | 
			
		||||
    wide_notification_icon_path_buffer[..wide_notification_icon_path.as_slice().len()]
 | 
			
		||||
      .clone_from_slice(wide_notification_icon_path.as_slice());
 | 
			
		||||
 | 
			
		||||
    let options = RawUIOptions {
 | 
			
		||||
      show_icon: if self.show_icon { 1 } else { 0 },
 | 
			
		||||
      icon_paths,
 | 
			
		||||
      icon_paths_count: self.icons.len() as i32,
 | 
			
		||||
      notification_icon_path: wide_notification_icon_path_buffer,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let mut error_code = 0;
 | 
			
		||||
| 
						 | 
				
			
			@ -190,12 +175,6 @@ impl UIEventLoop for Win32EventLoop {
 | 
			
		|||
          )
 | 
			
		||||
          .into(),
 | 
			
		||||
        ),
 | 
			
		||||
        -3 => Err(
 | 
			
		||||
          Win32UIError::EventLoopInitError(
 | 
			
		||||
            "Unable to initialize Win32EventLoop, initializing notifications".to_string(),
 | 
			
		||||
          )
 | 
			
		||||
          .into(),
 | 
			
		||||
        ),
 | 
			
		||||
        _ => Err(
 | 
			
		||||
          Win32UIError::EventLoopInitError(
 | 
			
		||||
            "Unable to initialize Win32EventLoop, unknown error".to_string(),
 | 
			
		||||
| 
						 | 
				
			
			@ -284,7 +263,12 @@ impl Win32Remote {
 | 
			
		|||
  pub(crate) fn new(
 | 
			
		||||
    handle: Arc<AtomicPtr<c_void>>,
 | 
			
		||||
    icon_indexes: HashMap<TrayIcon, usize>,
 | 
			
		||||
    notification_icon_path: PathBuf,
 | 
			
		||||
  ) -> Self {
 | 
			
		||||
    if let Err(err) = notification::initialize_notification_thread(notification_icon_path) {
 | 
			
		||||
      error!("unable to initialize notification thread: {}", err);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Self {
 | 
			
		||||
      handle,
 | 
			
		||||
      icon_indexes,
 | 
			
		||||
| 
						 | 
				
			
			@ -308,21 +292,8 @@ impl UIRemote for Win32Remote {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  fn show_notification(&self, message: &str) {
 | 
			
		||||
    let handle = self.handle.load(Ordering::Acquire);
 | 
			
		||||
    if handle.is_null() {
 | 
			
		||||
      error!("Unable to show notification, pointer is null");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let wide_message = widestring::WideCString::from_str(message);
 | 
			
		||||
    match wide_message {
 | 
			
		||||
      Ok(wide_message) => unsafe { ui_show_notification(handle, wide_message.as_ptr()) },
 | 
			
		||||
      Err(error) => {
 | 
			
		||||
        error!(
 | 
			
		||||
          "Unable to show notification, invalid message encoding {}",
 | 
			
		||||
          error
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    if let Err(err) = notification::show_notification(message) {
 | 
			
		||||
      error!("Unable to show notification: {}", err);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,16 +43,12 @@
 | 
			
		|||
 | 
			
		||||
#include <Windows.h>
 | 
			
		||||
 | 
			
		||||
#include "WinToast/wintoastlib.h"
 | 
			
		||||
using namespace WinToastLib;
 | 
			
		||||
 | 
			
		||||
#include "json/json.hpp"
 | 
			
		||||
using json = nlohmann::json;
 | 
			
		||||
 | 
			
		||||
#define APPWM_ICON_CLICK (WM_APP + 1)
 | 
			
		||||
#define APPWM_SHOW_CONTEXT_MENU (WM_APP + 2)
 | 
			
		||||
#define APPWM_UPDATE_TRAY_ICON (WM_APP + 3)
 | 
			
		||||
#define APPWM_SHOW_NOTIFICATION (WM_APP + 4)
 | 
			
		||||
 | 
			
		||||
const wchar_t *const ui_winclass = L"EspansoUI";
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -61,7 +57,6 @@ typedef struct
 | 
			
		|||
  UIOptions options;
 | 
			
		||||
  NOTIFYICONDATA nid;
 | 
			
		||||
  HICON g_icons[MAX_ICON_COUNT];
 | 
			
		||||
  wchar_t notification_icon_path[MAX_FILE_PATH];
 | 
			
		||||
 | 
			
		||||
  // Rust interop
 | 
			
		||||
  void *rust_instance;
 | 
			
		||||
| 
						 | 
				
			
			@ -71,16 +66,6 @@ typedef struct
 | 
			
		|||
// Needed to detect when Explorer crashes
 | 
			
		||||
UINT WM_TASKBARCREATED = RegisterWindowMessage(L"TaskbarCreated");
 | 
			
		||||
 | 
			
		||||
// Notification handler using: https://mohabouje.github.io/WinToast/
 | 
			
		||||
class EspansoNotificationHandler : public IWinToastHandler
 | 
			
		||||
{
 | 
			
		||||
public:
 | 
			
		||||
  void toastActivated() const {}
 | 
			
		||||
  void toastActivated(int actionIndex) const {}
 | 
			
		||||
  void toastDismissed(WinToastDismissalReason state) const {}
 | 
			
		||||
  void toastFailed() const {}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * Message handler procedure for the window
 | 
			
		||||
 */
 | 
			
		||||
| 
						 | 
				
			
			@ -154,19 +139,6 @@ LRESULT CALLBACK ui_window_procedure(HWND window, unsigned int msg, WPARAM wp, L
 | 
			
		|||
 | 
			
		||||
    break;
 | 
			
		||||
  }
 | 
			
		||||
  case APPWM_SHOW_NOTIFICATION:
 | 
			
		||||
  {
 | 
			
		||||
    std::unique_ptr<wchar_t> message(reinterpret_cast<wchar_t *>(lp));
 | 
			
		||||
 | 
			
		||||
    std::cout << "hello" << variables->notification_icon_path << std::endl;
 | 
			
		||||
 | 
			
		||||
    WinToastTemplate templ = WinToastTemplate(WinToastTemplate::ImageAndText02);
 | 
			
		||||
    templ.setImagePath(variables->notification_icon_path);
 | 
			
		||||
    templ.setTextField(L"Espanso", WinToastTemplate::FirstLine);
 | 
			
		||||
    templ.setTextField(message.get(), WinToastTemplate::SecondLine);
 | 
			
		||||
    WinToast::instance()->showToast(templ, new EspansoNotificationHandler());
 | 
			
		||||
    break;
 | 
			
		||||
  }
 | 
			
		||||
  case APPWM_ICON_CLICK: // Click on the tray icon
 | 
			
		||||
  {
 | 
			
		||||
    switch (lp)
 | 
			
		||||
| 
						 | 
				
			
			@ -240,7 +212,6 @@ void *ui_initialize(void *_self, UIOptions _options, int32_t *error_code)
 | 
			
		|||
      UIVariables *variables = new UIVariables();
 | 
			
		||||
      variables->options = _options;
 | 
			
		||||
      variables->rust_instance = _self;
 | 
			
		||||
      wcscpy(variables->notification_icon_path, _options.notification_icon_path);
 | 
			
		||||
      SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast<::LONG_PTR>(variables));
 | 
			
		||||
 | 
			
		||||
      // Load the tray icons
 | 
			
		||||
| 
						 | 
				
			
			@ -252,11 +223,11 @@ void *ui_initialize(void *_self, UIOptions _options, int32_t *error_code)
 | 
			
		|||
      // Hide the window
 | 
			
		||||
      ShowWindow(window, SW_HIDE);
 | 
			
		||||
 | 
			
		||||
      // Setup the icon in the notification space
 | 
			
		||||
      // Setup the icon in the tray space
 | 
			
		||||
      SendMessage(window, WM_SETICON, ICON_BIG, (LPARAM)variables->g_icons[0]);
 | 
			
		||||
      SendMessage(window, WM_SETICON, ICON_SMALL, (LPARAM)variables->g_icons[0]);
 | 
			
		||||
 | 
			
		||||
      // Notification
 | 
			
		||||
      // Tray icon 
 | 
			
		||||
      variables->nid.cbSize = sizeof(variables->nid);
 | 
			
		||||
      variables->nid.hWnd = window;
 | 
			
		||||
      variables->nid.uID = 1;
 | 
			
		||||
| 
						 | 
				
			
			@ -283,16 +254,6 @@ void *ui_initialize(void *_self, UIOptions _options, int32_t *error_code)
 | 
			
		|||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Initialize the notification handler
 | 
			
		||||
  WinToast::instance()->setAppName(L"Espanso");
 | 
			
		||||
  const auto aumi = WinToast::configureAUMI(L"federico.terzi", L"Espanso", L"Core", L"1.0.0");
 | 
			
		||||
  WinToast::instance()->setAppUserModelId(aumi);
 | 
			
		||||
  if (!WinToast::instance()->initialize())
 | 
			
		||||
  {
 | 
			
		||||
    *error_code = -3;
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return window;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -337,17 +298,6 @@ void ui_update_tray_icon(void *window, int32_t index)
 | 
			
		|||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int32_t ui_show_notification(void *window, wchar_t *message)
 | 
			
		||||
{
 | 
			
		||||
  if (window)
 | 
			
		||||
  {
 | 
			
		||||
    wchar_t *message_copy = _wcsdup(message);
 | 
			
		||||
    PostMessage((HWND)window, APPWM_SHOW_NOTIFICATION, 0, (LPARAM)message_copy);
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
  return -1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Menu related methods
 | 
			
		||||
 | 
			
		||||
void _insert_separator_menu(HMENU parent)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,7 +35,6 @@ typedef struct {
 | 
			
		|||
 | 
			
		||||
  wchar_t icon_paths[MAX_ICON_COUNT][MAX_FILE_PATH];
 | 
			
		||||
  int32_t icon_paths_count;
 | 
			
		||||
  wchar_t notification_icon_path[MAX_FILE_PATH];
 | 
			
		||||
} UIOptions;
 | 
			
		||||
 | 
			
		||||
typedef struct {
 | 
			
		||||
| 
						 | 
				
			
			@ -61,9 +60,6 @@ extern "C" void ui_exit(void * window);
 | 
			
		|||
// the icon within the UIOptions.icon_paths array.
 | 
			
		||||
extern "C" void ui_update_tray_icon(void * window, int32_t index);
 | 
			
		||||
 | 
			
		||||
// Show a native Windows 10 notification
 | 
			
		||||
extern "C" int32_t ui_show_notification(void * window, wchar_t * message);
 | 
			
		||||
 | 
			
		||||
// Display the context menu on the tray icon.
 | 
			
		||||
// Payload is passed as JSON as given the complex structure, parsing
 | 
			
		||||
// this manually would have been complex.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										102
									
								
								espanso-ui/src/win32/notification.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								espanso-ui/src/win32/notification.rs
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,102 @@
 | 
			
		|||
/*
 | 
			
		||||
 * This file is part of espanso.
 | 
			
		||||
 *
 | 
			
		||||
 * Copyright (C) 2019-2021 Federico Terzi
 | 
			
		||||
 *
 | 
			
		||||
 * espanso is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * espanso is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with espanso.  If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
use std::{path::{PathBuf}, sync::{Arc, Mutex, mpsc::{Sender, channel}}};
 | 
			
		||||
 | 
			
		||||
use anyhow::{Result, anyhow, bail};
 | 
			
		||||
use lazy_static::lazy_static;
 | 
			
		||||
use log::{error, warn};
 | 
			
		||||
use std::process::Command;
 | 
			
		||||
use winrt_notification::{IconCrop, Toast};
 | 
			
		||||
use std::os::windows::process::CommandExt;
 | 
			
		||||
 | 
			
		||||
const ESPANSO_APP_USER_MODEL_ID: &str = "{5E3B6C0F-1A4D-45C4-8872-D8174702101A}";
 | 
			
		||||
 | 
			
		||||
lazy_static! {
 | 
			
		||||
  static ref SEND_CHANNEL: Arc<Mutex<Option<Sender<String>>>> = Arc::new(Mutex::new(None));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn initialize_notification_thread(notification_icon_path: PathBuf) -> Result<()> {
 | 
			
		||||
  let (sender, receiver) = channel::<String>();
 | 
			
		||||
  
 | 
			
		||||
  {
 | 
			
		||||
    let mut lock = SEND_CHANNEL.lock().map_err(|e| anyhow!("failed to define shared notification sender: {}", e))?;
 | 
			
		||||
    *lock = Some(sender);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  std::thread::Builder::new().name("notification-thread".to_string()).spawn(move || {
 | 
			
		||||
    // First determine which AppUserModelID we can use
 | 
			
		||||
    lazy_static! {
 | 
			
		||||
      static ref APP_USER_MODEL_ID: &'static str = if is_espanso_app_user_model_id_set() {
 | 
			
		||||
        ESPANSO_APP_USER_MODEL_ID
 | 
			
		||||
      } else {
 | 
			
		||||
        warn!("unable to find espanso AppUserModelID in the list of registered ones, falling back to Powershell");
 | 
			
		||||
        Toast::POWERSHELL_APP_ID
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    while let Ok(message) = receiver.recv() {
 | 
			
		||||
      if let Err(err) = Toast::new(&APP_USER_MODEL_ID)
 | 
			
		||||
        .icon(¬ification_icon_path, IconCrop::Square, "Espanso")
 | 
			
		||||
        .title("Espanso")
 | 
			
		||||
        .text1(&message)
 | 
			
		||||
        .sound(None)
 | 
			
		||||
        .show()
 | 
			
		||||
        .map_err(|e| anyhow!("failed to show notification: {}", e)) {    
 | 
			
		||||
          error!("unable to show notification: {}", err); 
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  })?;
 | 
			
		||||
 | 
			
		||||
  Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn show_notification(msg: &str) -> Result<()> {
 | 
			
		||||
  let mut lock = SEND_CHANNEL.lock().map_err(|e| anyhow!("unable to acquire notification send channel: {}", e))?;
 | 
			
		||||
  match &mut *lock {
 | 
			
		||||
    Some(sender) => {
 | 
			
		||||
      sender.send(msg.to_string())?;
 | 
			
		||||
      Ok(())
 | 
			
		||||
    },
 | 
			
		||||
    None => bail!("notification sender not available"),
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn is_espanso_app_user_model_id_set() -> bool {
 | 
			
		||||
  match Command::new("powershell")
 | 
			
		||||
    .args(&["-c", "get-startapps"])
 | 
			
		||||
    .creation_flags(0x08000000)
 | 
			
		||||
    .output()
 | 
			
		||||
  {
 | 
			
		||||
    Ok(output) => {
 | 
			
		||||
      let output_str = String::from_utf8_lossy(&output.stdout);
 | 
			
		||||
      // Check if espanso is present
 | 
			
		||||
      output_str
 | 
			
		||||
        .lines()
 | 
			
		||||
        .any(|line| line.contains(ESPANSO_APP_USER_MODEL_ID))
 | 
			
		||||
    }
 | 
			
		||||
    Err(err) => {
 | 
			
		||||
      error!(
 | 
			
		||||
        "unable to determine if AppUserModelID was registered: {}",
 | 
			
		||||
        err
 | 
			
		||||
      );
 | 
			
		||||
      false
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user