Compare commits

...

2 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
d1e07080e8 CmdPal: Add calendar page to time/date dock band clock item
Clicking the time/date item in the CmdPal dock now opens a calendar
page showing the current month with today's date highlighted in bold.

- Add CalendarPage (ContentPage) that renders a markdown table calendar
  respecting the locale's first day of week and abbreviated day names
- Wire NowDockBand.Command to CalendarPage instead of NoOpCommand
- Add timedate_calendar_page_name resource string
- Add CalendarPageTests covering structure, today highlighting, and dock wiring

Co-authored-by: niels9001 <9866362+niels9001@users.noreply.github.com>
Agent-Logs-Url: https://github.com/microsoft/PowerToys/sessions/1d52e678-b258-441c-a409-1de12a42cb2a
2026-03-25 20:36:48 +00:00
copilot-swe-agent[bot]
e918714f22 Initial plan 2026-03-25 20:24:33 +00:00
5 changed files with 251 additions and 1 deletions

View File

@@ -0,0 +1,138 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using Microsoft.CmdPal.Ext.TimeDate.Pages;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.TimeDate.UnitTests
{
[TestClass]
public class CalendarPageTests
{
private CultureInfo originalCulture;
private CultureInfo originalUiCulture;
[TestInitialize]
public void Setup()
{
originalCulture = CultureInfo.CurrentCulture;
CultureInfo.CurrentCulture = new CultureInfo("en-us", false);
originalUiCulture = CultureInfo.CurrentUICulture;
CultureInfo.CurrentUICulture = new CultureInfo("en-us", false);
}
[TestCleanup]
public void Cleanup()
{
CultureInfo.CurrentCulture = originalCulture;
CultureInfo.CurrentUICulture = originalUiCulture;
}
[TestMethod]
public void CalendarPage_HasExpectedId()
{
var page = new CalendarPage();
Assert.AreEqual("com.microsoft.cmdpal.timedate.calendarPage", page.Id);
}
[TestMethod]
public void CalendarPage_HasNonEmptyName()
{
var page = new CalendarPage();
Assert.IsFalse(string.IsNullOrEmpty(page.Name));
}
[TestMethod]
public void GetContent_ReturnsOneMarkdownContent()
{
var page = new CalendarPage();
var content = page.GetContent();
Assert.IsNotNull(content);
Assert.AreEqual(1, content.Length);
Assert.IsInstanceOfType(content[0], typeof(MarkdownContent));
}
[TestMethod]
public void GetContent_TitleIsCurrentMonthAndYear()
{
var page = new CalendarPage();
page.GetContent();
var expected = DateTime.Now.ToString("MMMM yyyy", CultureInfo.CurrentCulture);
Assert.AreEqual(expected, page.Title);
}
[TestMethod]
public void GetContent_BodyContainsTodayBold()
{
var page = new CalendarPage();
var content = page.GetContent();
var markdown = content[0] as MarkdownContent;
var today = DateTime.Now.Day.ToString(CultureInfo.InvariantCulture);
Assert.IsTrue(markdown!.Body.Contains($"**{today}**"), "Today's day number should be bold in the calendar markdown.");
}
[TestMethod]
public void GetContent_BodyContainsDayHeaders()
{
var page = new CalendarPage();
var content = page.GetContent();
var markdown = content[0] as MarkdownContent;
// The body should contain abbreviated day names as column headers
var dayNames = CultureInfo.CurrentCulture.DateTimeFormat.AbbreviatedDayNames;
foreach (var dayName in dayNames)
{
Assert.IsTrue(markdown!.Body.Contains(dayName), $"Calendar should contain day header '{dayName}'.");
}
}
[TestMethod]
public void GetContent_BodyContainsAllDaysOfCurrentMonth()
{
var page = new CalendarPage();
var content = page.GetContent();
var markdown = content[0] as MarkdownContent;
var daysInMonth = DateTime.DaysInMonth(DateTime.Now.Year, DateTime.Now.Month);
for (var day = 1; day <= daysInMonth; day++)
{
Assert.IsTrue(markdown!.Body.Contains(day.ToString(CultureInfo.InvariantCulture)), $"Calendar should contain day {day}.");
}
}
[TestMethod]
public void GetDockBands_CommandIsCalendarPage()
{
var provider = new TimeDateCommandsProvider();
var bands = provider.GetDockBands();
Assert.IsNotNull(bands);
Assert.AreEqual(1, bands.Length);
// The band wraps a list; dig into its items to find the NowDockBand command.
var wrappedBand = bands[0] as WrappedDockItem;
Assert.IsNotNull(wrappedBand, "Dock band should be a WrappedDockItem.");
var items = wrappedBand.Items;
Assert.IsTrue(items.Length > 0, "Dock band should have at least one item.");
var command = items[0].Command;
Assert.IsInstanceOfType(command, typeof(CalendarPage), "The clock dock item's command should be a CalendarPage.");
}
}
}

View File

@@ -0,0 +1,99 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Text;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.TimeDate.Pages;
internal sealed partial class CalendarPage : ContentPage
{
public CalendarPage()
{
Id = "com.microsoft.cmdpal.timedate.calendarPage";
Name = Resources.timedate_calendar_page_name;
Icon = Icons.TimeDateExtIcon;
}
public override IContent[] GetContent()
{
var now = DateTime.Now;
Title = now.ToString("MMMM yyyy", CultureInfo.CurrentCulture);
return [new MarkdownContent(GenerateCalendarMarkdown(now))];
}
private static string GenerateCalendarMarkdown(DateTime now)
{
var sb = new StringBuilder();
var culture = CultureInfo.CurrentCulture;
var firstDayOfMonth = new DateTime(now.Year, now.Month, 1);
var daysInMonth = DateTime.DaysInMonth(now.Year, now.Month);
var firstDayOfWeek = culture.DateTimeFormat.FirstDayOfWeek;
var abbreviatedDayNames = culture.DateTimeFormat.AbbreviatedDayNames;
// Header row: abbreviated day names starting from the culture's first day of week
sb.Append('|');
for (var i = 0; i < 7; i++)
{
var dayIndex = ((int)firstDayOfWeek + i) % 7;
sb.Append($" {abbreviatedDayNames[dayIndex]} |");
}
sb.AppendLine();
// Separator
sb.Append('|');
for (var i = 0; i < 7; i++)
{
sb.Append(":-:|");
}
sb.AppendLine();
// Calculate how many blank cells appear before day 1
var firstDayDow = (int)firstDayOfMonth.DayOfWeek;
var offset = ((firstDayDow - (int)firstDayOfWeek) + 7) % 7;
sb.Append('|');
for (var i = 0; i < offset; i++)
{
sb.Append(" |");
}
var col = offset;
for (var day = 1; day <= daysInMonth; day++)
{
if (day == now.Day)
{
sb.Append($" **{day}** |");
}
else
{
sb.Append($" {day} |");
}
col++;
if (col == 7 && day < daysInMonth)
{
sb.AppendLine();
sb.Append('|');
col = 0;
}
}
// Pad the last row with empty cells
while (col > 0 && col < 7)
{
sb.Append(" |");
col++;
}
sb.AppendLine();
return sb.ToString();
}
}

View File

@@ -959,5 +959,14 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
return ResourceManager.GetString("timedate_copy_time_command_name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Calendar.
/// </summary>
public static string timedate_calendar_page_name {
get {
return ResourceManager.GetString("timedate_calendar_page_name", resourceCulture);
}
}
}
}

View File

@@ -444,4 +444,8 @@
<value>Copy date</value>
<comment>Name of a command to copy the current date to the clipboard</comment>
</data>
<data name="timedate_calendar_page_name" xml:space="preserve">
<value>Calendar</value>
<comment>Name of the calendar page shown when clicking the clock in the dock</comment>
</data>
</root>

View File

@@ -75,7 +75,7 @@ internal sealed partial class NowDockBand : ListItem
public NowDockBand()
{
Command = new NoOpCommand() { Id = "com.microsoft.cmdpal.timedate.dockBand" };
Command = new CalendarPage();
_copyTimeCommand = new CopyTextCommand(string.Empty) { Name = Resources.timedate_copy_time_command_name };
_copyDateCommand = new CopyTextCommand(string.Empty) { Name = Resources.timedate_copy_date_command_name };
MoreCommands = [