feat: Add Get-Calendar command and Debug harnesses

- add Get-Calendar command
- add initial debug harness for local debug
This commit is contained in:
Scott 2026-07-19 00:32:32 +00:00
commit 274d279732
15 changed files with 938 additions and 1 deletions

View file

@ -0,0 +1,216 @@
using System.Collections.ObjectModel;
using System.Globalization;
using System.Management.Automation;
using System.Management.Automation.Host;
using System.Security;
using System.Text;
namespace PowershellHarness;
// A whole bunch of empty implementations just so cmdlets have access to anything within Host.Ui (and probably any cmdlets
// that prompt for information later)
// https://github.com/leechristensen/OffensivePowerShellTasking/blob/master/OffensivePowerShellTasking/CustomPSHost.cs#L13
public class CustomHost : PSHost
{
public override string Name => "Custom Host";
public override Version Version { get; } = new Version(0, 0, 0, 0);
public override Guid InstanceId { get; } = Guid.NewGuid();
private CustomUiHost _ui = new CustomUiHost();
public override CustomUiHost UI => _ui;
public override CultureInfo CurrentCulture { get; } = CultureInfo.CurrentCulture;
public override CultureInfo CurrentUICulture { get; } = CultureInfo.CurrentUICulture;
public CustomHost(int width, int height = 100)
{
_ui.RawUI.WindowSize = new() { Width = width, Height = height };
}
public override void EnterNestedPrompt()
{
throw new NotImplementedException("EnterNestedPrompt is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input.");
}
public override void ExitNestedPrompt()
{
throw new NotImplementedException("ExitNestedPrompt is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input.");
}
public override void NotifyBeginApplication()
{
}
public override void NotifyEndApplication()
{
}
public override void SetShouldExit(int exitCode)
{
}
}
// https://github.com/leechristensen/OffensivePowerShellTasking/blob/d1b498d874948f41e6dc053204c511fa6fc11c9c/OffensivePowerShellTasking/CustomPSHostUserInterface.cs#L8
public class CustomUiHost : PSHostUserInterface
{
// The only stuff that really matters to expose a host for any cmdlets
private readonly CustomRawUiHost _rawUI = new CustomRawUiHost();
public override CustomRawUiHost RawUI => _rawUI;
// Replace StringBuilder with whatever your preferred output method is (e.g. a socket or a named pipe)
public StringBuilder output { get; set; }
public CustomUiHost()
{
output = new StringBuilder();
}
public CustomUiHost(ref StringBuilder sb)
{
output = sb;
}
public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value)
{
//output.Append("!").Append(value);
}
public override void WriteLine()
{
//output.Append("!").Append("\n");
}
public override void WriteLine(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value)
{
//output.Append("!").Append(value + "\n");
}
public override void Write(string value)
{
//output.Append("!").Append(value);
}
public override void WriteDebugLine(string message)
{
//output.Append("!").AppendLine("DEBUG: " + message);
}
public override void WriteErrorLine(string value)
{
//output.Append("!").AppendLine("ERROR: " + value);
}
public override void WriteLine(string value)
{
//output.Append("!").AppendLine(value);
}
public override void WriteVerboseLine(string message)
{
//output.Append("!").AppendLine("VERBOSE: " + message);
}
public override void WriteWarningLine(string message)
{
//output.Append("!").AppendLine("WARNING: " + message);
}
public override void WriteProgress(long sourceId, ProgressRecord record)
{
}
public string Output => output.ToString();
public override Dictionary<string, PSObject> Prompt(string caption, string message, System.Collections.ObjectModel.Collection<FieldDescription> descriptions)
{
throw new NotImplementedException("Prompt is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input.");
}
public override int PromptForChoice(string caption, string message, System.Collections.ObjectModel.Collection<ChoiceDescription> choices, int defaultChoice)
{
throw new NotImplementedException("PromptForChoice is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input.");
}
public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName, PSCredentialTypes allowedCredentialTypes, PSCredentialUIOptions options)
{
throw new NotImplementedException("PromptForCredential1 is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input.");
}
public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName)
{
throw new NotImplementedException("PromptForCredential2 is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input.");
}
public override string ReadLine()
{
throw new NotImplementedException("ReadLine is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input.");
}
public override SecureString ReadLineAsSecureString()
{
throw new NotImplementedException("ReadLineAsSecureString is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input.");
}
}
// https://github.com/leechristensen/OffensivePowerShellTasking/blob/master/OffensivePowerShellTasking/CustomPSRHostRawUserInterface.cs#L8
public class CustomRawUiHost : PSHostRawUserInterface
{
public override ConsoleColor BackgroundColor { get; set; } = ConsoleColor.Black;
public override Size BufferSize { get; set; } = new Size { Width = 100, Height = 1000 };
public override Coordinates CursorPosition { get; set; } = new Coordinates { X = 0, Y = 0 };
public override int CursorSize { get; set; } = 1;
public override void FlushInputBuffer()
{
throw new NotImplementedException("FlushInputBuffer is not implemented.");
}
public override ConsoleColor ForegroundColor { get; set; } = ConsoleColor.White;
public override BufferCell[,] GetBufferContents(Rectangle rectangle)
{
throw new NotImplementedException("GetBufferContents is not implemented.");
}
public override bool KeyAvailable
{
get { throw new NotImplementedException("KeyAvailable is not implemented."); }
}
public override Size MaxPhysicalWindowSize { get; } = new Size
{
Width = int.MaxValue,
Height = int.MaxValue
};
public override Size MaxWindowSize { get; } = new Size { Width = 100, Height = 100 };
public override KeyInfo ReadKey(ReadKeyOptions options)
{
throw new NotImplementedException("ReadKey is not implemented. The script is asking for input, which is a problem since there's no console. Make sure the script can execute without prompting the user for input.");
}
public override void ScrollBufferContents(Rectangle source, Coordinates destination, Rectangle clip, BufferCell fill)
{
throw new NotImplementedException("ScrollBufferContents is not implemented");
}
public override void SetBufferContents(Rectangle rectangle, BufferCell fill)
{
throw new NotImplementedException("SetBufferContents is not implemented.");
}
public override void SetBufferContents(Coordinates origin, BufferCell[,] contents)
{
throw new NotImplementedException("SetBufferContents is not implemented");
}
public override Coordinates WindowPosition { get; set; } = new Coordinates { X = 0, Y = 0 };
public override Size WindowSize { get; set; } = new Size { Width = 120, Height = 100 };
public override string WindowTitle { get; set; } = "";
}

View file

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.6.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PowershellModule\PowershellModule.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,145 @@
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Text;
using PowershellModule.Calendar;
namespace PowershellHarness;
// https://github.com/FuseCP/FuseCP/blob/278a19dc06949600f25a1b4ed74d0419a8fa3fc2/FuseCP/Sources/FuseCP.Providers.HostedSolution.SfB2015/SfBBase.cs#L224
// some useful code in here tbh
class Program
{
static void Main(string[] args)
{
// I've gotta work out how to get the rider terminal to act closer to powershell because .PadLeft in powershell
// will correctly output lines when output with WriteOutput, but the Jetbrains debug worker (or whatever
// it is that rider spawns), has none of that and operates in its own world.
// Which is fine if you never want new lines.
// Ideally you'd just launch pwsh.exe and attach to that, but _that_ has its own issues as well.
// The new line issue might be related to how I've set up the host seeing as I just did a bunch of copy paste shit
// from something I found on github to get it across the line so I could debug it.
// I'll eventually revisit those hosts and rewrite them properly once I start adding more commands, and if I care.
// Threads & variables has all the visuals I need after all.
#if DEBUG
if (!System.Diagnostics.Debugger.IsAttached)
{
Console.BackgroundColor = ConsoleColor.Yellow;
Console.WriteLine(CenterText("Running without a debugger attached may result in lines not being output correctly"));
Console.ResetColor();
}
#endif
Console.WriteLine(CenterText(" PowerShell Debug Harness "));
Console.WriteLine($"Reported console window size: {Console.WindowWidth}x{Console.WindowHeight}");
Console.WriteLine($"Reported console buffer size: {Console.BufferWidth}x{Console.BufferHeight}");
Console.WriteLine(CenterText(" WIDTH ", '-'));
var host = new CustomHost(Console.WindowWidth);
var runspace = InitialisePowershellHost(host);
// InvokeCommand(runspace, GetCalendarCommand.FullName);
foreach (var day in Enum.GetValues<DayOfWeek>())
{
InvokeCommand(runspace, GetCalendarCommand.FullName, [
CreateCommand(nameof(GetCalendarCommand.MarkedDaySymbol), "faker"),
CreateCommand(nameof(GetCalendarCommand.StartOfWeek), day.ToString()),
CreateCommand(nameof(GetCalendarCommand.Date), "22/6/26"),
// CreateCommand(nameof(GetCalendarCommand.AlignRight))
]);
}
// InvokeCommand(runspace, GetCalendarCommand.FullName, [
// new CommandParameter(nameof(GetCalendarCommand.MarkedDaySymbol), "🤫"),
// new CommandParameter(nameof(GetCalendarCommand.StartOfWeek), DayOfWeek.Saturday)
// ]);
// InvokeCommand(runspace, GetCalendarCommand.FullName, [new CommandParameter(nameof(GetCalendarCommand.StartOfWeek), "Sunday")]);
}
private static CommandParameter CreateCommand(string name, string? argument = null)
{
if (argument is not null)
{
return new CommandParameter(name, argument);
}
return new CommandParameter(name);
}
static string CenterText(string text, char paddingChar = ' ')
{
return text.PadLeft((Console.WindowWidth + text.Length) / 2, paddingChar).PadRight(Console.WindowWidth, paddingChar);
}
/// <summary>
/// Initialises a PowerShell session and returns an open Runspace
/// </summary>
/// <param name="host"></param>
/// <returns></returns>
static Runspace InitialisePowershellHost(CustomHost host)
{
// Create the initial session state for the host. Yes, that 2 on the end does indicate that the underlying
// implementation for this is C++
// Welcome to the lands of fuck all documentation and figuring shit out from an increasingly shit internet
// where finding non-slop answers gets harder by the day as people close sources of information to prevent
// scraping. Fuck every single person involved modern AI.
// It wasn't much better finding documentation about the System.Management.* namespace before that, but it's
// definitely a lot worse because of it
var initialSessionState = InitialSessionState.CreateDefault2();
// No idea what the helpFileName should be. As is common with _a lot_ of the System.Management.* namespace,
// fuck all is actually documented with comments, or documented at all!
// From https://learn.microsoft.com/en-us/powershell/scripting/developer/hosting/creating-a-constrained-runspace?view=powershell-7.6
// (which yes, this section _is_ under a legacy category!), it's perfectly fine to leave it as null.
// We also don't really care as we're just doing this so we can test commands without having to deal with Rider
// and it's quirks around runnning a powershell terminal and attaching to it. Yeah it technically works, but
// it's way too common to end up with Rider refusing to build correctly or ensure the right dll is used for the module
var getCalendarCommand = new SessionStateCmdletEntry(GetCalendarCommand.FullName, typeof(GetCalendarCommand), null);
initialSessionState.Commands.Add(getCalendarCommand);
// Create a runspace from the state, open and return it
var runspace = RunspaceFactory.CreateRunspace(host, initialSessionState);
// A runspace is technically disposable, but we're not reimplementing a full host and we won't be doing anything
// that would require a fresh runspace multiple times over (yet), so we can just treat the lifetime of the
// disposable as application lifetime
runspace.Open();
return runspace;
}
static void InvokeCommand(Runspace runspace, string command, IEnumerable<CommandParameter>? parameters = null)
{
// Not too sure on the difference of runspace vs PowerShell here. Doesn't seem to make a difference either way
// and this is just a debug harness so it doesn't really matter for now
using var pipeline = runspace.CreatePipeline();
// using var powershell = PowerShell.Create(runspace);
var cmd = new Command(command);
if (parameters is not null)
{
foreach (var commandParameter in parameters)
{
cmd.Parameters.Add(commandParameter);
}
}
pipeline.Commands.Add(cmd);
// powershell.Commands.AddCommand(cmd);
try
{
var results = pipeline.Invoke();
// var results = powershell.Invoke();
foreach (var result in results)
{
Console.Write(result);
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"Unable to execute command {command}");
Console.Error.WriteLine(ex.GetBaseException().Message);
}
}
}