├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md └── src ├── AvalonShell.sln └── AvalonShell ├── App.config ├── AvalonShell.csproj ├── PowerShellHost.cs ├── PowerShellInvocation.cs ├── Properties └── AssemblyInfo.cs ├── Themes └── Generic.xaml ├── UI ├── Application.xaml ├── Application.xaml.cs ├── Controls │ └── PowerShellConsole.cs ├── Window.xaml └── Window.xaml.cs └── packages.config /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{cs,xaml}] 4 | indent_style = tab 5 | indent_size = 4 6 | end_of_line = crlf 7 | insert_final_newline = true 8 | charset = utf-8-bom 9 | 10 | [*.{xml,wxs}] 11 | indent_style = space 12 | indent_size = 2 13 | end_of_line = crlf 14 | insert_final_newline = true 15 | charset = utf-8-bom 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | # The packages folder can be ignored because of Package Restore 156 | **/packages/* 157 | # except build/, which is used as an MSBuild target. 158 | !**/packages/build/ 159 | # Uncomment if necessary however generally it will be regenerated when needed 160 | #!**/packages/repositories.config 161 | # NuGet v3's project.json files produces more ignoreable files 162 | *.nuget.props 163 | *.nuget.targets 164 | 165 | # Microsoft Azure Build Output 166 | csx/ 167 | *.build.csdef 168 | 169 | # Microsoft Azure Emulator 170 | ecf/ 171 | rcf/ 172 | 173 | # Windows Store app package directories and files 174 | AppPackages/ 175 | BundleArtifacts/ 176 | Package.StoreAssociation.xml 177 | _pkginfo.txt 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | ~$* 188 | *~ 189 | *.dbmdl 190 | *.dbproj.schemaview 191 | *.pfx 192 | *.publishsettings 193 | node_modules/ 194 | orleans.codegen.cs 195 | 196 | # Since there are multiple workflows, uncomment next line to ignore bower_components 197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 198 | #bower_components/ 199 | 200 | # RIA/Silverlight projects 201 | Generated_Code/ 202 | 203 | # Backup & report files from converting an old project file 204 | # to a newer Visual Studio version. Backup files are not needed, 205 | # because we have git ;-) 206 | _UpgradeReport_Files/ 207 | Backup*/ 208 | UpgradeLog*.XML 209 | UpgradeLog*.htm 210 | 211 | # SQL Server files 212 | *.mdf 213 | *.ldf 214 | 215 | # Business Intelligence projects 216 | *.rdl.data 217 | *.bim.layout 218 | *.bim_*.settings 219 | 220 | # Microsoft Fakes 221 | FakesAssemblies/ 222 | 223 | # GhostDoc plugin setting file 224 | *.GhostDoc.xml 225 | 226 | # Node.js Tools for Visual Studio 227 | .ntvs_analysis.dat 228 | 229 | # Visual Studio 6 build log 230 | *.plg 231 | 232 | # Visual Studio 6 workspace options file 233 | *.opt 234 | 235 | # Visual Studio LightSwitch build output 236 | **/*.HTMLClient/GeneratedArtifacts 237 | **/*.DesktopClient/GeneratedArtifacts 238 | **/*.DesktopClient/ModelManifest.xml 239 | **/*.Server/GeneratedArtifacts 240 | **/*.Server/ModelManifest.xml 241 | _Pvt_Extensions 242 | 243 | # Paket dependency manager 244 | .paket/paket.exe 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Manato KAMEYA 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## AvalonShell 2 | 3 | WPF で Windows PowerShell コンソールを再現するコードです。 4 | 5 | ![posh2](https://cloud.githubusercontent.com/assets/1779073/21416705/512cdfd0-c858-11e6-9b6c-e9561812d094.gif) 6 | 7 | コマンドレットの入力と結果の出力、および History などいくつかの PowerShell コンソール機能をサポートしています。 8 | 現時点ではブログ サンプル程度のもので、完全に再現はできていません。 9 | 10 | see also: http://grabacr.net/archives/7331 11 | -------------------------------------------------------------------------------- /src/AvalonShell.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AvalonShell", "AvalonShell\AvalonShell.csproj", "{DFFEB365-45DD-4CB5-B0A9-8F91DEC9D99A}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {DFFEB365-45DD-4CB5-B0A9-8F91DEC9D99A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {DFFEB365-45DD-4CB5-B0A9-8F91DEC9D99A}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {DFFEB365-45DD-4CB5-B0A9-8F91DEC9D99A}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {DFFEB365-45DD-4CB5-B0A9-8F91DEC9D99A}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /src/AvalonShell/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/AvalonShell/AvalonShell.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {DFFEB365-45DD-4CB5-B0A9-8F91DEC9D99A} 8 | WinExe 9 | Properties 10 | AvalonShell 11 | AvalonShell 12 | v4.6.2 13 | 512 14 | {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 15 | 4 16 | true 17 | 18 | 19 | AnyCPU 20 | true 21 | full 22 | false 23 | bin\Debug\ 24 | DEBUG;TRACE 25 | prompt 26 | 4 27 | 28 | 29 | AnyCPU 30 | pdbonly 31 | true 32 | bin\Release\ 33 | TRACE 34 | prompt 35 | 4 36 | 37 | 38 | 39 | 40 | 41 | packages\Microsoft.PowerShell.5.ReferenceAssemblies.1.0.0\lib\net4\System.Management.Automation.dll 42 | True 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 4.0 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | MSBuild:Compile 60 | Designer 61 | 62 | 63 | MSBuild:Compile 64 | Designer 65 | 66 | 67 | MSBuild:Compile 68 | Designer 69 | 70 | 71 | Application.xaml 72 | Code 73 | 74 | 75 | 76 | 77 | 78 | Window.xaml 79 | Code 80 | 81 | 82 | 83 | 84 | Code 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 101 | -------------------------------------------------------------------------------- /src/AvalonShell/PowerShellHost.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using System.Management.Automation; 6 | using System.Management.Automation.Runspaces; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace AvalonShell 11 | { 12 | public interface IPowerShellHost 13 | { 14 | ReadOnlyObservableCollection Invocations { get; } 15 | } 16 | 17 | public class PowerShellHost : IPowerShellHost, IDisposable 18 | { 19 | private const string _errorMessage = "{0}\r\n + CategoryInfo : {1}\r\n + FullyQualifiedErrorId : {2}"; 20 | private const string _errorMessageWithPosition = "{0}\r\n{1}\r\n + CategoryInfo : {2}\r\n + FullyQualifiedErrorId : {3}"; 21 | 22 | private readonly Runspace _runspace; 23 | private readonly List _history = new List(); 24 | private readonly ObservableCollection _invocations = new ObservableCollection(); 25 | private ReadOnlyObservableCollection _readonlyInvocations; 26 | private int _count; 27 | 28 | ReadOnlyObservableCollection IPowerShellHost.Invocations 29 | => this._readonlyInvocations ?? (this._readonlyInvocations = new ReadOnlyObservableCollection(this._invocations)); 30 | 31 | public PowerShellHost() 32 | { 33 | this._runspace = RunspaceFactory.CreateRunspace(); 34 | } 35 | 36 | public void Open() 37 | { 38 | this._runspace.Open(); 39 | 40 | this._invocations.Add(new PowerShellMessage("Custom PowerShell Host - version 0.1")); 41 | this._invocations.Add(new PowerShellInvocation(++this._count, x => this.HandleInvocationRequested(x), this._history)); 42 | } 43 | 44 | protected async void HandleInvocationRequested(PowerShellInvocation sender) 45 | { 46 | try 47 | { 48 | if (string.IsNullOrWhiteSpace(sender.Script)) 49 | { 50 | sender.SetResult(new InvocationResult()); 51 | } 52 | else 53 | { 54 | using (var powershell = PowerShell.Create()) 55 | { 56 | powershell.Runspace = this._runspace; 57 | powershell.AddScript(sender.Script); 58 | 59 | // ReSharper disable once AccessToDisposedClosure 60 | var results = await Task.Factory.FromAsync(powershell.BeginInvoke(), x => powershell.EndInvoke(x)).ConfigureAwait(false); 61 | var error = this.CreateResultIfError(powershell); 62 | 63 | sender.SetResult(error ?? await this.HandleResult(results)); 64 | } 65 | 66 | this._history.Add(sender.Script); 67 | } 68 | } 69 | catch (Exception ex) 70 | { 71 | this.CreateErrorMessage(ex); 72 | } 73 | finally 74 | { 75 | this._invocations.Add(new PowerShellInvocation(++this._count, x => this.HandleInvocationRequested(x), this._history)); 76 | } 77 | } 78 | 79 | protected virtual Task HandleResult(PSDataCollection results) 80 | { 81 | return Task.Run(() => this.OutString(results)); 82 | } 83 | 84 | protected InvocationResult OutString(IEnumerable input) 85 | { 86 | try 87 | { 88 | var sb = new StringBuilder(); 89 | 90 | using (var powershell = PowerShell.Create()) 91 | { 92 | powershell.Runspace = this._runspace; 93 | powershell.AddCommand("Out-String"); 94 | 95 | foreach (var result in powershell.Invoke(input)) 96 | { 97 | sb.AppendLine(result.ToString()); 98 | } 99 | } 100 | 101 | return new InvocationResult(InvocationResultKind.Normal, sb.ToString()); 102 | } 103 | catch (Exception ex) 104 | { 105 | return new InvocationResult(InvocationResultKind.Error, this.CreateErrorMessage(ex)); 106 | } 107 | } 108 | 109 | protected InvocationResult CreateResultIfError(PowerShell powershell) 110 | { 111 | if (powershell.Streams.Error == null || powershell.Streams.Error.Count == 0) return null; 112 | 113 | var sb = new StringBuilder(); 114 | foreach (var error in powershell.Streams.Error) 115 | { 116 | sb.AppendLine(string.Format(_errorMessageWithPosition, error, error.InvocationInfo.PositionMessage, error.CategoryInfo, error.FullyQualifiedErrorId)); 117 | } 118 | 119 | return new InvocationResult(InvocationResultKind.Error, sb.ToString()); 120 | } 121 | 122 | protected string CreateErrorMessage(Exception ex) 123 | { 124 | var container = ex as IContainsErrorRecord; 125 | if (container?.ErrorRecord == null) 126 | { 127 | return ex.Message; 128 | } 129 | 130 | var invocationInfo = container.ErrorRecord.InvocationInfo; 131 | if (invocationInfo == null) 132 | { 133 | return string.Format(_errorMessage, container.ErrorRecord, container.ErrorRecord.CategoryInfo, container.ErrorRecord.FullyQualifiedErrorId); 134 | } 135 | 136 | if (invocationInfo.PositionMessage != null && _errorMessage.IndexOf(invocationInfo.PositionMessage, StringComparison.Ordinal) != -1) 137 | { 138 | return string.Format(_errorMessage, container.ErrorRecord, container.ErrorRecord.CategoryInfo, container.ErrorRecord.FullyQualifiedErrorId); 139 | } 140 | 141 | return string.Format(_errorMessageWithPosition, container.ErrorRecord, invocationInfo.PositionMessage, container.ErrorRecord.CategoryInfo, container.ErrorRecord.FullyQualifiedErrorId); 142 | } 143 | 144 | public void Dispose() 145 | { 146 | this._runspace?.Dispose(); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/AvalonShell/PowerShellInvocation.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Grabacr07/AvalonShell/d0b1362e8ca9f14c816c758a8fc4a1e96923b127/src/AvalonShell/PowerShellInvocation.cs -------------------------------------------------------------------------------- /src/AvalonShell/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Resources; 3 | using System.Runtime.CompilerServices; 4 | using System.Runtime.InteropServices; 5 | using System.Windows; 6 | 7 | // General Information about an assembly is controlled through the following 8 | // set of attributes. Change these attribute values to modify the information 9 | // associated with an assembly. 10 | [assembly: AssemblyTitle("AvalonShell")] 11 | [assembly: AssemblyDescription("Custom PowerShell Host")] 12 | [assembly: AssemblyCompany("grabacr.net")] 13 | [assembly: AssemblyProduct("AvalonShell")] 14 | [assembly: AssemblyCopyright("Copyright © Manato Kameya 2016")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | //In order to begin building localizable applications, set 22 | //CultureYouAreCodingWith in your .csproj file 23 | //inside a . For example, if you are using US english 24 | //in your source files, set the to en-US. Then uncomment 25 | //the NeutralResourceLanguage attribute below. Update the "en-US" in 26 | //the line below to match the UICulture setting in the project file. 27 | 28 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] 29 | 30 | 31 | [assembly: ThemeInfo( 32 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 33 | //(used if a resource is not found in the page, 34 | // or application resource dictionaries) 35 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 36 | //(used if a resource is not found in the page, 37 | // app, or any theme specific resource dictionaries) 38 | )] 39 | 40 | 41 | // Version information for an assembly consists of the following four values: 42 | // 43 | // Major Version 44 | // Minor Version 45 | // Build Number 46 | // Revision 47 | // 48 | // You can specify all the values or you can default the Build and Revision Numbers 49 | // by using the '*' as shown below: 50 | // [assembly: AssemblyVersion("1.0.*")] 51 | [assembly: AssemblyVersion("0.1.*")] 52 | -------------------------------------------------------------------------------- /src/AvalonShell/Themes/Generic.xaml: -------------------------------------------------------------------------------- 1 |  4 | 5 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/AvalonShell/UI/Application.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/AvalonShell/UI/Application.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace AvalonShell.UI 6 | { 7 | partial class Application 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/AvalonShell/UI/Controls/PowerShellConsole.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Specialized; 4 | using System.ComponentModel; 5 | using System.Linq; 6 | using System.Runtime.InteropServices; 7 | using System.Text; 8 | using System.Windows; 9 | using System.Windows.Controls; 10 | using System.Windows.Documents; 11 | using System.Windows.Input; 12 | using System.Windows.Media; 13 | 14 | namespace AvalonShell.UI.Controls 15 | { 16 | public class PowerShellConsole : RichTextBox 17 | { 18 | private InvocationSection _currentSection; 19 | 20 | static PowerShellConsole() 21 | { 22 | DefaultStyleKeyProperty.OverrideMetadata( 23 | typeof(PowerShellConsole), 24 | new FrameworkPropertyMetadata(typeof(PowerShellConsole))); 25 | } 26 | 27 | #region PowerShellHost dependency property 28 | 29 | public static readonly DependencyProperty PowerShellHostProperty = DependencyProperty.Register( 30 | nameof(PowerShellHost), typeof(IPowerShellHost), typeof(PowerShellConsole), new PropertyMetadata(default(IPowerShellHost), HandlePowerShellHostChanged)); 31 | 32 | private static void HandlePowerShellHostChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args) 33 | { 34 | var instance = (PowerShellConsole)sender; 35 | var oldValue = (IPowerShellHost)args.OldValue; 36 | var newValue = (IPowerShellHost)args.NewValue; 37 | 38 | if (oldValue != null) 39 | { 40 | ((INotifyCollectionChanged)oldValue.Invocations).CollectionChanged -= instance.HandleInvocations; 41 | } 42 | if (newValue != null) 43 | { 44 | ((INotifyCollectionChanged)newValue.Invocations).CollectionChanged += instance.HandleInvocations; 45 | var last = newValue.Invocations.LastOrDefault(); 46 | if (last != null) instance.Next(last); 47 | } 48 | } 49 | 50 | public IPowerShellHost PowerShellHost 51 | { 52 | get { return (IPowerShellHost)this.GetValue(PowerShellHostProperty); } 53 | set { this.SetValue(PowerShellHostProperty, value); } 54 | } 55 | 56 | #endregion 57 | 58 | #region ErrorForeground dependency property 59 | 60 | public static readonly DependencyProperty ErrorForegroundProperty = DependencyProperty.Register( 61 | nameof(ErrorForeground), typeof(Brush), typeof(PowerShellConsole), new PropertyMetadata(default(Brush))); 62 | 63 | public Brush ErrorForeground 64 | { 65 | get { return (Brush)this.GetValue(ErrorForegroundProperty); } 66 | set { this.SetValue(ErrorForegroundProperty, value); } 67 | } 68 | 69 | #endregion 70 | 71 | public PowerShellConsole() 72 | { 73 | this.Loaded += (sender, args) => DataObject.AddPastingHandler(this, this.HandlePaste); 74 | this.Unloaded += (sender, args) => DataObject.RemovePastingHandler(this, this.HandlePaste); 75 | 76 | CommandManager.AddPreviewExecutedHandler(this, this.HandleCommandExecuted); 77 | 78 | this.Document.PageWidth = 2000; 79 | this.Document.Blocks.Remove(this.Document.Blocks.FirstBlock); 80 | } 81 | 82 | private void HandleInvocations(object sender, NotifyCollectionChangedEventArgs args) 83 | { 84 | if (!this.Dispatcher.CheckAccess()) 85 | { 86 | this.Dispatcher.Invoke(() => this.HandleInvocations(sender, args)); 87 | return; 88 | } 89 | 90 | if (args.Action == NotifyCollectionChangedAction.Add) 91 | { 92 | this.Next(args.NewItems.OfType().First()); 93 | } 94 | } 95 | 96 | private void Next(IPowerShellInvocation invocation) 97 | { 98 | var section = new InvocationSection(this, invocation); 99 | 100 | this.Document.Blocks.Add(section); 101 | 102 | if (section.CanEditing) 103 | { 104 | this._currentSection = section; 105 | this.CaretPosition = section.Editor.ContentEnd; 106 | } 107 | } 108 | 109 | private void Invoke() 110 | { 111 | if (this._currentSection == null 112 | || this._currentSection.Invocation.Status != InvocationStatus.Ready) 113 | { 114 | return; 115 | } 116 | 117 | var area = this._currentSection.Editor; 118 | var prompt = this._currentSection.Prompt; 119 | var script = new TextRange(area.ContentStart.GetPositionAtOffset(prompt.Length + 1), area.ContentEnd).Text; 120 | 121 | this._currentSection.Invocation.Script = script; 122 | this._currentSection.Invocation.Invoke(); 123 | } 124 | 125 | private void HandleCommandExecuted(object sender, ExecutedRoutedEventArgs args) 126 | { 127 | if (!this.CaretIsInEditArea()) 128 | { 129 | if (args.Command == ApplicationCommands.Cut || 130 | args.Command == ApplicationCommands.Delete) 131 | { 132 | args.Handled = true; 133 | } 134 | } 135 | } 136 | 137 | private void MoveCaretToDocumentEnd() 138 | { 139 | if (!this.CaretIsInEditArea()) 140 | { 141 | this.CaretPosition = this.Document.ContentEnd; 142 | } 143 | } 144 | 145 | protected override void OnPreviewKeyDown(KeyEventArgs e) 146 | { 147 | base.OnPreviewKeyDown(e); 148 | 149 | if (e.Key == Key.Enter) 150 | { 151 | this.Invoke(); 152 | e.Handled = true; 153 | } 154 | else if (this.CaretIsInEditArea()) 155 | { 156 | if (e.Key == Key.Escape) 157 | { 158 | this._currentSection.Invocation.Script = ""; 159 | e.Handled = true; 160 | } 161 | if (e.Key == Key.Up) 162 | { 163 | this._currentSection.Invocation.SetNextHistory(); 164 | e.Handled = true; 165 | } 166 | else if (e.Key == Key.Down) 167 | { 168 | this._currentSection.Invocation.SetPreviousHistory(); 169 | e.Handled = true; 170 | } 171 | else if ((e.Key == Key.Left || e.Key == Key.Back) 172 | && this.CaretIsInLeftMostOfEditArea()) 173 | { 174 | e.Handled = true; 175 | } 176 | } 177 | else 178 | { 179 | if (e.Key == Key.Back || e.Key == Key.Delete) 180 | { 181 | e.Handled = true; 182 | } 183 | else if (Interop.GetCharFromKey(e.Key) != null) 184 | { 185 | this.MoveCaretToDocumentEnd(); 186 | } 187 | } 188 | } 189 | 190 | private void HandlePaste(object sender, DataObjectPastingEventArgs e) 191 | { 192 | var isText = e.SourceDataObject.GetDataPresent(DataFormats.UnicodeText, true); 193 | if (!isText) return; 194 | 195 | var text = e.SourceDataObject.GetData(DataFormats.UnicodeText) as string; 196 | if (text == null) return; 197 | 198 | this.MoveCaretToDocumentEnd(); 199 | } 200 | 201 | private bool CaretIsInEditArea() 202 | => this._currentSection != null && this._currentSection.CaretIsInEditArea(this.CaretPosition); 203 | 204 | private bool CaretIsInLeftMostOfEditArea() 205 | => this._currentSection != null && this._currentSection.CaretIsInLeftMostOfEditArea(this.CaretPosition); 206 | 207 | 208 | private static class Interop 209 | { 210 | private enum MapType : uint 211 | { 212 | // ReSharper disable InconsistentNaming 213 | // ReSharper disable UnusedMember.Local 214 | MAPVK_VK_TO_VSC = 0x0, 215 | MAPVK_VSC_TO_VK = 0x1, 216 | MAPVK_VK_TO_CHAR = 0x2, 217 | MAPVK_VSC_TO_VK_EX = 0x3, 218 | // ReSharper restore UnusedMember.Local 219 | // ReSharper restore InconsistentNaming 220 | } 221 | 222 | [DllImport("user32.dll")] 223 | private static extern int ToUnicode( 224 | uint wVirtKey, 225 | uint wScanCode, 226 | byte[] lpKeyState, 227 | [Out, MarshalAs(UnmanagedType.LPWStr, SizeParamIndex = 4)] StringBuilder pwszBuff, 228 | int cchBuff, 229 | uint wFlags); 230 | 231 | [DllImport("user32.dll")] 232 | private static extern bool GetKeyboardState(byte[] lpKeyState); 233 | 234 | [DllImport("user32.dll")] 235 | private static extern uint MapVirtualKey(uint uCode, MapType uMapType); 236 | 237 | /// 238 | /// 入力されたキーの文字を返し、文字でない場合は null を返します。 239 | /// 240 | public static char? GetCharFromKey(Key key) 241 | { 242 | char? ch = null; 243 | 244 | var virtualKey = KeyInterop.VirtualKeyFromKey(key); 245 | var keyboardState = new byte[256]; 246 | GetKeyboardState(keyboardState); 247 | 248 | var scanCode = MapVirtualKey((uint)virtualKey, MapType.MAPVK_VK_TO_VSC); 249 | var stringBuilder = new StringBuilder(2); 250 | 251 | var result = ToUnicode((uint)virtualKey, scanCode, keyboardState, stringBuilder, stringBuilder.Capacity, 0); 252 | switch (result) 253 | { 254 | case -1: 255 | break; 256 | case 0: 257 | break; 258 | case 1: 259 | ch = stringBuilder[0]; 260 | break; 261 | default: 262 | ch = stringBuilder[0]; 263 | break; 264 | } 265 | 266 | return ch; 267 | } 268 | } 269 | 270 | public class InvocationSection : Section 271 | { 272 | private readonly PowerShellConsole _owner; 273 | 274 | public IPowerShellInvocation Invocation { get; } 275 | 276 | public bool CanEditing { get; private set; } 277 | 278 | public Paragraph Editor { get; } 279 | 280 | public string Prompt { get; } 281 | 282 | public InvocationSection(PowerShellConsole owner, IPowerShellInvocation invocation) 283 | { 284 | if (invocation.Result == null) 285 | { 286 | this._owner = owner; 287 | this.Invocation = invocation; 288 | this.Invocation.PropertyChanged += this.HandleInvocationPropertyChanged; 289 | 290 | this.Prompt = $"[{invocation.Number}] > "; 291 | this.Editor = new Paragraph(); 292 | this.Editor.Inlines.Add(this.Prompt); 293 | this.Blocks.Add(this.Editor); 294 | 295 | this.CanEditing = invocation.Status == InvocationStatus.Ready; 296 | } 297 | else 298 | { 299 | this._owner = owner; 300 | this.Invocation = invocation; 301 | this.SetResult(invocation.Result); 302 | 303 | this.CanEditing = false; 304 | } 305 | } 306 | 307 | private void HandleInvocationPropertyChanged(object sender, PropertyChangedEventArgs args) 308 | { 309 | if (!this.Dispatcher.CheckAccess()) 310 | { 311 | this.Dispatcher.Invoke(() => this.HandleInvocationPropertyChanged(sender, args)); 312 | return; 313 | } 314 | 315 | switch (args.PropertyName) 316 | { 317 | case nameof(IPowerShellInvocation.Script): 318 | { 319 | if (Equals(this, this._owner._currentSection)) 320 | { 321 | var range = new TextRange(this.Editor.ContentStart.GetPositionAtOffset(this.Prompt.Length + 1), this.Editor.ContentEnd); 322 | var script = this.Invocation.Script; 323 | range.Text = script; 324 | this._owner.CaretPosition = this.Editor.ContentEnd; 325 | } 326 | break; 327 | } 328 | case nameof(IPowerShellInvocation.Status): 329 | { 330 | switch (this.Invocation.Status) 331 | { 332 | case InvocationStatus.Ready: 333 | this.CanEditing = true; 334 | break; 335 | 336 | case InvocationStatus.Invoking: 337 | this.CanEditing = false; 338 | break; 339 | 340 | case InvocationStatus.Invoked: 341 | this.CanEditing = false; 342 | this.Invocation.PropertyChanged -= this.HandleInvocationPropertyChanged; 343 | break; 344 | } 345 | break; 346 | } 347 | case nameof(IPowerShellInvocation.Result): 348 | { 349 | this.SetResult(this.Invocation.Result); 350 | break; 351 | } 352 | } 353 | } 354 | 355 | private void SetResult(InvocationResult result) 356 | { 357 | switch (result.Kind) 358 | { 359 | case InvocationResultKind.Empty: 360 | break; 361 | 362 | case InvocationResultKind.Normal: 363 | { 364 | var paragraph = new Paragraph(); 365 | paragraph.Inlines.Add(result.Message); 366 | this.Blocks.Add(paragraph); 367 | break; 368 | } 369 | 370 | case InvocationResultKind.Error: 371 | { 372 | var paragraph = new Paragraph(); 373 | paragraph.Inlines.Add(result.Message); 374 | paragraph.Foreground = this._owner.ErrorForeground; 375 | this.Blocks.Add(paragraph); 376 | this.Blocks.Add(new Paragraph()); 377 | break; 378 | } 379 | } 380 | } 381 | 382 | public bool CaretIsInEditArea(TextPointer caretPostion) 383 | => this.CanEditing 384 | && this.Editor.ContentStart.IsInSameDocument(caretPostion) 385 | && this.Editor.ContentStart.GetOffsetToPosition(caretPostion) - 1 >= this.Prompt.Length; 386 | 387 | public bool CaretIsInLeftMostOfEditArea(TextPointer caretPostion) 388 | => this.CanEditing 389 | && this.Editor.ContentStart.IsInSameDocument(caretPostion) 390 | && this.Editor.ContentStart.GetOffsetToPosition(caretPostion) - 1 == this.Prompt.Length; 391 | } 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /src/AvalonShell/UI/Window.xaml: -------------------------------------------------------------------------------- 1 |  11 | 13 | 14 | -------------------------------------------------------------------------------- /src/AvalonShell/UI/Window.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace AvalonShell.UI 6 | { 7 | partial class Window 8 | { 9 | public Window() 10 | { 11 | var host = new PowerShellHost(); 12 | 13 | this.InitializeComponent(); 14 | this.Console.PowerShellHost = host; 15 | 16 | host.Open(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/AvalonShell/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | --------------------------------------------------------------------------------