21 Commits
Author SHA1 Message Date
Stone_Red 2139938496 Upgrade project to .NET 8 2023-11-22 17:38:53 +01:00
Stone_Red 6439c9c4bb Add inno setup installer script 2023-11-22 17:14:45 +01:00
Stone_Red 21e50c4f0e Move source files to src directory 2023-11-22 16:55:03 +01:00
Stone_Red 923b00cfc8 Use guid instead of app name to create the mutex 2023-11-22 16:53:31 +01:00
Stone_Red 64c693e81b Merge pull request #4 from yassinMi/main
fixed mutex lifetime issue
2023-11-22 16:41:07 +01:00
yassinMi 1afa6cef02 fixed mutex lifetime issue 2023-11-22 09:06:10 +00:00
Stone_Red bca0c378c1 Fixed a whoopsie 2023-06-06 23:21:53 +02:00
Stone_Red 6048a8330c Small UI improvments 2023-06-06 23:20:52 +02:00
Stone_Red 4f75699850 Add AutoType option and rework setting storeage 2023-06-06 19:08:34 +02:00
Stone_Red 6a8414c0a9 Update README.md 2023-03-19 01:07:09 +01:00
Stone_Red ae580599cb Store data in AppData 2023-03-19 00:42:48 +01:00
Stone_Red cee54c9364 Add support for script parameters 2023-03-18 23:50:02 +01:00
Stone_Red e23ae0a47a Add logo and improve error handling 2023-03-17 15:02:14 +01:00
Stone_Red 71451f22b2 Change syntax and add powershell support 2022-12-22 16:51:09 +01:00
Stone_Red 48af2275a7 Update issue templates 2022-06-08 15:08:30 +02:00
Stone_Red 2c294c4f84 Update version number 2022-05-29 23:47:09 +02:00
Stone_Red 3d67ad37a0 Add list command 2022-05-28 17:21:00 +02:00
Stone_Red ffb13fa0e8 Minor code imporvment 2022-05-06 15:32:21 +02:00
Stone_Red faec5d20bb Fix UI alignment and order of log messages 2022-05-06 15:32:21 +02:00
Stone_Red a282049074 Fix formatting 2022-05-04 21:52:47 +02:00
Stone_Red 5cde74dbe9 Update README.md 2022-05-03 22:30:15 +02:00
23 changed files with 796 additions and 244 deletions
+32
View File
@@ -0,0 +1,32 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: Stone-Red-Code
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
+20
View File
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: Stone-Red-Code
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
-15
View File
@@ -1,15 +0,0 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
namespace ClipCMD;
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}
-14
View File
@@ -1,14 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="InputSimulator" Version="1.0.4" />
</ItemGroup>
</Project>
-36
View File
@@ -1,36 +0,0 @@
<Window x:Class="ClipCMD.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ClipCMD"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<Label Content="Prefix" />
<TextBox x:Name="prefixTextBox" KeyUp="FixTextBox_TextChanged" Text="01" />
</StackPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<Label Content="Suffix" />
<TextBox x:Name="suffixTextBox" KeyUp="FixTextBox_TextChanged" Text="01" />
</StackPanel>
<Grid DockPanel.Dock="Top" VerticalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<DockPanel Grid.Column="0">
<Label DockPanel.Dock="Top" Content="Static Commands:" />
<TextBox DockPanel.Dock="Top" x:Name="staticCommandsTextBox" TextChanged="StaticCommandsTextBox_TextChanged" TextWrapping="Wrap" AcceptsReturn="True" VerticalAlignment="Stretch" />
</DockPanel>
<DockPanel Grid.Column="1">
<Label DockPanel.Dock="Top" Content="History:" />
<ListBox DockPanel.Dock="Top" x:Name="logListBox" VerticalAlignment="Stretch" />
</DockPanel>
</Grid>
</DockPanel>
</Window>
-166
View File
@@ -1,166 +0,0 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using WindowsInput;
using WindowsInput.Native;
namespace ClipCMD;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private readonly Dictionary<string, Func<string, string>> stuffToReplace = new()
{
{ "time", (_) => DateTime.Now.ToShortTimeString() },
{ "date", (_) => DateTime.Now.ToShortDateString() },
{ "calc", (math) => new DataTable().Compute(math.Trim()[4..], null)?.ToString() ?? string.Empty }
};
public MainWindow()
{
InitializeComponent();
}
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
// Initialize the clipboard now that we have a window soruce to use
ClipboardManager windowClipboardManager = new ClipboardManager(this);
windowClipboardManager.ClipboardChanged += ClipboardManager_ClipboardChanged;
if (File.Exists("cmd.txt"))
{
staticCommandsTextBox.Text = File.ReadAllText("cmd.txt");
}
if (File.Exists("fix.txt"))
{
string[] lines = File.ReadAllLines("fix.txt");
if (lines.Length == 2)
{
prefixTextBox.Text = lines[0];
suffixTextBox.Text = lines[1];
}
else
{
File.Delete("fix.txt");
}
}
}
private IDataObject? oldData;
private void ClipboardManager_ClipboardChanged(object? sender, EventArgs e)
{
if (!Clipboard.ContainsText() || oldData is not null && Clipboard.IsCurrent(oldData))
{
return;
}
try
{
bool result = SafeRepeat.Start(() => Clipboard.GetText().Trim(), 100, out string? text);
if (!result || text is null)
{
return;
}
if (!text.StartsWith(prefixTextBox.Text) || !text.EndsWith(suffixTextBox.Text))
{
oldData = Clipboard.GetDataObject();
return;
}
text = text[prefixTextBox.Text.Length..^suffixTextBox.Text.Length].Trim();
Func<string, string>? func = stuffToReplace.FirstOrDefault(v => text.StartsWith(v.Key)).Value;
string logText = text;
string? outText;
if (func is null)
{
string[]? parts = staticCommandsTextBox.Text.Split('\n').FirstOrDefault(l =>
{
if (string.IsNullOrWhiteSpace(l))
{
return false;
}
string? startText = l.Trim().Split('>').FirstOrDefault()?.Trim();
return startText is not null && text == startText;
})?.Split('>');
if (parts?.Length == 2)
{
outText = parts[1];
}
else
{
oldData = Clipboard.GetDataObject();
return;
}
}
else
{
outText = func(text);
}
outText = outText.Trim();
logListBox.Items.Insert(0, $"{logText} > {outText}");
Clipboard.SetDataObject(outText);
oldData = Clipboard.GetDataObject();
InputSimulator inputSimulator = new InputSimulator();
inputSimulator.Keyboard.KeyPress(new[] { VirtualKeyCode.CONTROL, VirtualKeyCode.VK_V });
}
catch (Exception ex)
{
logListBox.Items.Add($"Error: {ex.Message}");
Debug.WriteLine(ex);
}
}
private void StaticCommandsTextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
bool isError = false;
foreach (string line in staticCommandsTextBox.Text.Split('\n'))
{
if (!string.IsNullOrWhiteSpace(line) && line.Count(c => c == '>') != 1)
{
isError = true;
}
}
if (isError)
{
staticCommandsTextBox.Foreground = Brushes.Red;
}
else
{
staticCommandsTextBox.Foreground = Brushes.Black;
File.WriteAllText("cmd.txt", staticCommandsTextBox.Text);
}
}
private void FixTextBox_TextChanged(object sender, System.Windows.Input.KeyEventArgs e)
{
if (prefixTextBox is null || suffixTextBox is null)
{
return;
}
File.WriteAllLines("fix.txt", new[] { prefixTextBox.Text, suffixTextBox.Text });
}
}
+31 -11
View File
@@ -4,29 +4,49 @@
## What is it? ## What is it?
ClipCMD is a window app that allows you to create macros and run them by copying them to the clipboard. ClipCMD is a Windows app that allows you to create macros and run them by copying them to the clipboard.\
You write a macro anywhere, copy it to your clipboard and it will be automatically replaced by the specified macro text. You write a macro anywhere, copy it to your clipboard, and it will be automatically replaced by the output of the specified macro script.\
ClipCMD uses [PowerShell](https://learn.microsoft.com/en-us/powershell/scripting) as scripting language.
### Example ### Example
> Prefix: `_`\ > Prefix: `_`\
> Suffix: `_`\ > Suffix: `_`\
> Macro name: `email`\ > Macro name: `email`\
> Macro text: `[email protected]`\ > Macro script: `Write-Output [email protected]`\
> Macro script (alternative): `"[email protected]"`
> Input: `_email_` > Input: `_email_`\
> Output: `[email protected]` > Output: `[email protected]`
## Usage ## Usage
1. Download one of the [releases](https://github.com/Stone-Red-Code/ClipCMD/releases) 1. Download one of the [releases](https://github.com/Stone-Red-Code/ClipCMD/releases)
1. Start `ClipCMD.exe` 1. Start `setup.exe`
1. (Optional) Set the prefix and suffix for your commands 1. (Optional) Set the prefix and suffix for your commands
1. Specify some commands in the built in text field 1. Specify some commands in the built-in text field
- Format: `[macroName]>[macroText]` - Format:
- Each line is a new macro ```
1. There are some built in macros like `time` and `date` which will output the current local time and date [<command name(s) (separated by comma)>]
1. Write `[suffix][macroName][preffix]` somewhere, and then select it and press ``CTRL+C`` <scriptText>
<scriptText>
<scriptText>
...
[<command name(s) (separated by comma)>]
<scriptText>
<scriptText>
<scriptText>
...
```
- Strings between square brackets define macros.
- You can define macro aliases by separating them by a comma `[quit, exit, q]`
- Lines between a macro definition and the next macro (or the end of the file) is PowerShell code.
- Everything you write to the output stream replaces the input macro.
- You can pass parameters to the scripts by separating the parameters by space after the macro: `_combine hi nope_`
- You can access the parameters by accessing the `$args` array.
1. There is one prebuilt macro named `list`. This lists all of your macros.
1. Write `[suffix][macroName][preffix]` somewhere, and then select it and press `CTRL+C`
## Preview ## Preview
<img width="589" alt="Screenshot 2022-05-03 221232" src="https://user-images.githubusercontent.com/56473591/166560142-9e89c57f-1af0-4340-9317-dacf0064d6c8.png">
<img width="591" alt="image" src="https://user-images.githubusercontent.com/56473591/226146460-d9d2e8fc-3754-44c7-bc43-2e32f1d07847.png">
Binary file not shown.
+51
View File
@@ -0,0 +1,51 @@
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#expr Exec('cmd.exe /C', 'dotnet build -o "' + SourcePath + '\publish" -c Release ' + SourcePath + '..\src\ClipCMD\')
#define MyAppName "ClipCMD"
#define MyAppVersion GetVersionNumbersString("/publish/ClipCMD.exe")
#define MyAppPublisher "Stone_Red"
#define MyAppExeName "ClipCMD.exe"
[Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId=68d780d4-b946-4154-8815-e4632e5ec5d8
AppName={#MyAppName}
AppVersion={#MyAppVersion}
;AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
DefaultDirName={autopf}\{#MyAppName}
DisableProgramGroupPage=yes
; The [Icons] "quicklaunchicon" entry uses {userappdata} but its [Tasks] entry has a proper IsAdminInstallMode Check.
UsedUserAreasWarning=no
; Remove the following line to run in administrative install mode (install for all users.)
PrivilegesRequired=lowest
PrivilegesRequiredOverridesAllowed=dialog
OutputBaseFilename={#MyAppName}-Installer
OutputDir=.
Compression=lzma
SolidCompression=yes
WizardStyle=modern
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "german"; MessagesFile: "compiler:Languages\German.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 6.1; Check: not IsAdminInstallMode
[Files]
Source: "publish\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
View File
+44
View File
@@ -0,0 +1,44 @@
using System;
using System.IO;
using System.Threading;
using System.Windows;
namespace ClipCMD;
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
public const string AppName = "ClipCMD";
public const string AppGuid = "68d780d4-b946-4154-8815-e4632e5ec5d8";
private Mutex? mutex;
protected override void OnStartup(StartupEventArgs e)
{
mutex = new Mutex(true, AppGuid, out bool createdNew);
if (!createdNew)
{
//App is already running! Exiting the application
_ = MessageBox.Show($"Another instance of {AppName} already running!", $"{AppName} is already running!", MessageBoxButton.OK, MessageBoxImage.Exclamation);
Current.Shutdown();
}
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
Exit += CloseHandler;
}
protected void CloseHandler(object sender, EventArgs e)
{
mutex?.ReleaseMutex();
}
private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
File.WriteAllText("error.log", e.ExceptionObject.ToString());
}
}
+33
View File
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows7.0</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<AssemblyVersion>0.0.0.3</AssemblyVersion>
<FileVersion>0.0.0.3</FileVersion>
<Version>0.0.0.3</Version>
<ApplicationIcon>logo.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Content Include="logo.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="InputSimulator" Version="1.0.4" />
<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.0.0" />
<PackageReference Include="System.Management.Automation" Version="7.0.0" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<None Update="logo.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
@@ -10,8 +10,7 @@ public class ClipboardManager
public ClipboardManager(Window windowSource) public ClipboardManager(Window windowSource)
{ {
HwndSource? source = PresentationSource.FromVisual(windowSource) as HwndSource; if (PresentationSource.FromVisual(windowSource) is not HwndSource source)
if (source == null)
{ {
throw new ArgumentException( throw new ArgumentException(
"Window source MUST be initialized first, such as in the Window's OnSourceInitialized handler." "Window source MUST be initialized first, such as in the Window's OnSourceInitialized handler."
+35
View File
@@ -0,0 +1,35 @@
using System;
using System.Windows;
using System.Windows.Data;
namespace ClipCMD;
public class EnumBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (parameter is not string parameterString)
{
return DependencyProperty.UnsetValue;
}
if (!Enum.IsDefined(value.GetType(), value))
{
return DependencyProperty.UnsetValue;
}
object parameterValue = Enum.Parse(value.GetType(), parameterString);
return parameterValue.Equals(value);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (parameter is not string parameterString)
{
return DependencyProperty.UnsetValue;
}
return Enum.Parse(targetType, parameterString);
}
}
+85
View File
@@ -0,0 +1,85 @@
<Window x:Class="ClipCMD.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ClipCMD"
mc:Ignorable="d"
d:DataContext ="{d:DesignInstance {x:Type local:MainWindow}, IsDesignTimeCreatable=True}"
Title="ClipCMD" Height="450" Width="800"
Closing="OnClose" StateChanged="OnStateChanged" Icon="logo.ico">
<Window.Resources>
<local:EnumBooleanConverter x:Key="EnumBooleanConverter" />
</Window.Resources>
<DockPanel>
<Grid DockPanel.Dock="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<StackPanel Orientation="Horizontal">
<Label Content="Prefix:" Width="45" />
<TextBox Text="{Binding Settings.Prefix, UpdateSourceTrigger=PropertyChanged}" MinWidth="50" Height="20" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label Content="Suffix:" Width="45" />
<TextBox Text="{Binding Settings.Suffix,UpdateSourceTrigger=PropertyChanged}" MinWidth="50" Height="20" />
</StackPanel>
</StackPanel>
<StackPanel Grid.Column="1" Background="LightBlue" HorizontalAlignment="Right" Margin="10">
<Label Margin="0" Padding="0">Mode:</Label>
<StackPanel Orientation="Horizontal">
<RadioButton Content="ClipBoard" GroupName="Mode" IsChecked="{Binding Settings.Mode, Converter={StaticResource EnumBooleanConverter}, ConverterParameter=ClipBoard}" Margin="0,0,10,0" />
<StackPanel Orientation="Horizontal" IsEnabled="{Binding Settings.Mode, Converter={StaticResource EnumBooleanConverter}, ConverterParameter=ClipBoard}">
<CheckBox Content="AutoPaste" IsChecked="{Binding Settings.AutoPaste}" />
</StackPanel>
</StackPanel>
<Separator />
<StackPanel Orientation="Horizontal">
<RadioButton Content="AutoType" GroupName="Mode" IsChecked="{Binding Settings.Mode, Converter={StaticResource EnumBooleanConverter}, ConverterParameter=AutoType}" Margin="0,0,10,0" />
<StackPanel Orientation="Horizontal" IsEnabled="{Binding Settings.Mode, Converter={StaticResource EnumBooleanConverter}, ConverterParameter=AutoType}">
<Label Padding="0,0,10,0">Delay:</Label>
<TextBox Text="{Binding Settings.AutoTypeDelay, UpdateSourceTrigger=PropertyChanged}" Width="50" Height="20" />
</StackPanel>
</StackPanel>
<Button Content="Cancel AutoType" Click="CancelAutoTypeButton_Click" IsEnabled="{Binding RuntimeData.AutoTypeRunning}" />
</StackPanel>
</Grid>
<Grid DockPanel.Dock="Top" VerticalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<DockPanel Grid.Column="0">
<Label DockPanel.Dock="Top" Content="Commands:" />
<Grid DockPanel.Dock="Bottom">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="100" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Margin="4,0,0,3" Text="{Binding RuntimeData.CommandsInfo}" />
<Button Grid.Column="1" Background="AliceBlue" Content="Copy to clipboard" Click="CopyToClipboardButton_Click" />
</Grid>
<TextBox DockPanel.Dock="Top" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Text="{Binding RuntimeData.CommandsText, UpdateSourceTrigger=PropertyChanged}" Foreground="{Binding RuntimeData.CommandsTextColor}" TextChanged="StaticCommandsTextBox_TextChanged" AcceptsReturn="True" VerticalAlignment="Stretch" />
</DockPanel>
<DockPanel Visibility="{Binding RuntimeData.LogPanelVisible}" Grid.Column="1">
<Label DockPanel.Dock="Top" Content="History:" />
<ListBox DockPanel.Dock="Top" VerticalAlignment="Stretch" ItemsSource="{Binding RuntimeData.Logs}" />
</DockPanel>
<DockPanel Visibility="{Binding RuntimeData.ErrorPanelVisible}" Grid.Column="1">
<Label DockPanel.Dock="Top" Content="Errors:" />
<ListBox DockPanel.Dock="Top" Foreground="Red" VerticalAlignment="Stretch" ItemsSource="{Binding RuntimeData.Errors}" />
</DockPanel>
</Grid>
</DockPanel>
</Window>
+297
View File
@@ -0,0 +1,297 @@
using Microsoft.VisualBasic.FileIO;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Language;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Windows;
using WindowsInput;
using WindowsInput.Native;
namespace ClipCMD;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private readonly Dictionary<string, string> commands = new Dictionary<string, string>();
private readonly string commandsPath;
private readonly System.Windows.Forms.NotifyIcon notifyIcon;
private readonly string settingsPath;
private IDataObject? oldData;
private WindowState storedWindowState = WindowState.Normal;
public RuntimeData RuntimeData { get; set; } = new();
public Settings Settings { get; set; } = new();
public MainWindow()
{
notifyIcon = new System.Windows.Forms.NotifyIcon
{
BalloonTipText = "ClipCMD has been minimized. Click the tray icon to show.",
BalloonTipTitle = "ClipCMD",
Text = "ClipCMD",
Icon = new System.Drawing.Icon("logo.ico"),
Visible = true
};
notifyIcon.Click += NotifyIcon_Click;
string applicationDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "StoneRed");
string folderPath = Path.Combine(applicationDataPath, "ClipCMD");
if (!Directory.Exists(folderPath))
{
_ = Directory.CreateDirectory(folderPath);
}
commandsPath = Path.Combine(folderPath, "cmd.txt");
settingsPath = Path.Combine(folderPath, "settings.txt");
if (File.Exists(commandsPath))
{
RuntimeData.CommandsText = File.ReadAllText(commandsPath);
}
if (File.Exists(settingsPath))
{
Settings = JsonSerializer.Deserialize<Settings>(File.ReadAllText(settingsPath)) ?? new Settings();
}
DataContext = this;
InitializeComponent();
#if DEBUG
Title = $"{App.AppName} - Dev {Assembly.GetExecutingAssembly().GetName().Version}";
#else
Title = $"{App.AppName} - {Assembly.GetExecutingAssembly().GetName().Version}";
#endif
}
protected override void OnSourceInitialized(EventArgs e)
{
// Initialize the clipboard now that we have a window source to use
ClipboardManager windowClipboardManager = new ClipboardManager(this);
windowClipboardManager.ClipboardChanged += ClipboardManager_ClipboardChanged;
}
private void AddError(string commandNames, string error)
{
RuntimeData.Errors.Add($"[{commandNames}]\n{error}");
}
private void CancelAutoTypeButton_Click(object sender, RoutedEventArgs e)
{
RuntimeData.AutoTypeRunning = false;
}
private async void ClipboardManager_ClipboardChanged(object? sender, EventArgs e)
{
if (!Clipboard.ContainsText() || (oldData is not null && Clipboard.IsCurrent(oldData)))
{
return;
}
try
{
bool result = SafeRepeat.Start(() => Clipboard.GetText().Trim(), 100, out string? clipboardText);
if (!result || clipboardText is null)
{
return;
}
if (!clipboardText.StartsWith(Settings.Prefix) || !clipboardText.EndsWith(Settings.Suffix))
{
oldData = Clipboard.GetDataObject();
return;
}
clipboardText = clipboardText[Settings.Prefix.Length..^Settings.Suffix.Length].Trim().Replace("\"\"", "\0");
TextFieldParser parser = new TextFieldParser(new StringReader(clipboardText))
{
HasFieldsEnclosedInQuotes = true
};
parser.SetDelimiters(" ");
string[] sections = parser.ReadFields() ?? Array.Empty<string>();
sections = sections.Select(s => s.Replace('\0', '\"')).ToArray();
if (sections.Length == 0)
{
return;
}
string command = sections[0].Trim();
if (!commands.TryGetValue(command, out string? script))
{
oldData = Clipboard.GetDataObject();
return;
}
PowerShell ps = PowerShell
.Create()
.AddScript(script)
.AddParameters(sections.Skip(1).ToList());
StringBuilder outText = new StringBuilder();
foreach (PSObject commandResult in ps.Invoke())
{
_ = outText.AppendLine(commandResult?.ToString() ?? string.Empty);
}
RuntimeData.Logs.Insert(0, $"{command} > {outText.ToString().TrimEnd()}");
InputSimulator inputSimulator = new InputSimulator();
oldData = Clipboard.GetDataObject();
if (Settings.Mode == ClipCMDMode.ClipBoard)
{
Clipboard.SetText(outText.ToString().TrimEnd());
if (Settings.AutoPaste)
{
_ = inputSimulator.Keyboard.ModifiedKeyStroke(new[] { VirtualKeyCode.CONTROL }, new[] { VirtualKeyCode.VK_V });
}
}
else if (Settings.Mode == ClipCMDMode.AutoType)
{
RuntimeData.AutoTypeRunning = true;
foreach (char c in outText.ToString())
{
_ = inputSimulator.Keyboard.TextEntry(c);
await Task.Delay(Settings.AutoTypeDelay);
if (!RuntimeData.AutoTypeRunning)
{
break;
}
}
RuntimeData.AutoTypeRunning = false;
}
}
catch (Exception ex)
{
RuntimeData.Logs.Insert(0, $"Error: {ex.Message}");
Debug.WriteLine(ex);
}
}
private void CopyToClipboardButton_Click(object sender, RoutedEventArgs e)
{
Clipboard.SetText(RuntimeData.CommandsText);
_ = MessageBox.Show($"Successfully copied {commands.Count - 1} command(s) to clipboard!", "Success!", MessageBoxButton.OK, MessageBoxImage.Information);
}
private void NotifyIcon_Click(object? sender, EventArgs e)
{
Show();
WindowState = storedWindowState;
}
private void OnClose(object? sender, CancelEventArgs args)
{
notifyIcon.Dispose();
File.WriteAllText(settingsPath, JsonSerializer.Serialize(Settings));
}
private void OnStateChanged(object? sender, EventArgs args)
{
if (WindowState == WindowState.Minimized)
{
Hide();
notifyIcon?.ShowBalloonTip(2000);
}
else
{
storedWindowState = WindowState;
}
}
private void StaticCommandsTextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
string[] lines = RuntimeData.CommandsText.Split('\n').Append("[#END#]").ToArray();
string commandNames = string.Empty;
StringBuilder script = new StringBuilder();
RuntimeData.Errors.Clear();
commands.Clear();
RuntimeData.CommandsInfo = $"Lines: {lines.Length} Commands: {commands.Count} Errors: {RuntimeData.Errors.Count}";
foreach (string l in lines)
{
string line = l.Trim('\n', '\r');
if (line.StartsWith('[') && line.EndsWith(']'))
{
if (!string.IsNullOrEmpty(commandNames))
{
_ = Parser.ParseInput(script.ToString(), out _, out ParseError[] errors);
if (errors.Length > 0)
{
foreach (ParseError error in errors)
{
AddError(commandNames, error.ToString());
}
}
foreach (string commandName in commandNames.Split(','))
{
if (string.IsNullOrWhiteSpace(commandName))
{
AddError(commandNames, $"Command \"{commandName}\" is empty!");
}
else if (commandName.Trim().Any(char.IsWhiteSpace))
{
AddError(commandNames, $"Command \"{commandName}\" can't contain white spaces!");
}
else if (!commands.TryAdd(commandName.Trim(), script.ToString()))
{
AddError(commandNames, $"Command \"{commandName}\" already exists!");
}
}
}
commandNames = line.Trim()[1..^1].Trim();
script = new StringBuilder();
}
else if (!string.IsNullOrEmpty(commandNames))
{
if (!string.IsNullOrWhiteSpace(line))
{
_ = script.AppendLine(line);
}
}
else
{
AddError(commandNames, "Input has to start with [<Command Name>]!");
return;
}
}
RuntimeData.CommandsInfo = $"Lines: {lines.Length} Commands: {commands.Count} Errors: {RuntimeData.Errors.Count}";
_ = commands.TryAdd("list", $"\"{string.Join(", ", commands.Keys)}\"");
File.WriteAllText(commandsPath, RuntimeData.CommandsText);
}
}
+90
View File
@@ -0,0 +1,90 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Media;
namespace ClipCMD;
public class RuntimeData : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private bool autoTypeRunning = false;
private string commandsInfo = string.Empty;
private string commandsText = string.Empty;
private Visibility errorPanelVisible = Visibility.Collapsed;
private Visibility logPanelVisible = Visibility.Visible;
public bool AutoTypeRunning
{
get => autoTypeRunning;
set
{
autoTypeRunning = value;
OnPropertyChanged();
}
}
public string CommandsInfo
{
get => commandsInfo;
set
{
commandsInfo = value;
OnPropertyChanged();
}
}
public string CommandsText
{
get => commandsText;
set
{
commandsText = value;
OnPropertyChanged();
}
}
public Brush CommandsTextColor => Errors.Count > 0 ? Brushes.Red : Brushes.Black;
public Visibility ErrorPanelVisible
{
get => errorPanelVisible;
set
{
errorPanelVisible = value;
OnPropertyChanged();
}
}
public ObservableCollection<string> Errors { get; } = new();
public Visibility LogPanelVisible
{
get => logPanelVisible;
set
{
logPanelVisible = value;
OnPropertyChanged();
}
}
public ObservableCollection<string> Logs { get; } = new();
public RuntimeData()
{
Errors.CollectionChanged += (sender, args) =>
{
ErrorPanelVisible = Errors.Any() ? Visibility.Visible : Visibility.Collapsed;
LogPanelVisible = Errors.Any() ? Visibility.Collapsed : Visibility.Visible;
OnPropertyChanged(nameof(CommandsTextColor));
};
}
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
+77
View File
@@ -0,0 +1,77 @@
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace ClipCMD;
public class Settings : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private bool autoPaste = true;
private int autoTypeDelay = 100;
private ClipCMDMode mode = ClipCMDMode.ClipBoard;
private string prefix = "_";
private string suffix = "_";
public bool AutoPaste
{
get => autoPaste;
set
{
autoPaste = value;
OnPropertyChanged();
}
}
public int AutoTypeDelay
{
get => autoTypeDelay;
set
{
autoTypeDelay = Math.Clamp(value, 0, 1000);
OnPropertyChanged();
}
}
public ClipCMDMode Mode
{
get => mode;
set
{
mode = value;
OnPropertyChanged();
}
}
public string Prefix
{
get => prefix;
set
{
prefix = value;
OnPropertyChanged();
}
}
public string Suffix
{
get => suffix;
set
{
suffix = value;
OnPropertyChanged();
}
}
protected void OnPropertyChanged([CallerMemberName] string? name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
public enum ClipCMDMode
{
ClipBoard,
AutoType
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB