Exemple #1
0
        public static async Task <bool> DarkMode(FunctionArgs args)
        {
            bool on = args["state"] == "on";

            Setting appSetting = App.Current.MorphicSession.Solutions.GetSetting(SettingId.LightThemeApps);
            await appSetting.SetValue(!on);

            return(true);
        }
Exemple #2
0
        public static async Task <bool> Screenshot(FunctionArgs args)
        {
            // Hide all application windows
            Dictionary <Window, double> opacity = new Dictionary <Window, double>();
            HashSet <Window>            visible = new HashSet <Window>();

            try
            {
                foreach (Window window in App.Current.Windows)
                {
                    if (window is BarWindow || window is QuickHelpWindow)
                    {
                        if (window.AllowsTransparency)
                        {
                            opacity[window] = window.Opacity;
                            window.Opacity  = 0;
                        }
                        else
                        {
                            visible.Add(window);
                            window.Visibility = Visibility.Collapsed;
                        }
                    }
                }

                // Give enough time for the windows to disappear
                await Task.Delay(500);

                // Hold down the windows key while pressing shift + s
                const uint windowsKey = 0x5b; // VK_LWIN
                Morphic.Windows.Native.Keyboard.PressKey(windowsKey, true);
                System.Windows.Forms.SendKeys.SendWait("+s");
                Morphic.Windows.Native.Keyboard.PressKey(windowsKey, false);
            }
            finally
            {
                // Give enough time for snip tool to grab the screen without the morphic UI.
                await Task.Delay(3000);

                // Restore the windows
                foreach ((Window window, double o) in opacity)
                {
                    window.Opacity = o;
                }

                foreach (Window window in visible)
                {
                    window.Visibility = Visibility.Visible;
                }
            }

            return(true);
        }
        /// <summary>
        /// Invokes a built-in function.
        /// </summary>
        /// <param name="functionName">The function name.</param>
        /// <param name="functionArgs">The parameters.</param>
        /// <returns></returns>
        public Task <bool> InvokeFunction(string functionName, Dictionary <string, string> functionArgs)
        {
            App.Current.Logger.LogDebug($"Invoking built-in function '{functionName}'");

            Task <bool> result;

            if (this.all.TryGetValue(functionName.ToLowerInvariant(),
                                     out InternalFunctionAttribute? functionAttribute))
            {
                FunctionArgs args = new FunctionArgs(functionAttribute, functionArgs);
                result = functionAttribute.Function(args);
            }
            else
            {
                throw new ActionException($"No internal function found for '{functionName}");
            }

            return(result);
        }
Exemple #4
0
        public static Task <bool> Volume(FunctionArgs args)
        {
            IntPtr taskTray = WinApi.FindWindow("Shell_TrayWnd", IntPtr.Zero);

            if (taskTray != IntPtr.Zero)
            {
                int action = args["direction"] == "up"
                    ? WinApi.APPCOMMAND_VOLUME_UP
                    : WinApi.APPCOMMAND_VOLUME_DOWN;

                // Each command moves the volume by 2 notches.
                int times = Math.Clamp(Convert.ToInt32(args["amount"]), 1, 20) / 2;
                for (int n = 0; n < times; n++)
                {
                    WinApi.SendMessage(taskTray, WinApi.WM_APPCOMMAND, IntPtr.Zero,
                                       (IntPtr)WinApi.MakeLong(0, (short)action));
                }
            }

            return(Task.FromResult(true));
        }
Exemple #5
0
        public async static Task <MorphicResult <MorphicUnit, MorphicUnit> > ShowMenuAsync(FunctionArgs args)
        {
            // NOTE: this internal function is only called by the MorphicBar's Morphie menu button
            await App.Current.ShowMenuAsync(null, Morphic.Client.Menu.MorphicMenu.MenuOpenedSource.morphicBarIcon);

            return(MorphicResult.OkResult());
        }
Exemple #6
0
        public static async Task <MorphicResult <MorphicUnit, MorphicUnit> > EjectAllUsbDrivesAsync(FunctionArgs args)
        {
            App.Current.Logger.LogError("EjectAllUsbDrives");

            var getRemovableDisksAndDrivesResult = await Functions.GetRemovableDisksAndDrivesAsync();

            if (getRemovableDisksAndDrivesResult.IsError == true)
            {
                Debug.Assert(false, "Could not get list of removable disks");
                App.Current.Logger.LogError("Could not get list of removable disks");
                return(MorphicResult.ErrorResult());
            }
            var removableDisks = getRemovableDisksAndDrivesResult.Value !.RemovableDisks;

            // now eject all the removable disks
            var allDisksRemoved = true;

            foreach (var disk in removableDisks)
            {
                App.Current.Logger.LogError("Safely ejecting drive");

                // NOTE: "safe eject" in this circumstance means to safely eject the usb device (removing it from the PnP system, not physically ejecting media)
                var safeEjectResult = disk.SafelyRemoveDevice();
                if (safeEjectResult.IsError == true)
                {
                    allDisksRemoved = false;
                }

                // wait 50ms between ejection
                await Task.Delay(50);
            }

            if (allDisksRemoved == false)
            {
                return(MorphicResult.ErrorResult());
            }

            return(allDisksRemoved ? MorphicResult.OkResult() : MorphicResult.ErrorResult());
        }
Exemple #7
0
        public static async Task <MorphicResult <MorphicUnit, MorphicUnit> > OpenAllUsbDrivesAsync(FunctionArgs args)
        {
            App.Current.Logger.LogError("OpenAllUsbDrives");

            var getRemovableDisksAndDrivesResult = await Functions.GetRemovableDisksAndDrivesAsync();

            if (getRemovableDisksAndDrivesResult.IsError == true)
            {
                Debug.Assert(false, "Could not get list of removable drives");
                App.Current.Logger.LogError("Could not get list of removable drives");
                return(MorphicResult.ErrorResult());
            }
            var removableDrives = getRemovableDisksAndDrivesResult.Value !.RemovableDrives;

            // as we only want to open usb drives which are mounted (i.e. not USB drives which have had their "media" ejected but who still have drive letters assigned)...
            var mountedRemovableDrives = new List <Morphic.WindowsNative.Devices.Drive>();

            foreach (var drive in removableDrives)
            {
                var getIsMountedResult = await drive.GetIsMountedAsync();

                if (getIsMountedResult.IsError == true)
                {
                    Debug.Assert(false, "Could not determine if drive is mounted");
                    App.Current.Logger.LogError("Could not determine if drive is mounted");
                    // gracefully degrade; skip this disk
                    continue;
                }
                var driveIsMounted = getIsMountedResult.Value !;

                if (driveIsMounted)
                {
                    mountedRemovableDrives.Add(drive);
                }
            }

            // now open all the *mounted* removable disks
            foreach (var drive in mountedRemovableDrives)
            {
                // get the drive's root path (e.g. "E:\"); note that we intentionally get the root path WITH the backslash so that we don't launch autoplay, etc.
                var tryGetDriveRootPathResult = await drive.TryGetDriveRootPathAsync();

                if (tryGetDriveRootPathResult.IsError == true)
                {
                    Debug.Assert(false, "Could not get removable drive's root path");
                    App.Current.Logger.LogError("Could not get removable drive's root path");
                    // gracefully degrade; skip this disk
                    continue;
                }
                var driveRootPath = tryGetDriveRootPathResult.Value !;

                // NOTE: there is also an API call which may be able to do this more directly
                // see: https://docs.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shopenfolderandselectitems

                // NOTE: we might also consider getting the current process for Explorer.exe and then asking it to "explore" the drive

                App.Current.Logger.LogError("Opening USB drive");

                Process.Start(new ProcessStartInfo()
                {
                    FileName        = driveRootPath,
                    UseShellExecute = true
                });
            }

            return(MorphicResult.OkResult());
        }
Exemple #8
0
        public static async Task <MorphicResult <MorphicUnit, MorphicUnit> > SignOutAsync(FunctionArgs args)
        {
            var success = Morphic.WindowsNative.WindowsSession.WindowsSession.LogOff();

            return(success ? MorphicResult.OkResult() : MorphicResult.ErrorResult());
        }
Exemple #9
0
        public static async Task <MorphicResult <MorphicUnit, MorphicUnit> > SendKeysAsync(FunctionArgs args)
        {
            await SelectionReader.Default.ActivateLastActiveWindow();

            System.Windows.Forms.SendKeys.SendWait(args["keys"]);
            return(MorphicResult.OkResult());
        }
Exemple #10
0
        public static async Task <MorphicResult <MorphicUnit, MorphicUnit> > ScreenSnipAsync(FunctionArgs args)
        {
            // Hide all application windows
            Dictionary <Window, double> opacity = new Dictionary <Window, double>();
            HashSet <Window>            visible = new HashSet <Window>();

            try
            {
                foreach (Window window in App.Current.Windows)
                {
                    if (window is BarWindow || window is QuickHelpWindow)
                    {
                        if (window.AllowsTransparency)
                        {
                            opacity[window] = window.Opacity;
                            window.Opacity  = 0;
                        }
                        else
                        {
                            visible.Add(window);
                            window.Visibility = Visibility.Collapsed;
                        }
                    }
                }

                // Give enough time for the windows to disappear
                await Task.Delay(500);

                //// method 1: hold down the windows key while pressing shift + s
                //// NOTE: this method does not seem to work when we have uiAccess set to true in our manifest (oddly)
                //const uint windowsKey = 0x5b; // VK_LWIN
                //Keyboard.PressKey(windowsKey, true);
                //System.Windows.Forms.SendKeys.SendWait("+s");
                //Keyboard.PressKey(windowsKey, false);

                // method 2: open up the special windows URI of ms-screenclip:
                var openPath = "ms-screenclip:";
                Process.Start(new ProcessStartInfo(openPath)
                {
                    UseShellExecute = true
                });
            }
            finally
            {
                // Give enough time for snip tool to grab the screen without the morphic UI.
                await Task.Delay(3000);

                // Restore the windows
                foreach ((Window window, double o) in opacity)
                {
                    window.Opacity = o;
                }

                foreach (Window window in visible)
                {
                    window.Visibility = Visibility.Visible;
                }
            }

            return(MorphicResult.OkResult());
        }
Exemple #11
0
        public static async Task <bool> ReadAloud(FunctionArgs args)
        {
            string action = args["action"];

            switch (action)
            {
            case "pause":
                App.Current.Logger.LogError("ReadAloud: pause not supported");
                break;

            case "stop":
            case "play":
                Functions.speechPlayer?.Stop();
                Functions.speechPlayer?.Dispose();
                Functions.speechPlayer = null;

                if (action == "stop")
                {
                    break;
                }

                App.Current.Logger.LogDebug("ReadAloud: Storing clipboard");
                IDataObject?clipboardData = Clipboard.GetDataObject();
                Dictionary <string, object?>?dataStored = null;
                if (clipboardData != null)
                {
                    dataStored = clipboardData.GetFormats()
                                 .ToDictionary(format => format, format => (object?)clipboardData.GetData(format, false));
                }

                Clipboard.Clear();

                // Get the selection
                App.Current.Logger.LogDebug("ReadAloud: Getting selected text");
                await SelectionReader.Default.GetSelectedText(System.Windows.Forms.SendKeys.SendWait);

                string text = Clipboard.GetText();

                // Restore the clipboard
                App.Current.Logger.LogDebug("ReadAloud: Restoring clipboard");
                Clipboard.Clear();
                dataStored?.Where(kv => kv.Value != null).ToList()
                .ForEach(kv => Clipboard.SetData(kv.Key, kv.Value));

                // Talk the talk
                SpeechSynthesizer     synth  = new SpeechSynthesizer();
                SpeechSynthesisStream stream = await synth.SynthesizeTextToStreamAsync(text);

                speechPlayer = new SoundPlayer(stream.AsStream());
                speechPlayer.LoadCompleted += (o, args) =>
                {
                    speechPlayer.Play();
                };

                speechPlayer.LoadAsync();

                break;
            }

            return(true);
        }
Exemple #12
0
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
        public static async Task <MorphicResult <MorphicUnit, MorphicUnit> > SetVolumeAsync(FunctionArgs args)
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
        {
            // NOTE: ideally we should switch this functionality to use AudioEndpoint.SetMasterVolumeLevel instead
            //       [it may not be practical to do that, however, if AudioEndpoint.SetMasterVolumeLevel doesn't activate to on-screen volume change dialog in Windows 10/11; to be tested...]

            var percent = Convert.ToUInt32(args["amount"]);

            // clean up the percent argument (if it's not even, is out of range, etc.)
            if (percent % 2 != 0)
            {
                percent += 1;
            }
            percent = System.Math.Clamp(percent, 0, 100);

            if (args["direction"] == "up")
            {
                var sendCommandResult = Morphic.WindowsNative.Audio.Utils.VolumeUtils.SendVolumeUpCommand(percent);
                if (sendCommandResult.IsError == true)
                {
                    return(MorphicResult.ErrorResult());
                }
            }
            else
            {
                var sendCommandResult = Morphic.WindowsNative.Audio.Utils.VolumeUtils.SendVolumeDownCommand(percent);
                if (sendCommandResult.IsError == true)
                {
                    return(MorphicResult.ErrorResult());
                }
            }

            return(MorphicResult.OkResult());
        }
Exemple #13
0
        public static async Task <MorphicResult <MorphicUnit, MorphicUnit> > VolumeMuteAsync(FunctionArgs args)
        {
            bool newState;

            if (args.Arguments.Keys.Contains("state"))
            {
                newState = (args["state"] == "on");
            }
            else
            {
                var getMuteStateResult = Functions.GetMuteState();
                if (getMuteStateResult.IsSuccess == true)
                {
                    newState = getMuteStateResult.Value !;
                }
                else
                {
                    // if we cannot get the current value, gracefully degrade (i.e. assume that the volume is not muted)
                    newState = false;
                }
            }

            // set the mute state to the new state value
            var getDefaultAudioOutputEndpointResult = Morphic.WindowsNative.Audio.AudioEndpoint.GetDefaultAudioOutputEndpoint();

            if (getDefaultAudioOutputEndpointResult.IsError == true)
            {
                return(MorphicResult.ErrorResult());
            }
            var audioEndpoint = getDefaultAudioOutputEndpointResult.Value !;

            var setMasterMuteStateResult = audioEndpoint.SetMasterMuteState(newState);

            if (setMasterMuteStateResult.IsError == true)
            {
                return(MorphicResult.ErrorResult());
            }

            return(MorphicResult.OkResult());
        }
Exemple #14
0
 public static async Task <MorphicResult <MorphicUnit, MorphicUnit> > VolumeDownAsync(FunctionArgs args)
 {
     args.Arguments.Add("direction", "down");
     args.Arguments.Add("amount", "6");
     return(await SetVolumeAsync(args));
 }
Exemple #15
0
 public static Task <bool> ShowMenu(FunctionArgs args)
 {
     App.Current.ShowMenu();
     return(Task.FromResult(true));
 }
Exemple #16
0
        public static async Task <MorphicResult <MorphicUnit, MorphicUnit> > DarkModeAsync(FunctionArgs args)
        {
            // if we have a "value" property, this is a multi-segmented button and we should use "value" instead of "state"
            bool on;

            if (args.Arguments.Keys.Contains("value"))
            {
                on = (args["value"] == "on");
            }
            else if (args.Arguments.Keys.Contains("state"))
            {
                on = (args["state"] == "on");
            }
            else
            {
                System.Diagnostics.Debug.Assert(false, "Function 'darkMode' did not receive a new state");
                on = false;
            }

            var setDarkModeStateResult = await Functions.SetDarkModeStateAsync(on);

            if (setDarkModeStateResult.IsError == true)
            {
                return(MorphicResult.ErrorResult());
            }

            return(MorphicResult.OkResult());
        }
Exemple #17
0
        public static async Task <MorphicResult <MorphicUnit, MorphicUnit> > ToggleBasicWordRibbonAsync(FunctionArgs args)
        {
            // if we have a "value" property, this is a multi-segmented button and we should use "value" instead of "state"
            bool on;

            if (args.Arguments.Keys.Contains("value"))
            {
                on = (args["value"] == "on");
            }
            else if (args.Arguments.Keys.Contains("state"))
            {
                on = (args["state"] == "on");
            }
            else
            {
                System.Diagnostics.Debug.Assert(false, "Function 'basicWordRibbon' did not receive a new state");
                on = false;
            }

            if (Functions.IsSafeToModifyRibbonFile_WarnUser() == false)
            {
                // Word is running, so we are choosing not to execute this function
                return(MorphicResult.ErrorResult());
            }

            if (on == true)
            {
                var enableRibbonResult = Morphic.Integrations.Office.WordRibbon.EnableBasicSimplifyRibbon();
                return(enableRibbonResult.IsSuccess ? MorphicResult.OkResult() : MorphicResult.ErrorResult());
            }
            else
            {
                var disableRibbonResult = Morphic.Integrations.Office.WordRibbon.DisableBasicSimplifyRibbon();
                return(disableRibbonResult.IsSuccess ? MorphicResult.OkResult() : MorphicResult.ErrorResult());
            }
        }
Exemple #18
0
        public static async Task <MorphicResult <MorphicUnit, MorphicUnit> > ReadAloudAsync(FunctionArgs args)
        {
            string action = args["action"];

            switch (action)
            {
            case "pause":
                App.Current.Logger.LogError("ReadAloud: pause not supported.");

                return(MorphicResult.ErrorResult());

            case "stop":
                App.Current.Logger.LogDebug("ReadAloud: Stop reading selected text.");
                TextToSpeechHelper.Instance.Stop();

                return(MorphicResult.OkResult());

            case "play":
                string?selectedText = null;

                try
                {
                    App.Current.Logger.LogDebug("ReadAloud: Getting selected text.");

                    // activate the target window (i.e. topmost/last-active window, rather than the MorphicBar); we will then capture the current selection in that window
                    // NOTE: ideally we would activate the last window as part of our atomic operation, but we really have no control over whether or not another application
                    //       or the user changes the activated window (and our internal code is also not set up to block us from moving activation/focus temporarily).
                    await SelectionReader.Default.ActivateLastActiveWindow();

                    // as a primary strategy, try using the built-in Windows functionality for capturing the current selection via UI automation
                    // NOTE: this does not work with some apps (such as Internet Explorer...but also others)
                    bool captureTextViaAutomationSucceeded = false;
                    //
                    TextPatternRange[]? textRangeCollection = null;
                    //
                    // capture (or wait on) our "capture text" semaphore; we'll release this in the finally block
                    await s_captureTextSemaphore.WaitAsync();

                    //
                    try
                    {
                        var focusedElement = AutomationElement.FocusedElement;
                        if (focusedElement is not null)
                        {
                            object?pattern = null;
                            if (focusedElement.TryGetCurrentPattern(TextPattern.Pattern, out pattern))
                            {
                                if ((pattern is not null) && (pattern is TextPattern textPattern))
                                {
                                    // App.Current.Logger.LogDebug("ReadAloud: Capturing select text range(s).");

                                    // get the collection of text ranges in the selection; note that this can be a disjoint collection if multiple disjoint items were selected
                                    textRangeCollection = textPattern.GetSelection();
                                }
                            }
                            else
                            {
                                App.Current.Logger.LogDebug("ReadAloud: Selected element is not text.");
                            }
                        }
                        else
                        {
                            App.Current.Logger.LogDebug("ReadAloud: No element is currently selected.");
                        }
                    }
                    finally
                    {
                        s_captureTextSemaphore.Release();
                    }
                    //
                    // if we just captured a text range collection (i.e. were able to copy the current selection), convert that capture into a string now
                    StringBuilder?selectedTextBuilder = null;
                    if (textRangeCollection is not null)
                    {
                        // we have captured a range (presumably either an empty or non-empty selection)
                        selectedTextBuilder = new StringBuilder();

                        // append each text range
                        foreach (var textRange in textRangeCollection)
                        {
                            if (textRange is not null)
                            {
                                selectedTextBuilder.Append(textRange.GetText(-1 /* maximumRange */));
                            }
                        }

                        //if (selectedTextBuilder is not null /* && stringBuilder.Length > 0 */)
                        //{
                        selectedText = selectedTextBuilder.ToString();
                        captureTextViaAutomationSucceeded = true;

                        if (selectedText != String.Empty)
                        {
                            App.Current.Logger.LogDebug("ReadAloud: Captured selected text.");
                        }
                        else
                        {
                            App.Current.Logger.LogDebug("ReadAloud: Captured empty selection.");
                        }
                        //}
                    }

                    // as a backup strategy, use the clipboard and send ctrl+c to the target window to capture the text contents (while preserving as much of the previous
                    // clipboard's contents as possible); this is necessary in Internet Explorer and some other programs
                    if (captureTextViaAutomationSucceeded == false)
                    {
                        // capture (or wait on) our "capture text" semaphore; we'll release this in the finally block
                        await s_captureTextSemaphore.WaitAsync();

                        //
                        try
                        {
                            // App.Current.Logger.LogDebug("ReadAloud: Attempting to back up current clipboard.");

                            Dictionary <String, object?> clipboardContentsToRestore = new Dictionary <string, object?>();

                            var previousClipboardData = Clipboard.GetDataObject();
                            if (previousClipboardData is not null)
                            {
                                // App.Current.Logger.LogDebug("ReadAloud: Current clipboard has contents; attempting to capture format(s) of contents.");
                                string[]? previousClipboardFormats = previousClipboardData.GetFormats();
                                if (previousClipboardFormats is not null)
                                {
                                    // App.Current.Logger.LogDebug("ReadAloud: Current clipboard has contents; attempting to back up current clipboard.");

                                    foreach (var format in previousClipboardFormats)
                                    {
                                        object?dataObject;
                                        try
                                        {
                                            dataObject = previousClipboardData.GetData(format, false /* autoConvert */);
                                        }
                                        catch
                                        {
                                            // NOTE: in the future, we should look at using Project Reunion to use the UWP APIs (if they can deal with this scenario better)
                                            // see: https://docs.microsoft.com/en-us/uwp/api/windows.applicationmodel.datatransfer.clipboard?view=winrt-19041
                                            // see: https://docs.microsoft.com/en-us/windows/apps/desktop/modernize/desktop-to-uwp-enhance
                                            App.Current.Logger.LogDebug("ReadAloud: Unable to back up clipboard contents; this can happen with files copied to the clipboard, etc.");

                                            return(MorphicResult.ErrorResult());
                                        }
                                        clipboardContentsToRestore[format] = dataObject;
                                    }
                                }
                                else
                                {
                                    App.Current.Logger.LogDebug("ReadAloud: Current clipboard has contents, but we were unable to obtain their formats.");
                                }
                            }
                            else
                            {
                                App.Current.Logger.LogDebug("ReadAloud: Current clipboard has no contents.");
                            }

                            // clear the current clipboard
                            App.Current.Logger.LogDebug("ReadAloud: Clearing the current clipboard.");
                            try
                            {
                                // try to clear the clipboard for up to 500ms (4 delays of 125ms)
                                await Functions.ClearClipboardAsync(5, new TimeSpan(0, 0, 0, 0, 125));
                            }
                            catch
                            {
                                App.Current.Logger.LogDebug("ReadAloud: Could not clear the current clipboard.");
                            }

                            // copy the current selection to the clipboard
                            App.Current.Logger.LogDebug("ReadAloud: Sending Ctrl+C to copy the current selection to the clipboard.");
                            await SelectionReader.Default.GetSelectedTextAsync(System.Windows.Forms.SendKeys.SendWait);

                            // wait 100ms (an arbitrary amount of time, but in our testing some wait is necessary...even with the WM-triggered copy logic above)
                            // NOTE: perhaps, in the future, we should only do this if our first call to Clipboard.GetText() returns (null? or) an empty string;
                            //       or perhaps we should wait up to a certain number of milliseconds to receive a SECOND WM (the one that GetSelectedTextAsync
                            //       waited for).
                            await Task.Delay(100);

                            // capture the current selection
                            var selectionWasCopiedToClipboard = false;
                            var textCopiedToClipboard         = Clipboard.GetText();
                            if (textCopiedToClipboard is not null)
                            {
                                selectionWasCopiedToClipboard = true;

                                // we now have our selected text
                                selectedText = textCopiedToClipboard;

                                if (selectedText is not null)
                                {
                                    App.Current.Logger.LogDebug("ReadAloud: Captured selected text.");
                                }
                                else
                                {
                                    App.Current.Logger.LogDebug("ReadAloud: Captured empty selection.");
                                }
                            }
                            else
                            {
                                var copiedDataFormats = Clipboard.GetDataObject()?.GetFormats();
                                if (copiedDataFormats is not null)
                                {
                                    selectionWasCopiedToClipboard = true;

                                    // var formatsCsvBuilder = new StringBuilder();
                                    // formatsCsvBuilder.Append("[");
                                    // if (copiedDataFormats.Length > 0)
                                    // {
                                    //     formatsCsvBuilder.Append("\"");
                                    //     formatsCsvBuilder.Append(String.Join("\", \"", copiedDataFormats));
                                    //     formatsCsvBuilder.Append("\"");
                                    // }
                                    // formatsCsvBuilder.Append("]");

                                    // App.Current.Logger.LogDebug("ReadAloud: Ctrl+C did not copy text; instead it copied data in these format(s): " + formatsCsvBuilder.ToString());
                                    App.Current.Logger.LogDebug("ReadAloud: Ctrl+C copied non-text (un-speakable) contents to the clipboard.");
                                }
                                else
                                {
                                    App.Current.Logger.LogDebug("ReadAloud: Ctrl+C did not copy anything to the clipboard.");
                                }
                            }

                            // restore the previous clipboard's contents
                            // App.Current.Logger.LogDebug("ReadAloud: Attempting to restore the previous clipboard's contents");
                            //
                            if (selectionWasCopiedToClipboard == true)
                            {
                                // App.Current.Logger.LogDebug("ReadAloud: Clearing the selected text from the clipboard.");
                                try
                                {
                                    // try to clear the clipboard for up to 500ms (4 delays of 125ms)
                                    await Functions.ClearClipboardAsync(5, new TimeSpan(0, 0, 0, 0, 125));
                                }
                                catch
                                {
                                    App.Current.Logger.LogDebug("ReadAloud: Could not clear selected text from the clipboard.");
                                }
                            }
                            //
                            if (clipboardContentsToRestore.Count > 0)
                            {
                                // App.Current.Logger.LogDebug("ReadAloud: Attempting to restore " + clipboardContentsToRestore.Count.ToString() + " item(s) to the clipboard.");
                            }
                            else
                            {
                                // App.Current.Logger.LogDebug("ReadAloud: there is nothing to restore to the clipboard.");
                            }
                            //
                            foreach (var(format, data) in clipboardContentsToRestore)
                            {
                                // NOTE: sometimes, data is null (which is not something that SetData can accept) so we have to just skip that element
                                if (data is not null)
                                {
                                    Clipboard.SetData(format, data);
                                }
                            }
                            //
                            App.Current.Logger.LogDebug("ReadAloud: Clipboard restoration complete");
                        }
                        finally
                        {
                            s_captureTextSemaphore.Release();
                        }
                    }
                }
                catch (Exception ex)
                {
                    App.Current.Logger.LogError(ex, "ReadAloud: Error reading selected text.");

                    return(MorphicResult.ErrorResult());
                }

                if (selectedText is not null)
                {
                    if (selectedText != String.Empty)
                    {
                        try
                        {
                            App.Current.Logger.LogDebug("ReadAloud: Saying selected text.");

                            var sayResult = await TextToSpeechHelper.Instance.Say(selectedText);

                            if (sayResult.IsError == true)
                            {
                                App.Current.Logger.LogError("ReadAloud: Error saying selected text.");

                                return(MorphicResult.ErrorResult());
                            }

                            return(MorphicResult.OkResult());
                        }
                        catch (Exception ex)
                        {
                            App.Current.Logger.LogError(ex, "ReadAloud: Error reading selected text.");

                            return(MorphicResult.ErrorResult());
                        }
                    }
                    else
                    {
                        App.Current.Logger.LogDebug("ReadAloud: No text to say; skipping 'say' command.");

                        return(MorphicResult.OkResult());
                    }
                }
                else
                {
                    // could not capture any text
                    // App.Current.Logger.LogError("ReadAloud: Could not capture any selected text; this may or may not be an error.");

                    return(MorphicResult.ErrorResult());
                }

            default:
                throw new Exception("invalid code path");
            }
        }