From 9322e109133fd2084e5bea7d19629a0c261bdd47 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Mon, 6 May 2024 19:32:38 +0200 Subject: [PATCH] Implement most features the old app had --- src/ClipCMD/App.axaml | 2 + src/ClipCMD/App.axaml.cs | 1 + src/ClipCMD/ClipCmd.csproj | 7 ++ src/ClipCMD/GlobalSuppressions.cs | 5 +- src/ClipCMD/Models/ClipCmdCommand.cs | 4 +- src/ClipCMD/Models/Settings.cs | 4 +- .../Utilities/ClipCmdCommandHandler.cs | 56 +++++++++- src/ClipCMD/Utilities/Configuration.cs | 2 + .../ViewModels/ClipCmdCommandItemViewModel.cs | 73 ++++++++++++ .../ViewModels/EditorMessageViewModel.cs | 10 ++ src/ClipCMD/ViewModels/EditorViewModel.cs | 35 ++++++ .../ViewModels/InputDialogViewModel.cs | 18 +++ src/ClipCMD/ViewModels/MainViewModel.cs | 70 +++++++++++- src/ClipCMD/Views/EditorWindow.axaml | 27 +++++ src/ClipCMD/Views/EditorWindow.axaml.cs | 65 +++++++++++ src/ClipCMD/Views/InputDialogView.axaml | 26 +++++ src/ClipCMD/Views/InputDialogView.axaml.cs | 11 ++ src/ClipCMD/Views/MainView.axaml | 105 ++++++++++++------ src/ClipCMD/Views/MainView.axaml.cs | 36 ------ src/ClipCMD/Views/MainWindow.axaml | 2 + 20 files changed, 479 insertions(+), 80 deletions(-) create mode 100644 src/ClipCMD/ViewModels/ClipCmdCommandItemViewModel.cs create mode 100644 src/ClipCMD/ViewModels/EditorMessageViewModel.cs create mode 100644 src/ClipCMD/ViewModels/EditorViewModel.cs create mode 100644 src/ClipCMD/ViewModels/InputDialogViewModel.cs create mode 100644 src/ClipCMD/Views/EditorWindow.axaml create mode 100644 src/ClipCMD/Views/EditorWindow.axaml.cs create mode 100644 src/ClipCMD/Views/InputDialogView.axaml create mode 100644 src/ClipCMD/Views/InputDialogView.axaml.cs diff --git a/src/ClipCMD/App.axaml b/src/ClipCMD/App.axaml index a15550a..0d29022 100644 --- a/src/ClipCMD/App.axaml +++ b/src/ClipCMD/App.axaml @@ -1,5 +1,6 @@ @@ -16,6 +17,7 @@ + \ No newline at end of file diff --git a/src/ClipCMD/App.axaml.cs b/src/ClipCMD/App.axaml.cs index b35fbd5..fea6b25 100644 --- a/src/ClipCMD/App.axaml.cs +++ b/src/ClipCMD/App.axaml.cs @@ -37,6 +37,7 @@ public partial class App : Application desktop.MainWindow.Closing += (sender, e) => { Settings.SaveSettings(); + clipCmdCommandHandler.SaveCommands(); }; clipCmdCommandHandler.Start(); diff --git a/src/ClipCMD/ClipCmd.csproj b/src/ClipCMD/ClipCmd.csproj index f800838..30ab02a 100644 --- a/src/ClipCMD/ClipCmd.csproj +++ b/src/ClipCMD/ClipCmd.csproj @@ -19,6 +19,7 @@ + @@ -30,4 +31,10 @@ + + + + InputDialogView.axaml + + diff --git a/src/ClipCMD/GlobalSuppressions.cs b/src/ClipCMD/GlobalSuppressions.cs index 924b2cd..adae0f4 100644 --- a/src/ClipCMD/GlobalSuppressions.cs +++ b/src/ClipCMD/GlobalSuppressions.cs @@ -1,4 +1,7 @@ -// This file is used by Code Analysis to maintain SuppressMessage +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "")] +// This file is used by Code Analysis to maintain SuppressMessage // attributes that are applied to this project. // Project-level suppressions either have no target or are given // a specific target and scoped to a namespace, type, member, etc. \ No newline at end of file diff --git a/src/ClipCMD/Models/ClipCmdCommand.cs b/src/ClipCMD/Models/ClipCmdCommand.cs index ea58fbe..1d609f3 100644 --- a/src/ClipCMD/Models/ClipCmdCommand.cs +++ b/src/ClipCMD/Models/ClipCmdCommand.cs @@ -1,6 +1,6 @@ namespace ClipCmd.Models; -internal class ClipCmdCommand +public class ClipCmdCommand(string script) { - public string Script { get; set; } = string.Empty; + public string Script { get; set; } = script; } \ No newline at end of file diff --git a/src/ClipCMD/Models/Settings.cs b/src/ClipCMD/Models/Settings.cs index d351d61..cca274b 100644 --- a/src/ClipCMD/Models/Settings.cs +++ b/src/ClipCMD/Models/Settings.cs @@ -13,7 +13,7 @@ public class Settings public bool AutoPaste { get; set; } = true; - public int AutoTypeDelay { get; set; } + public int AutoTypeDelay { get; set; } = 100; public ClipCmdMode Mode { get; set; } @@ -21,6 +21,8 @@ public class Settings public string Suffix { get; set; } = ""; + public string CommandArgsSeperator { get; set; } = ":"; + public static void SaveSettings() { if (!Directory.Exists(Configuration.ApplicationDataPath)) diff --git a/src/ClipCMD/Utilities/ClipCmdCommandHandler.cs b/src/ClipCMD/Utilities/ClipCmdCommandHandler.cs index db95667..129d910 100644 --- a/src/ClipCMD/Utilities/ClipCmdCommandHandler.cs +++ b/src/ClipCMD/Utilities/ClipCmdCommandHandler.cs @@ -1,4 +1,5 @@ -using Avalonia.Controls; +using Avalonia.Collections; +using Avalonia.Controls; using Avalonia.Input.Platform; using ClipCmd.Models; @@ -7,8 +8,12 @@ using SharpHook; using SharpHook.Native; using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Management.Automation; using System.Text; +using System.Text.Json; using System.Threading.Tasks; namespace ClipCmd.Utilities; @@ -17,13 +22,15 @@ public class ClipCmdCommandHandler(Window window) { private readonly IClipboard clipboard = window.Clipboard!; private readonly EventSimulator simulator = new EventSimulator(); - private readonly ConcurrentDictionary commands = new(); private string lastText = string.Empty; + public AvaloniaDictionary Commands { get; } = []; public void Start() { _ = new TaskFactory().StartNew(async () => { + LoadCommands(); + while (true) { await Run(); @@ -32,6 +39,33 @@ public class ClipCmdCommandHandler(Window window) }, TaskCreationOptions.LongRunning); } + public void ClearCommands() + { + Commands.Clear(); + } + + public void SaveCommands() + { + string json = JsonSerializer.Serialize(Commands); + File.WriteAllText(Configuration.CommandsPath, json); + } + + public void LoadCommands() + { + if (!File.Exists(Configuration.CommandsPath)) + { + return; + } + + string json = File.ReadAllText(Configuration.CommandsPath); + Commands.Clear(); + + foreach (KeyValuePair command in JsonSerializer.Deserialize>(json) ?? new()) + { + Commands[command.Key] = command.Value; + } + } + private async Task Run() { string? text = await clipboard.GetTextAsync(); @@ -48,14 +82,18 @@ public class ClipCmdCommandHandler(Window window) return; } - string commandName = text[Settings.Current.Prefix.Length..^Settings.Current.Suffix.Length]; + string input = text[Settings.Current.Prefix.Length..^Settings.Current.Suffix.Length].Replace(Settings.Current.CommandArgsSeperator + Settings.Current.CommandArgsSeperator, "\0"); + string[] parts = input.Split(Settings.Current.CommandArgsSeperator); + string commandName = parts[0].Replace("\0", Settings.Current.CommandArgsSeperator); + string[] parameters = parts.Skip(1).Select(p => p.Replace("\0", Settings.Current.CommandArgsSeperator)).ToArray(); StringBuilder outText = new StringBuilder(); - if (commands.TryGetValue(commandName, out ClipCmdCommand? command)) + if (Commands.TryGetValue(commandName, out ClipCmdCommand? command)) { PowerShell powerShell = PowerShell.Create(); _ = powerShell.AddScript(command.Script); + _ = powerShell.AddParameters(parameters); foreach (PSObject commandResult in await powerShell.InvokeAsync()) { @@ -66,6 +104,10 @@ public class ClipCmdCommandHandler(Window window) { return; } + else if (commandName == "list") + { + _ = outText.AppendLine($"Commands: {string.Join(", ", Commands.Keys)}"); + } else { _ = outText.AppendLine("Command not found!"); @@ -90,6 +132,12 @@ public class ClipCmdCommandHandler(Window window) { foreach (char c in outText.ToString().TrimEnd()) { + if (c == '\n') + { + _ = simulator.SimulateKeyPress(KeyCode.VcEnter); + continue; + } + _ = simulator.SimulateTextEntry(c.ToString()); await Task.Delay(Settings.Current.AutoTypeDelay); } diff --git a/src/ClipCMD/Utilities/Configuration.cs b/src/ClipCMD/Utilities/Configuration.cs index d1f7c37..825cca1 100644 --- a/src/ClipCMD/Utilities/Configuration.cs +++ b/src/ClipCMD/Utilities/Configuration.cs @@ -21,4 +21,6 @@ internal static class Configuration public static string SettingsFilePath => Path.Combine(ApplicationDataPath, "settings.json"); public static string LogFilePath => Path.Combine(ApplicationDataPath, $"ClipCMD.log"); + + public static string CommandsPath => Path.Combine(ApplicationDataPath, "commands.json"); } \ No newline at end of file diff --git a/src/ClipCMD/ViewModels/ClipCmdCommandItemViewModel.cs b/src/ClipCMD/ViewModels/ClipCmdCommandItemViewModel.cs new file mode 100644 index 0000000..7530c94 --- /dev/null +++ b/src/ClipCMD/ViewModels/ClipCmdCommandItemViewModel.cs @@ -0,0 +1,73 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; + +using ClipCmd.Models; +using ClipCmd.Utilities; +using ClipCmd.Views; + +using DialogHostAvalonia; + +using ReactiveUI; + +using System.Collections.Generic; +using System.Windows.Input; + +namespace ClipCmd.ViewModels; + +public class ClipCmdCommandItemViewModel(string name, ClipCmdCommandHandler clipCmdCommandHandler) +{ + public string Name { get; } = name; + + public ICommand RemoveCommand => ReactiveCommand.Create(Remove); + + public ICommand RenameCommand => ReactiveCommand.Create(Rename); + + public ICommand EditCommand => ReactiveCommand.Create(Edit); + + private void Remove() + { + _ = clipCmdCommandHandler.Commands.Remove(Name); + clipCmdCommandHandler.SaveCommands(); + } + + private async void Rename() + { + string? newName = await DialogHost.Show(new InputDialogViewModel("Edit command name:", Name)) as string; + newName = newName?.Trim(); + + if (!clipCmdCommandHandler.Commands.Remove(Name, out ClipCmdCommand? clipCmdCommand)) + { + return; + } + + if (string.IsNullOrWhiteSpace(newName)) + { + clipCmdCommandHandler.Commands.Add(Name, clipCmdCommand); + return; + } + + int counter = 0; + string originalName = newName; + + while (clipCmdCommandHandler.Commands.ContainsKey(newName)) + { + newName = $"{originalName} ({++counter})"; + } + + clipCmdCommandHandler.Commands.Add(newName, clipCmdCommand); + clipCmdCommandHandler.SaveCommands(); + } + + private async void Edit() + { + Avalonia.Controls.Window? mainWindow = Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop ? desktop.MainWindow : null; + + EditorWindow editorWindow = new EditorWindow + { + DataContext = new EditorViewModel(Name, clipCmdCommandHandler) + }; + + await editorWindow.ShowDialog(mainWindow!); + clipCmdCommandHandler.SaveCommands(); + } +} \ No newline at end of file diff --git a/src/ClipCMD/ViewModels/EditorMessageViewModel.cs b/src/ClipCMD/ViewModels/EditorMessageViewModel.cs new file mode 100644 index 0000000..cd70b7c --- /dev/null +++ b/src/ClipCMD/ViewModels/EditorMessageViewModel.cs @@ -0,0 +1,10 @@ +using Avalonia.Media; + +namespace ClipCmd.ViewModels; + +internal class EditorMessageViewModel(string message, IBrush color) +{ + public string Message { get; } = message; + + public IBrush Color { get; } = color; +} \ No newline at end of file diff --git a/src/ClipCMD/ViewModels/EditorViewModel.cs b/src/ClipCMD/ViewModels/EditorViewModel.cs new file mode 100644 index 0000000..30aa479 --- /dev/null +++ b/src/ClipCMD/ViewModels/EditorViewModel.cs @@ -0,0 +1,35 @@ +using ClipCmd.Utilities; + +using System.Collections.ObjectModel; + +namespace ClipCmd.ViewModels; + +internal class EditorViewModel : ViewModelBase +{ + private readonly ClipCmdCommandHandler clipCmdCommandHandler; + + public string Title => $"ClipCMD Editor - {Name}"; + + public string Name { get; } + + public string Script + { + get => clipCmdCommandHandler.Commands[Name].Script; + set => clipCmdCommandHandler.Commands[Name].Script = value; + } + + public ObservableCollection Messages { get; } = []; + + public EditorViewModel(string name, ClipCmdCommandHandler clipCmdCommandHandler) + { + Name = name; + this.clipCmdCommandHandler = clipCmdCommandHandler; + } + + // Only used for the designer + public EditorViewModel() + { + Name = "Test"; + clipCmdCommandHandler = new ClipCmdCommandHandler(new()); + } +} \ No newline at end of file diff --git a/src/ClipCMD/ViewModels/InputDialogViewModel.cs b/src/ClipCMD/ViewModels/InputDialogViewModel.cs new file mode 100644 index 0000000..1d44928 --- /dev/null +++ b/src/ClipCMD/ViewModels/InputDialogViewModel.cs @@ -0,0 +1,18 @@ +using DialogHostAvalonia; + +using ReactiveUI; + +using System.Windows.Input; + +namespace ClipCmd.ViewModels; + +internal class InputDialogViewModel(string title, string defaultValue = "") : ViewModelBase +{ + public string Title { get; set; } = title; + + public string Value { get; set; } = defaultValue; + + public ICommand OkCommand => ReactiveCommand.Create(() => DialogHost.GetDialogSession(null)!.Close(Value)); + + public ICommand CancelCommand => ReactiveCommand.Create(() => DialogHost.GetDialogSession(null)!.Close(null)); +} \ No newline at end of file diff --git a/src/ClipCMD/ViewModels/MainViewModel.cs b/src/ClipCMD/ViewModels/MainViewModel.cs index b3b010f..9362d0b 100644 --- a/src/ClipCMD/ViewModels/MainViewModel.cs +++ b/src/ClipCMD/ViewModels/MainViewModel.cs @@ -1,12 +1,22 @@ using ClipCmd.Models; using ClipCmd.Utilities; +using DialogHostAvalonia; + using ReactiveUI; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Input; + namespace ClipCmd.ViewModels; public class MainViewModel : ViewModelBase { + private readonly ClipCmdCommandHandler clipCmdCommandHandler; + + public List Commands => [.. clipCmdCommandHandler.Commands.Keys.Select(x => new ClipCmdCommandItemViewModel(x, clipCmdCommandHandler))]; + public bool AutoTypeModeEnabled { get => Settings.Current.Mode == ClipCmdMode.AutoType; @@ -31,12 +41,12 @@ public class MainViewModel : ViewModelBase } } - public int AutoTypeDelay + public int? AutoTypeDelay { get => Settings.Current.AutoTypeDelay; set { - Settings.Current.AutoTypeDelay = value; + Settings.Current.AutoTypeDelay = value ?? 1; this.RaisePropertyChanged(); } } @@ -71,11 +81,65 @@ public class MainViewModel : ViewModelBase } } - public MainViewModel(ClipCmdCommandHandler clipCmdCommandHandler) + public string CommandArgsSeperator { + get => Settings.Current.CommandArgsSeperator; + set + { + value = value.Replace(Settings.Current.CommandArgsSeperator, string.Empty); + + if (string.IsNullOrWhiteSpace(value)) + { + value = Settings.Current.CommandArgsSeperator; + } + + if (value.Length > 1) + { + value = value[..1]; + } + + Settings.Current.CommandArgsSeperator = value; + this.RaisePropertyChanged(); + } } + public ICommand AddCommand => ReactiveCommand.Create(Add); + + public MainViewModel(ClipCmdCommandHandler clipCmdCommandHandler) + { + this.clipCmdCommandHandler = clipCmdCommandHandler; + + clipCmdCommandHandler.Commands.CollectionChanged += (sender, e) => + { + this.RaisePropertyChanged(nameof(Commands)); + }; + } + + // Only used for the designer public MainViewModel() { + clipCmdCommandHandler = new ClipCmdCommandHandler(new()); + } + + private async void Add() + { + string? name = await DialogHost.Show(new InputDialogViewModel("Enter command name:")) as string; + name = name?.Trim(); + + if (string.IsNullOrWhiteSpace(name)) + { + return; + } + + int counter = 0; + string originalName = name; + + while (clipCmdCommandHandler.Commands.ContainsKey(name)) + { + name = $"{originalName} ({++counter})"; + } + + clipCmdCommandHandler.Commands.Add(name, new ClipCmdCommand(@$"Write-Output ""{name} command""")); + clipCmdCommandHandler.SaveCommands(); } } \ No newline at end of file diff --git a/src/ClipCMD/Views/EditorWindow.axaml b/src/ClipCMD/Views/EditorWindow.axaml new file mode 100644 index 0000000..fc5b5ce --- /dev/null +++ b/src/ClipCMD/Views/EditorWindow.axaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/ClipCMD/Views/EditorWindow.axaml.cs b/src/ClipCMD/Views/EditorWindow.axaml.cs new file mode 100644 index 0000000..c2483af --- /dev/null +++ b/src/ClipCMD/Views/EditorWindow.axaml.cs @@ -0,0 +1,65 @@ +using Avalonia.Controls; +using Avalonia.Media; + +using AvaloniaEdit; +using AvaloniaEdit.TextMate; + +using ClipCmd.ViewModels; + +using System; +using System.Diagnostics; +using System.Management.Automation.Language; + +using TextMateSharp.Grammars; + +namespace ClipCmd.Views; + +public partial class EditorWindow : Window +{ + public EditorWindow() + { + InitializeComponent(); + } + + private void Window_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + EditorViewModel editorViewModel = DataContext as EditorViewModel ?? throw new InvalidOperationException("DataContext is not EditorViewModel"); + + TextEditor textEditor = this.FindControl("Editor")!; + + textEditor.Text = editorViewModel.Script; + + textEditor.TextChanged += (s, e) => + { + editorViewModel.Script = textEditor.Text; + CheckScript(editorViewModel); + }; + + RegistryOptions registryOptions = new RegistryOptions(ThemeName.DarkPlus); + TextMate.Installation _textMateInstallation = textEditor.InstallTextMate(registryOptions); + + _textMateInstallation.SetGrammar(registryOptions.GetScopeByLanguageId(registryOptions.GetLanguageByExtension(".ps1").Id)); + + CheckScript(editorViewModel); + } + + private void CheckScript(EditorViewModel editorViewModel) + { + _ = Parser.ParseInput(editorViewModel.Script, out _, out ParseError[] parseErrors); + + editorViewModel.Messages.Clear(); + + if (parseErrors.Length > 0) + { + foreach (ParseError parseError in parseErrors) + { + Debug.WriteLine(parseError.Message + " at line " + parseError.Extent.StartLineNumber + " and column " + parseError.Extent.StartColumnNumber); + editorViewModel.Messages.Add(new($"Error: {parseError.Message} at line {parseError.Extent.StartLineNumber} and column {parseError.Extent.StartColumnNumber}", Brushes.Red)); + } + } + else + { + editorViewModel.Messages.Add(new("No errors", Brushes.Green)); + } + } +} \ No newline at end of file diff --git a/src/ClipCMD/Views/InputDialogView.axaml b/src/ClipCMD/Views/InputDialogView.axaml new file mode 100644 index 0000000..9f45648 --- /dev/null +++ b/src/ClipCMD/Views/InputDialogView.axaml @@ -0,0 +1,26 @@ + + + + + + + + \ No newline at end of file diff --git a/src/ClipCMD/Views/MainView.axaml.cs b/src/ClipCMD/Views/MainView.axaml.cs index a525946..de75bce 100644 --- a/src/ClipCMD/Views/MainView.axaml.cs +++ b/src/ClipCMD/Views/MainView.axaml.cs @@ -1,13 +1,5 @@ using Avalonia.Controls; -using AvaloniaEdit; -using AvaloniaEdit.TextMate; - -using System.Diagnostics; -using System.Management.Automation.Language; - -using TextMateSharp.Grammars; - namespace ClipCmd.Views; public partial class MainView : UserControl @@ -16,32 +8,4 @@ public partial class MainView : UserControl { InitializeComponent(); } - - private void UserControl_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e) - { - return; - //First of all you need to have a reference for your TextEditor for it to be used inside AvaloniaEdit.TextMate project. - TextEditor? textEditor = this.FindControl("TextEditor"); - - textEditor.TextChanged += (s, e) => - { - _ = Parser.ParseInput(textEditor.Text, out _, out ParseError[] parseErrors); - - if (parseErrors.Length > 0) - { - foreach (ParseError parseError in parseErrors) - { - Debug.WriteLine(parseError.Message + " at line " + parseError.Extent.StartLineNumber + " and column " + parseError.Extent.StartColumnNumber); - } - } - }; - RegistryOptions registryOptions = new RegistryOptions(ThemeName.DarkPlus); - - //Initial setup of TextMate. - TextMate.Installation _textMateInstallation = textEditor.InstallTextMate(registryOptions); - - //Here we are getting the language by the extension and right after that we are initializing grammar with this language. - //And that's all 😀, you are ready to use AvaloniaEdit with syntax highlighting! - _textMateInstallation.SetGrammar(registryOptions.GetScopeByLanguageId(registryOptions.GetLanguageByExtension(".ps1").Id)); - } } \ No newline at end of file diff --git a/src/ClipCMD/Views/MainWindow.axaml b/src/ClipCMD/Views/MainWindow.axaml index 4993950..4e81e1c 100644 --- a/src/ClipCMD/Views/MainWindow.axaml +++ b/src/ClipCMD/Views/MainWindow.axaml @@ -3,10 +3,12 @@ xmlns:vm="using:ClipCmd.ViewModels" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia" xmlns:views="clr-namespace:ClipCmd.Views" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="ClipCmd.Views.MainWindow" Icon="/Assets/avalonia-logo.ico" Title="ClipCmd" Width="800" Height="450"> + \ No newline at end of file