<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
  <channel><title>Mads Kristensen</title>
<description>A blog about everything technology.</description>
<generator>Miniblog.Core</generator>
<link>https://madskristensen.azurewebsites.net/</link>
<item>
  <title>Why I built Home Automation Cookbook, and how I keep it useful</title>
  <link>https://madskristensen.azurewebsites.net/blog/why-i-built-home-automation-cookbook-and-how-i-keep-it-useful/</link>
  <description>&lt;figure&gt;&lt;img src="https://www.homeautomationcookbook.com/assets/img/hero.jpg" alt="Illustration of a connected living room representing Home Automation Cookbook" width="1200" height="630"&gt;&lt;figcaption&gt;&lt;em&gt;Home Automation Cookbook collects practical patterns for making a home helpful without making it harder to use.&lt;/em&gt;&lt;/figcaption&gt;&lt;/figure&gt;

&lt;p&gt;I have written about home automation on this blog for years. In 2019, I published a collection of &lt;a href="https://madskristensen.net/blog/home-automation-ideas/"&gt;home automation ideas&lt;/a&gt; that worked well for my family. The important part was never a particular hub, protocol, or brand. It was the logic behind each idea and whether it quietly made everyday life easier.&lt;/p&gt;

&lt;p&gt;That original post kept growing in my head. A long list of ideas is useful for inspiration, but it is not a great format for explaining everything that makes an automation dependable: what triggers it, which conditions matter, how someone overrides it, and what happens when a sensor or service fails.&lt;/p&gt;

&lt;p&gt;That is why I built &lt;a href="https://www.homeautomationcookbook.com/"&gt;Home Automation Cookbook&lt;/a&gt;. It gives each household job enough room to become a reusable recipe rather than a platform-specific trick.&lt;/p&gt;

&lt;h2&gt;The problem I wanted to solve&lt;/h2&gt;

&lt;p&gt;Most smart-home content begins with technology. It starts with a hub, a product, or a feature and then looks for something to do with it. I wanted to reverse that.&lt;/p&gt;

&lt;p&gt;The Cookbook starts with an ordinary burden:&lt;/p&gt;

&lt;ul&gt;&lt;li&gt;Remembering to move laundry after the washer finishes.&lt;/li&gt;&lt;li&gt;Checking whether the garage door was left open.&lt;/li&gt;&lt;li&gt;Adjusting the thermostat when a window is open.&lt;/li&gt;&lt;li&gt;Reaching for a light switch while carrying something through the house.&lt;/li&gt;&lt;/ul&gt;

&lt;p&gt;Only after the job is clear does the recipe discuss sensors, switches, platforms, or products. This makes the idea easier to adapt, and it prevents the site from becoming a catalog of gadgets looking for a purpose.&lt;/p&gt;

&lt;p&gt;The other problem is that an automation can work perfectly for the person who created it and still be bad for everyone else. My family is the real QA team. Kids, guests, and anyone who did not configure the system should still be able to use the house without learning an app or remembering special rules.&lt;/p&gt;

&lt;p&gt;A useful automation should remove work without removing control.&lt;/p&gt;

&lt;h2&gt;The wall switch is part of the design&lt;/h2&gt;

&lt;p&gt;One principle appears throughout the site: the physical control should keep working.&lt;/p&gt;

&lt;p&gt;If motion turns on a light, the wall switch should still behave like a wall switch. If the hub is restarting, the internet is unavailable, or the automation made the wrong decision, nobody should be trapped behind the technology.&lt;/p&gt;

&lt;p&gt;I learned this through normal household use. An automatic bathroom light that turns on a fraction too late can collide with muscle memory. Someone reaches for the switch just as the automation turns on the light, and their tap immediately turns it off again. The rule technically worked, but the experience did not.&lt;/p&gt;

&lt;p&gt;The fix was not to explain the automation more carefully. It was to move the sensor and make the response fast enough that the room behaved naturally.&lt;/p&gt;

&lt;p&gt;This is also why the site emphasizes local execution where a platform supports it. Cloud services and voice assistants can be useful extras, but basic household behavior should degrade gracefully when they are unavailable.&lt;/p&gt;

&lt;figure&gt;&lt;img src="https://www.homeautomationcookbook.com/assets/img/diagrams/motion-lighting-flow.svg" alt="Motion is detected, darkness is checked, and the light turns on while the wall switch remains available" width="960" height="280"&gt;&lt;figcaption&gt;&lt;em&gt;A recipe makes the decision path and the ordinary fallback explicit.&lt;/em&gt;&lt;/figcaption&gt;&lt;/figure&gt;

&lt;h2&gt;Recipes instead of walkthroughs&lt;/h2&gt;

&lt;p&gt;I have personally used Amazon Echo with Alexa, SmartThings, and Hubitat. Hubitat is what I use today. Those experiences inform the site, but the core recipes remain platform-neutral.&lt;/p&gt;

&lt;p&gt;A typical recipe describes:&lt;/p&gt;

&lt;ul&gt;&lt;li&gt;The trigger that begins the automation.&lt;/li&gt;&lt;li&gt;The conditions that must also be true.&lt;/li&gt;&lt;li&gt;The action the house should take.&lt;/li&gt;&lt;li&gt;Any wait or timeout.&lt;/li&gt;&lt;li&gt;The condition that stops or reverses it.&lt;/li&gt;&lt;li&gt;The manual override.&lt;/li&gt;&lt;li&gt;The likely failure modes.&lt;/li&gt;&lt;/ul&gt;

&lt;p&gt;That structure is deliberately close to plain English. A reader can understand the behavior before translating it into a Hubitat rule, a SmartThings routine, an Alexa routine, or another system.&lt;/p&gt;

&lt;p&gt;Platform notes are supporting material rather than the recipe itself. I also distinguish between firsthand experience and technical fit. For example, I have not personally run Home Assistant, so the site does not present Home Assistant guidance as a hands-on walkthrough.&lt;/p&gt;

&lt;h2&gt;How the site is built&lt;/h2&gt;

&lt;p&gt;The implementation is intentionally simple. Home Automation Cookbook is a static &lt;a href="https://jekyllrb.com/"&gt;Jekyll&lt;/a&gt; site made primarily from Markdown, HTML, CSS, JavaScript, YAML data, and shared layouts.&lt;/p&gt;

&lt;h3&gt;Visual Studio is my workshop&lt;/h3&gt;

&lt;p&gt;I built the site in Visual Studio. That may sound like an unusual choice for a Jekyll site, but it gives me one workspace for the entire project: Markdown recipes, Liquid layouts, YAML data, CSS, JavaScript, images, and Git history. I can make a site-wide change, review the diff, run the site locally, and push it without switching between a collection of specialized tools.&lt;/p&gt;

&lt;p&gt;Visual Studio is not a requirement for contributing to the site, and it is not part of the production runtime. It is simply the workshop I prefer. Jekyll turns the source into static pages, and GitHub Actions deploys them to GitHub Pages after I push to the main branch.&lt;/p&gt;

&lt;p&gt;The source is public in the &lt;a href="https://github.com/madskristensen/HomeAutomationCookbook"&gt;Home Automation Cookbook GitHub repository&lt;/a&gt;. A push to the main branch runs a GitHub Actions workflow, builds the production site with Jekyll, and deploys the result to GitHub Pages.&lt;/p&gt;

&lt;p&gt;Static generation is a good fit for this project. The pages are fast, the content is easy to review in a pull request, and the repository contains the history of how the guidance changes. There is no application server or database to maintain just to publish a recipe.&lt;/p&gt;

&lt;p&gt;The site currently contains 49 automation recipes across lighting, climate, safety and security, appliances, notifications, entertainment, and daily routines. Shared Jekyll layouts keep those pages consistent, while structured category data powers the recipe directories and starting points.&lt;/p&gt;

&lt;p&gt;The automation directory is organized around the reader's goal rather than the implementation. Instead of asking which integration someone uses, it asks whether they want to stop checking an appliance, move through the house more easily, reduce repeated adjustments, or coordinate a household transition.&lt;/p&gt;

&lt;figure&gt;&lt;img src="https://www.homeautomationcookbook.com/assets/img/social/recipes/appliances/washer-done-notification.svg" alt="Home Automation Cookbook recipe for sending one notification when the washer finishes" width="1200" height="630"&gt;&lt;figcaption&gt;&lt;em&gt;Recipes begin with a household job, then describe the trigger, checks, and action without tying the idea to one platform.&lt;/em&gt;&lt;/figcaption&gt;&lt;/figure&gt;

&lt;h2&gt;Writing the failure before calling it finished&lt;/h2&gt;

&lt;p&gt;The happy path is usually the easy part of home automation. The more useful questions are:&lt;/p&gt;

&lt;ul&gt;&lt;li&gt;What if the sensor stops reporting?&lt;/li&gt;&lt;li&gt;What if its last known state is stale?&lt;/li&gt;&lt;li&gt;What if someone manually changes the device?&lt;/li&gt;&lt;li&gt;What if a guest behaves differently from the person who built the rule?&lt;/li&gt;&lt;li&gt;What remains available when the hub or internet is down?&lt;/li&gt;&lt;/ul&gt;

&lt;p&gt;Every substantive recipe includes failure modes because a rule is not complete until the household knows how it fails. A notification should not claim success when the final state was never confirmed. An unavailable contact sensor should not silently remain "closed" forever. A manual change should not immediately be undone by an overconfident automation.&lt;/p&gt;

&lt;p&gt;This way of writing also improves the automations in my own home. Explaining a rule exposes assumptions that are easy to miss while looking at a visual rule editor.&lt;/p&gt;

&lt;h2&gt;The site is a cookbook, not a blueprint&lt;/h2&gt;

&lt;p&gt;A recipe is a starting pattern. Every home has different rooms, routines, tolerances, devices, and people. The goal is not to copy my house. It is to understand why a pattern works, identify the signals it needs, and adapt it without losing the fallback behavior.&lt;/p&gt;

&lt;p&gt;I recommend beginning with one small, reversible win. Motion lighting in a hallway or a washer-finished notification is easy to observe and easy to override. Run it for a week. Watch how people actually use it. Change one assumption at a time.&lt;/p&gt;

&lt;p&gt;That loop is how the site grows too. I started the repository in November 2025, and it has evolved through hundreds of small revisions to its recipes, navigation, safety guidance, platform notes, and visual design. The work is less about declaring an automation complete and more about making each explanation clearer and more dependable.&lt;/p&gt;

&lt;h2&gt;What I hope the site becomes&lt;/h2&gt;

&lt;p&gt;I want Home Automation Cookbook to be useful to someone before they buy a device and after the novelty of that device is gone.&lt;/p&gt;

&lt;p&gt;Before buying, it should help define the household job and the capabilities required to solve it. After installation, it should help test the rule, preserve manual control, recognize failure, and adjust the behavior as the household changes.&lt;/p&gt;

&lt;p&gt;Most of all, I want the site to keep the technology in a supporting role. The lasting benefit is not that a home contains more smart devices. It is that there is less to remember, fewer repeated checks and adjustments, and more confidence that ordinary controls will still work.&lt;/p&gt;

&lt;p&gt;You can browse the recipes at &lt;a href="https://www.homeautomationcookbook.com/automation/index.html"&gt;homeautomationcookbook.com&lt;/a&gt;, or inspect and contribute to the &lt;a href="https://github.com/madskristensen/HomeAutomationCookbook"&gt;source on GitHub&lt;/a&gt;.&lt;/p&gt;</description>
  <author>test@example.com</author>
  <category>home automation</category>
  <guid isPermaLink="false">https://madskristensen.azurewebsites.net/blog/why-i-built-home-automation-cookbook-and-how-i-keep-it-useful/</guid>
  <pubDate>Thu, 17 Sep 2026 22:34:22 GMT</pubDate>
</item>
<item>
  <title>Solution Colors: Telling Visual Studio Windows Apart at a Glance</title>
  <link>https://madskristensen.azurewebsites.net/blog/solution-colors-telling-visual-studio-windows-apart-at-a-glance/</link>
  <description>&lt;p&gt;When several Visual Studio instances are open, identifying the right one becomes a surprisingly repetitive task. Two windows showing different checkouts of the same solution can look almost identical. Reading the title works, but a small visual cue is easier to spot.&lt;/p&gt;

&lt;p&gt;That's the problem behind &lt;a href="https://marketplace.visualstudio.com/items?itemName=MadsKristensen.SolutionColors"&gt;Solution Colors&lt;/a&gt;. Inspired by the &lt;a href="https://marketplace.visualstudio.com/items?itemName=johnpapa.vscode-peacock"&gt;Peacock extension for VS Code&lt;/a&gt; and a &lt;a href="https://developercommunity.visualstudio.com/t/Per-SolutionFolder-Color-Theme/608136?space=8&amp;amp;ftype=idea"&gt;Visual Studio feature request&lt;/a&gt;, it associates a color with a solution or folder without replacing your editor theme.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://madskristensen.net/posts/files/solution-colors-windows_639246604550024077.png" alt="Overlapping Visual Studio windows with purple, cyan, green, and orange bottom borders." /&gt;&lt;/p&gt;

&lt;h2&gt;Give each solution a visual identity&lt;/h2&gt;

&lt;p&gt;Right-click the solution in Solution Explorer, choose &lt;strong&gt;Set Solution Color&lt;/strong&gt;, and select a color. The predefined choices use the familiar document-tab color palette, and &lt;strong&gt;Custom...&lt;/strong&gt; opens a color picker when you want something different. Open Folder workspaces are supported too.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://madskristensen.net/posts/files/solution-colors-context-menu_639246604552996906.png" alt="Solution Explorer context menu with Set Solution Color expanded and Gold highlighted." /&gt;&lt;/p&gt;

&lt;p&gt;The color can appear around the window, behind the solution name in the title bar, and in taskbar thumbnails. Taskbar icon overlays are also available, with the documented limitation that taskbar items need to be ungrouped. You can choose the surfaces that help you and leave the others alone.&lt;/p&gt;

&lt;p&gt;Under &lt;strong&gt;Tools &amp;gt; Options &amp;gt; Environment &amp;gt; Fonts and Colors &amp;gt; Solution Colors&lt;/strong&gt;, you can adjust border placement and thickness or enable automatic color assignment. Automatic mode selects from the palette using a hash of the solution path; explicitly saved colors take precedence.&lt;/p&gt;

&lt;h2&gt;A small extension with two kinds of integration&lt;/h2&gt;

&lt;p&gt;The project is an in-process VSIX built with the Visual Studio SDK and the Community Toolkit. Its &lt;code&gt;ToolkitPackage&lt;/code&gt; registers commands and listens for solution and folder open/close events. The menu lives in a VSCT command table, while the color commands share a generic &lt;code&gt;BaseCommand&amp;lt;T&amp;gt;&lt;/code&gt; implementation.&lt;/p&gt;

&lt;p&gt;That is the conventional part. Coloring the existing window chrome is less conventional.&lt;/p&gt;

&lt;p&gt;The implementation searches the WPF visual tree for named shell elements such as &lt;code&gt;BottomDockBorder&lt;/code&gt; and &lt;code&gt;PART_SolutionNameTextBlock&lt;/code&gt;. It changes brushes and border thickness rather than introducing a new tool window. For the title label, it also uses reflection to access the foreground property and Visual Studio's &lt;code&gt;ColorUtilities.CompareContrastWithBlackAndWhite&lt;/code&gt; to choose black or white text.&lt;/p&gt;

&lt;p&gt;This is an important tradeoff for extension authors: finding an element in the shell's visual tree is not the same as having a dedicated, stable extensibility contract for it. Names and structure can change. The lookup code is worth isolating, and these integrations need testing against the Visual Studio versions you support.&lt;/p&gt;

&lt;h2&gt;Decoration must not get in the way&lt;/h2&gt;

&lt;p&gt;One revealing detail is hit testing. The extension disables hit testing on the non-top borders it colors so they don't intercept mouse input. But it deliberately leaves the top element alone: &lt;code&gt;MainWindowTitleBar&lt;/code&gt; contains interactive UI, and disabling hit testing there would also block menu interaction.&lt;/p&gt;

&lt;p&gt;That distinction is easy to overlook when the feature appears to be "just a border." Decorative changes still participate in layout and input routing.&lt;/p&gt;

&lt;p&gt;Timing matters too. Package initialization does not guarantee that every shell element is ready. The startup path makes a bounded series of colorization attempts, with delays between them. UI changes switch to the main thread, while Git branch file reads are dispatched to a background task.&lt;/p&gt;

&lt;p&gt;Taskbar integration uses a different mechanism: WPF's &lt;code&gt;TaskbarItemInfo&lt;/code&gt;, noninteractive &lt;code&gt;ThumbButtonInfo&lt;/code&gt; entries, and an overlay image. There is no need to reach into the taskbar's visual tree.&lt;/p&gt;

&lt;h2&gt;Branch colors are a state problem, not just a brush problem&lt;/h2&gt;

&lt;p&gt;The settings offer one color across branches, a separate color per branch, or a combined gradient. Assignments are stored as simple &lt;code&gt;branch:color&lt;/code&gt; lines in a &lt;code&gt;color.txt&lt;/code&gt; file, normally under the solution's &lt;code&gt;.vs&lt;/code&gt; directory. An option puts the file in the root instead. The parser also accepts the older single-color format.&lt;/p&gt;

&lt;p&gt;There is a compatibility detail worth knowing: the implementation uses the literal key &lt;code&gt;master&lt;/code&gt; for its shared/base color. That is an internal convention, not automatic discovery of a repository's configured default branch.&lt;/p&gt;

&lt;p&gt;A recent fix illustrates why separating decisions from rendering helps. In per-branch mode, a branch with an explicit color should remain colored even when no base color has been assigned. The code now expresses the removal decision in &lt;code&gt;ShouldRemoveColorization&lt;/code&gt;, with focused unit tests covering the different modes and automatic-color behavior.&lt;/p&gt;

&lt;p&gt;That is a useful pattern beyond this extension: put the state rules in a testable method, then let the UI code apply the result. You shouldn't need to launch an experimental Visual Studio instance just to verify whether a missing base setting overrides a branch-specific one.&lt;/p&gt;

&lt;h2&gt;Try it, or borrow the ideas&lt;/h2&gt;

&lt;p&gt;If you regularly juggle Visual Studio windows, &lt;a href="https://marketplace.visualstudio.com/items?itemName=MadsKristensen.SolutionColors"&gt;install Solution Colors from the Marketplace&lt;/a&gt; and give your solutions a visual identity.&lt;/p&gt;

&lt;p&gt;If you're building extensions, &lt;a href="https://github.com/madskristensen/SolutionColors"&gt;browse the source on GitHub&lt;/a&gt;. Start with &lt;code&gt;SolutionColorsPackage&lt;/code&gt; for the lifecycle, &lt;code&gt;ColorHelper&lt;/code&gt; for the shell integration, and the tests for the branch-color rules. A few pixels of color turn out to be a useful case study in extending the IDE without getting in the user's way.&lt;/p&gt;</description>
  <author>test@example.com</author>
  <category>visual studio</category>
  <category>extensions</category>
  <guid isPermaLink="false">https://madskristensen.azurewebsites.net/blog/solution-colors-telling-visual-studio-windows-apart-at-a-glance/</guid>
  <pubDate>Thu, 10 Sep 2026 18:08:54 GMT</pubDate>
</item>
<item>
  <title>Lore for Visual Studio: Source control without leaving the IDE</title>
  <link>https://madskristensen.azurewebsites.net/blog/lore-for-visual-studio-source-control-without-leaving-the-ide/</link>
  <description>&lt;p&gt;&lt;a href="https://github.com/EpicGames/lore"&gt;Lore&lt;/a&gt; is a new source control system from Epic Games. It is designed for fast, distributed workflows, with local repositories that work offline and optional Lore servers when you need to collaborate.&lt;/p&gt;

&lt;p&gt;Lore for Visual Studio brings that workflow directly into the IDE. It is also a practical example of what it takes to add a new source-control provider to Visual Studio: integrate onboarding, status, changes, commits, history, branches, authentication, and native diff and merge experiences into the places developers already use.&lt;/p&gt;

&lt;p&gt;The extension lets you onboard a solution or folder, see file status in Solution Explorer, review changes, commit, manage branches, inspect history, and synchronize with a Lore server - all without dropping to a terminal.&lt;/p&gt;

&lt;h2&gt;A source-control provider that feels native&lt;/h2&gt;

&lt;p&gt;Visual Studio already provides the integration points a source-control provider needs to participate in the everyday development workflow. Lore for Visual Studio uses them to surface repository status in Solution Explorer, provide commands in solution and file context menus, and offer dedicated Changes and History tool windows. Rather than making developers learn a separate workflow, the extension makes Lore available alongside the IDE experiences they already know.&lt;/p&gt;

&lt;h2&gt;Start with a local repository&lt;/h2&gt;

&lt;p&gt;Open a solution or folder and select &lt;strong&gt;Add to Source Control&lt;/strong&gt; in the Visual Studio status bar. Choose &lt;strong&gt;Lore&lt;/strong&gt;, then create a local repository.&lt;/p&gt;

&lt;p&gt;Local repositories are fully offline. You can commit, create branches, switch branches, and browse history while all content remains on your machine. When you are ready to collaborate, create or connect to a Lore server and push your work.&lt;/p&gt;

&lt;p&gt;&lt;img src="/posts/files/lorevs-add-to-source-control_639235472182075744.png" alt="Add a solution to Lore" /&gt;&lt;/p&gt;

&lt;p&gt;You can also clone an existing Lore repository directly from Solution Explorer. The extension checks that the server is reachable, clones the working tree, and records the remote configuration so push and pull are ready immediately.&lt;/p&gt;

&lt;p&gt;&lt;img src="/posts/files/lorevs-clone-repository_639235472185109683.png" alt="Clone a Lore repository" /&gt;&lt;/p&gt;

&lt;h2&gt;Source-control status where you expect it&lt;/h2&gt;

&lt;p&gt;Once a solution is under Lore source control, status glyphs appear beside files in Solution Explorer. They refresh automatically as documents are saved, and the Lore Changes window provides a manual refresh command whenever you need one.&lt;/p&gt;

&lt;p&gt;File context menus include the essentials:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Undo Changes&lt;/strong&gt; to restore the committed version&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compare with Unmodified&lt;/strong&gt; to view a native Visual Studio diff&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ignore and Untrack Item&lt;/strong&gt; to add a file to &lt;code&gt;.loreignore&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Moves and renames performed in Solution Explorer are tracked automatically. The extension also uses Visual Studio's shared file-change service to detect changes made outside the IDE.&lt;/p&gt;

&lt;h2&gt;Review and commit changes&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Lore Changes&lt;/strong&gt; window provides a dedicated commit experience modeled after Visual Studio's Git Changes window.&lt;/p&gt;

&lt;p&gt;&lt;img src="/posts/files/lorevs-changes-window_639235472178571660.png" alt="Lore Changes window" /&gt;&lt;/p&gt;

&lt;p&gt;It presents changed files in a repository-root folder tree, including status badges for modified, added, deleted, renamed, and copied files.&lt;/p&gt;

&lt;p&gt;You can select exactly which files belong in the next commit. Folder checkboxes are tri-state, the header checkbox selects everything at once, and your selection survives automatic refreshes while you write a commit message.&lt;/p&gt;

&lt;p&gt;From the window, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Commit selected files or commit and push in one operation&lt;/li&gt;
&lt;li&gt;Amend the latest revision&lt;/li&gt;
&lt;li&gt;Open native Visual Studio diffs for individual files&lt;/li&gt;
&lt;li&gt;Discard a file's changes&lt;/li&gt;
&lt;li&gt;Pull, push, and refresh from the toolbar&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Browse history without touching the working tree&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;Lore History&lt;/strong&gt; tool window shows a read-only log for the repository associated with the open solution or folder.&lt;/p&gt;

&lt;p&gt;History is paged, newest first, so even repositories with thousands of revisions remain responsive. Each revision shows its message, short hash, author, and local commit time. Select a revision to load only the files that it changed, then double-click a file to compare it with its direct parent in Visual Studio's native diff viewer.&lt;/p&gt;

&lt;p&gt;The window stays in sync with the workspace and refreshes automatically after relevant Lore operations, such as commits, pulls, branch changes, and merges.&lt;/p&gt;

&lt;h2&gt;Branching and merging&lt;/h2&gt;

&lt;p&gt;The branch picker in the Lore Changes window lets you switch branches, create a branch from the current revision, and merge another branch into the one you are working on.&lt;/p&gt;

&lt;p&gt;Lore is offline-first. When the needed content is already cached locally, switching and merging do not require a network round trip. If content has been evicted from the local cache, Lore fetches it from the configured server and clearly reports when the server is unavailable.&lt;/p&gt;

&lt;p&gt;When a merge conflicts, Visual Studio's built-in three-way merge experience opens for each conflicted file. Resolve the conflicts, accept the results, and Lore completes the merge commit.&lt;/p&gt;

&lt;h2&gt;Sign in only when it matters&lt;/h2&gt;

&lt;p&gt;There is no separate sign-in command. If a server-backed operation such as cloning, pushing, or pulling requires authentication, Lore asks whether to sign in and opens the default browser. After sign-in completes, the original operation is retried.&lt;/p&gt;

&lt;p&gt;That keeps the experience simple: work locally when you can, authenticate only when a server operation actually needs it.&lt;/p&gt;

&lt;h2&gt;Try it out&lt;/h2&gt;

&lt;p&gt;Lore for Visual Studio is available from the &lt;a href="https://marketplace.visualstudio.com/items?itemName=MadsKristensen.LoreVS"&gt;Visual Studio Marketplace&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The extension is open source, and contributions are welcome on &lt;a href="https://github.com/madskristensen/LoreVS"&gt;GitHub&lt;/a&gt;. If you are building a source-control integration of your own, the project is also an example of how a provider can fit into Visual Studio's existing source-control workflow.&lt;/p&gt;</description>
  <author>test@example.com</author>
  <category>visual studio</category>
  <category>extensions</category>
  <category>source control</category>
  <guid isPermaLink="false">https://madskristensen.azurewebsites.net/blog/lore-for-visual-studio-source-control-without-leaving-the-ide/</guid>
  <pubDate>Fri, 28 Aug 2026 20:54:50 GMT</pubDate>
</item>
<item>
  <title>Building Max Line Length: Making EditorConfig Formatting Visible in Visual Studio</title>
  <link>https://madskristensen.azurewebsites.net/blog/building-max-line-length-making-editorconfig-formatting-visible-in-visual-studio/</link>
  <description>&lt;p&gt;Line length is one of those conventions that teams agree on easily and enforce inconsistently. EditorConfig gives us a shared vocabulary with &lt;code&gt;max_line_length&lt;/code&gt;, but Visual Studio does not natively draw a boundary for it or apply it during formatting.&lt;/p&gt;

&lt;p&gt;That gap is what inspired &lt;a href="https://github.com/madskristensen/MaxLineLength"&gt;Max Line Length&lt;/a&gt;. The extension makes the effective EditorConfig value visible in the editor and adds a carefully constrained reflow pipeline for formatting and Code Cleanup.&lt;/p&gt;

&lt;h2&gt;Starting with the effective EditorConfig value&lt;/h2&gt;
&lt;p&gt;The extension does not introduce a second configuration system. A repository can declare its convention as usual:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[*.{cs,vb}]
max_line_length = 120&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For each text view, Max Line Length resolves the effective value for the document, including nested configuration files and matching sections. If the value is missing, invalid, or set to &lt;code&gt;unset&lt;/code&gt;, the ruler is removed.&lt;/p&gt;
&lt;p&gt;The interesting part is keeping the editor responsive when configuration changes. The adornment listens for refresh notifications, resolves file-based settings away from the UI thread, then switches back to the main thread before updating the view. A version counter prevents stale asynchronous results from replacing newer configuration values.&lt;/p&gt;

&lt;h2&gt;Drawing a ruler without interfering with editing&lt;/h2&gt;
&lt;p&gt;The ruler is implemented as a Visual Studio editor adornment. The extension obtains an &lt;code&gt;IWpfTextView&lt;/code&gt;, creates an &lt;code&gt;IAdornmentLayer&lt;/code&gt;, and owns a WPF &lt;code&gt;Line&lt;/code&gt; element. Its horizontal position is calculated from the formatted line source, indentation, column width, and the resolved maximum length.&lt;/p&gt;
&lt;p&gt;The adornment follows layout changes, so it tracks scrolling, zoom, font changes, and viewport updates. It is not hit-testable and does not intercept mouse or keyboard input. The extension also checks for Visual Studio's diff view role and does not display the ruler there.&lt;/p&gt;
&lt;p&gt;Color customization uses Visual Studio's format map rather than a private option. The &lt;strong&gt;Max Line Length ruler&lt;/strong&gt; format is exposed through Fonts and Colors, and the adornment reads the configured foreground brush when it redraws.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://madskristensen.net/posts/files/max-line-length-ruler_639227523019405959.png" alt="Visual Studio editor showing the Max Line Length vertical ruler."&gt;&lt;/p&gt;

&lt;h2&gt;One reflow service for several editor commands&lt;/h2&gt;
&lt;p&gt;Formatting support began as command-specific logic, but that quickly creates inconsistencies. The extension now centralizes the decision in &lt;code&gt;DocumentReflowService&lt;/code&gt;. It determines whether a content type can be reflowed, selects the appropriate engine, and applies the resulting text changes.&lt;/p&gt;
&lt;p&gt;Visual Studio's MEF composition supplies the classifier service used for comment-aware languages. For C# and Visual Basic, the extension uses language-specific reflow engines for lists and expressions. For classified-comment content types such as JavaScript, TypeScript, C++, CSS, LESS, SCSS, and SQL, it asks the editor classifier for comment spans and limits changes to those spans.&lt;/p&gt;
&lt;p&gt;Markdown and plain text use separate engines so that structural constructs remain intact. Markdown headings, tables, lists, block quotes, code fences, inline code, HTML, front matter, and hard line breaks are protected instead of being treated as ordinary prose.&lt;/p&gt;

&lt;h2&gt;Being conservative is a feature&lt;/h2&gt;
&lt;p&gt;Reflow changes source text, so the extension intentionally avoids transformations that could alter meaning. C# and Visual Basic string and character literals are left alone. Structured XML documentation elements, examples, lists, and preformatted content are preserved while ordinary documentation prose can be wrapped.&lt;/p&gt;
&lt;p&gt;Formatter suppression markers provide an explicit escape hatch. The extension recognizes &lt;code&gt;@formatter:off&lt;/code&gt; and &lt;code&gt;@formatter:on&lt;/code&gt; in the relevant comment syntax and excludes the protected region from reflow.&lt;/p&gt;
&lt;p&gt;Selection formatting is similarly constrained. It operates on complete selected lines or complete C# and Visual Basic syntax constructs rather than making partial edits that could produce surprising results.&lt;/p&gt;

&lt;h2&gt;Adding Code Cleanup without changing existing profiles&lt;/h2&gt;
&lt;p&gt;The Code Cleanup integration is opt-in. The fixer is exported through Visual Studio's &lt;code&gt;ICodeCleanUpFixerProvider&lt;/code&gt; and exposes a localized fix ID named &lt;strong&gt;Reflow lines to configured maximum length&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;When enabled in a cleanup profile, the fixer obtains the document path, resolves the same &lt;code&gt;ReflowOptions&lt;/code&gt; used by the formatting commands, and sends the buffer through &lt;code&gt;DocumentReflowService&lt;/code&gt;. This keeps document cleanup and manual formatting consistent.&lt;/p&gt;
&lt;p&gt;The fixer is disabled unless the user explicitly adds it to a profile. That default matters: installing an extension should not silently change existing cleanup behavior. The integration supports document cleanup and cleanup on save, while project and solution cleanup are not intercepted.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://madskristensen.net/posts/files/max-line-length-reflow_639227523039371958.gif" alt="Visual Studio reflowing a document to the configured maximum line length."&gt;&lt;/p&gt;

&lt;h2&gt;Lessons for other Visual Studio extensions&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Use the editor's effective configuration instead of creating a parallel options system.&lt;/li&gt;
&lt;li&gt;Keep UI adornments lightweight and recalculate them from the current formatted line source.&lt;/li&gt;
&lt;li&gt;Centralize behavior shared by commands, cleanup, and future entry points.&lt;/li&gt;
&lt;li&gt;Use classifications and syntax-aware boundaries to avoid editing executable code accidentally.&lt;/li&gt;
&lt;li&gt;Make automated cleanup opt-in when it can change source text.&lt;/li&gt;
&lt;li&gt;Design asynchronous refresh paths so stale results cannot overwrite newer state.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Try it or inspect the implementation&lt;/h2&gt;
&lt;p&gt;Install &lt;a href="https://marketplace.visualstudio.com/items?itemName=MadsKristensen.MaxLineLength"&gt;Max Line Length from the Visual Studio Marketplace&lt;/a&gt;, or download the latest CI build from the &lt;a href="https://www.vsixgallery.com/extension/MaxLineLength.bbc727b4-1dfb-4d9e-a159-032529f396ed"&gt;Open VSIX Gallery&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The complete source is available on &lt;a href="https://github.com/madskristensen/MaxLineLength"&gt;GitHub&lt;/a&gt;. The project is a useful example of combining editor adornments, EditorConfig resolution, MEF services, content classifiers, language-aware text transformations, and Code Cleanup integration in a focused Visual Studio extension.&lt;/p&gt;</description>
  <author>test@example.com</author>
  <category>visual studio</category>
  <category>extensions</category>
  <category>editorconfig</category>
  <guid isPermaLink="false">https://madskristensen.azurewebsites.net/blog/building-max-line-length-making-editorconfig-formatting-visible-in-visual-studio/</guid>
  <pubDate>Wed, 19 Aug 2026 16:05:17 GMT</pubDate>
</item>
<item>
  <title>Building Bookmark Studio: bookmarks that belong to the codebase</title>
  <link>https://madskristensen.azurewebsites.net/blog/building-bookmark-studio-bookmarks-that-belong-to-the-codebase/</link>
  <description>&lt;p&gt;Visual Studio bookmarks are useful for marking a line and returning to it later. The problem is that a line number alone does not say why the location matters, how it relates to other locations, or whether the rest of the team should know about it.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=MadsKristensen.BookmarkStudio"&gt;Bookmark Studio&lt;/a&gt; treats a bookmark as a small piece of project knowledge. A bookmark can have a name, note, color, folder, and one of nine direct-navigation shortcuts. It can stay private in the &lt;code&gt;.vs&lt;/code&gt; folder, live at the workspace level in source control, or be global across solutions.&lt;/p&gt;
&lt;p&gt;That sounds like a straightforward feature, but it touches several distinct parts of Visual Studio extensibility: editor tags and glyphs, text-buffer tracking, commands, a searchable tool window, options, solution lifecycle events, and persistent storage. The interesting part is making those pieces behave like one feature rather than a collection of extension points.&lt;/p&gt;
&lt;p&gt;&lt;img src="/posts/files/bookmark-studio-manager_639220100628782353.png" alt="Bookmark Manager tool window showing color-coded bookmarks organized in folders." /&gt;&lt;/p&gt;
&lt;h2&gt;Keep the editor layer fast&lt;/h2&gt;
&lt;p&gt;The visible bookmark in the editor margin starts with a MEF-exported &lt;code&gt;IViewTaggerProvider&lt;/code&gt;. It creates an &lt;code&gt;ITagger&amp;lt;BookmarkGlyphTag&amp;gt;&lt;/code&gt; for primary text document views, while an &lt;code&gt;IGlyphFactory&lt;/code&gt; turns each tag into the colored square shown in the glyph margin. If a bookmark has a shortcut from 1 through 9, the number is drawn inside the glyph.&lt;/p&gt;
&lt;p&gt;The tag also carries the bookmark label, note, color, and shortcut number. That gives the glyph factory enough information to build a useful tooltip and context menu without reading the bookmark file. A second tagger produces &lt;code&gt;OverviewMarkTag&lt;/code&gt; instances so the same bookmarks appear as colored markers in the editor scrollbar.&lt;/p&gt;
&lt;p&gt;A useful rule for editor extensions is to keep &lt;code&gt;GetTags&lt;/code&gt; cheap. Visual Studio can ask for tags often, including while scrolling and editing. Bookmark Studio does not load JSON or filter the complete bookmark collection on every request. The shared session owns the current bookmark state, and each tagger caches only the bookmarks for its document. When the session reports a change, the tagger replaces that small cache and raises &lt;code&gt;TagsChanged&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That separation is broadly useful: do I/O and normalization in a service, then give the editor a read-optimized snapshot.&lt;/p&gt;
&lt;h2&gt;A persisted line number is not an editor position&lt;/h2&gt;
&lt;p&gt;A bookmark must be serializable, so its stored location includes a document path and line number. An open text buffer is more dynamic. Insert three lines above a bookmark and the persisted number is immediately stale.&lt;/p&gt;
&lt;p&gt;Bookmark Studio handles that mismatch with &lt;code&gt;ITrackingPoint&lt;/code&gt;. While a document is open, each bookmark gets a tracking point anchored inside the line content. The editor moves that point as the buffer changes, which keeps the glyph attached to the intended code instead of the original numeric line. The tagger can still fall back to the stored line number when no tracking point exists.&lt;/p&gt;
&lt;p&gt;There is an additional wrinkle: users can explicitly drag a glyph to another line. The tagger therefore distinguishes an editor-tracked move from an explicit bookmark move before deciding whether to preserve or rebuild the tracking point. The custom &lt;code&gt;MouseProcessorBase&lt;/code&gt; implementation calculates the target editor line, displays a drop indicator, updates the bookmark, and refreshes the manager if it is open.&lt;/p&gt;
&lt;p&gt;&lt;img src="/posts/files/bookmark-studio-drag-drop_639220100634749316.gif" alt="Dragging a numbered bookmark glyph to a different line in the Visual Studio editor." /&gt;&lt;/p&gt;
&lt;p&gt;The lesson is that storage coordinates and live editor coordinates are different concepts. Persist a stable representation, but use the text model's tracking primitives while a buffer is active.&lt;/p&gt;
&lt;h2&gt;Reuse Visual Studio instead of recreating it&lt;/h2&gt;
&lt;p&gt;The Bookmark Manager is a WPF control hosted in a &lt;code&gt;BaseToolWindow&lt;/code&gt;. Its surrounding experience comes from Visual Studio rather than custom imitations.&lt;/p&gt;
&lt;p&gt;The package registers the window with &lt;code&gt;ProvideToolWindow&lt;/code&gt;, docks it alongside Solution Explorer, and assigns a toolbar declared through VSCT to &lt;code&gt;ToolWindowPane.ToolBar&lt;/code&gt;. Search uses the tool window's native search box by enabling &lt;code&gt;SearchEnabled&lt;/code&gt; and implementing &lt;code&gt;IVsSearchTask&lt;/code&gt;. Commands and keyboard bindings are also declared through VSCT, including direct navigation with &lt;code&gt;Alt+Shift+1&lt;/code&gt; through &lt;code&gt;Alt+Shift+9&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This approach matters for more than appearance. Native command placement participates in Visual Studio's command routing, toolbar customization, keyboard system, and accessibility behavior. The same principle applies to the search box: using the host's search contract gives the tool window familiar behavior without building another search control.&lt;/p&gt;
&lt;p&gt;Bookmark Studio also offers an opt-in bridge from Visual Studio's built-in bookmark gestures. &lt;code&gt;VS.Commands.InterceptAsync&lt;/code&gt; handles commands such as toggle, next, previous, and clear-in-document. The first toggle can prompt the user, and the setting determines whether later commands stop in Bookmark Studio or continue to Visual Studio's native implementation. The extension adds its own direct gestures as well, so interception is a choice rather than a requirement.&lt;/p&gt;
&lt;p&gt;&lt;img src="/posts/files/bookmark-studio-toolbar_639220100625484895.png" alt="Visual Studio toolbar menu listing numbered bookmarks for quick navigation." /&gt;&lt;/p&gt;
&lt;h2&gt;Make sharing explicit in the storage model&lt;/h2&gt;
&lt;p&gt;Bookmarks are stored with &lt;code&gt;System.Text.Json&lt;/code&gt; in &lt;code&gt;.bookmarks.json&lt;/code&gt;. The file is a folder tree containing bookmark IDs, relative document paths, line numbers, labels, notes, colors, shortcut slots, and manual sort indexes.&lt;/p&gt;
&lt;p&gt;There are three storage scopes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Personal bookmarks live under the solution's &lt;code&gt;.vs&lt;/code&gt; folder.&lt;/li&gt;
&lt;li&gt;Workspace bookmarks live beside the solution or at the Git repository root and can be committed.&lt;/li&gt;
&lt;li&gt;Global bookmarks live in &lt;code&gt;%USERPROFILE%\.bookmarks.json&lt;/code&gt; and use absolute paths because they are machine-specific.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Workspace discovery walks upward from the solution directory and reuses the first bookmarks file it finds. This is important for repositories where the solution sits below the root. New workspace files record &lt;code&gt;&amp;quot;documentPathRoot&amp;quot;: &amp;quot;bookmarksFile&amp;quot;&lt;/code&gt;, which tells the loader to resolve relative document paths from the bookmarks file itself. Existing files without that marker retain the older solution-relative behavior.&lt;/p&gt;
&lt;p&gt;That small format marker avoids a common portability bug. A relative path is only meaningful when both writer and reader agree on its base directory. If a team-shared file may move higher in a repository, the base must travel with the format or be defined by it.&lt;/p&gt;
&lt;h2&gt;Naming can use the editor's understanding of code&lt;/h2&gt;
&lt;p&gt;When name prompting is enabled, Bookmark Studio does not have to settle for the current line as the label. A MEF service queries Visual Studio's classification system and ranks identifiers such as methods, properties, fields, and types. It supports view classifiers first, then projection buffers and tag aggregators as fallbacks.&lt;/p&gt;
&lt;p&gt;The complete fallback order is pragmatic: selected text, classified identifier, word under the caret, file name, trimmed line text, and finally &lt;code&gt;Bookmark&lt;/code&gt;. Duplicate labels receive a numeric suffix.&lt;/p&gt;
&lt;p&gt;This is a good middle ground for editor tooling. The extension gets language-aware suggestions from classifications that Visual Studio already produces, without taking a dependency on a specific language compiler or syntax tree.&lt;/p&gt;
&lt;h2&gt;The extension points form one state machine&lt;/h2&gt;
&lt;p&gt;The package loads in the background for solutions, Open Folder workspaces, and the no-solution context because global bookmarks remain useful without a solution. Solution and folder events invalidate the cached path and refresh the shared session. Editor taggers, commands, and the tool window all observe or update that same session.&lt;/p&gt;
&lt;p&gt;That shared state is the architectural center of Bookmark Studio. The editor should not own persistence, the tool window should not be the only source of truth, and commands should not need to know where JSON lives. Each surface delegates bookmark operations to services, then reacts to the resulting state change.&lt;/p&gt;
&lt;p&gt;For extension authors, the reusable ideas are straightforward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Keep taggers read-only and inexpensive.&lt;/li&gt;
&lt;li&gt;Use tracking points for live buffers instead of treating line numbers as stable positions.&lt;/li&gt;
&lt;li&gt;Prefer native tool window search, VSCT commands, and host chrome over look-alike WPF controls.&lt;/li&gt;
&lt;li&gt;Define the base of relative paths as part of a shareable file format.&lt;/li&gt;
&lt;li&gt;Put cross-surface state behind a service so commands, editors, and tool windows stay synchronized.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Bookmark Studio is available on the &lt;a href="https://marketplace.visualstudio.com/items?itemName=MadsKristensen.BookmarkStudio"&gt;Visual Studio Marketplace&lt;/a&gt;. The complete source, including the editor providers, tool window, command table, storage format, and tests, is on &lt;a href="https://github.com/madskristensen/BookmarkStudio"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;</description>
  <author>test@example.com</author>
  <category>visual studio</category>
  <category>vsix</category>
  <guid isPermaLink="false">https://madskristensen.azurewebsites.net/blog/building-bookmark-studio-bookmarks-that-belong-to-the-codebase/</guid>
  <pubDate>Tue, 11 Aug 2026 01:54:47 GMT</pubDate>
</item>
<item>
  <title>Building CodeShot: turning Visual Studio editor state into a shareable PNG</title>
  <link>https://madskristensen.azurewebsites.net/blog/building-codeshot-turning-visual-studio-editor-state-into-a-shareable-png/</link>
  <description>&lt;p&gt;Turning a code sample into an image sounds simple until the image needs to look like the code inside Visual Studio.&lt;/p&gt;

&lt;p&gt;Copying text into a design tool loses editor colors and formatting. Taking a normal screen capture includes tabs, margins, toolbars, and whatever else happens to be visible. It also leaves no convenient way to emphasize an expression or cover a value before sharing the image.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=madskristensen.CodeShot"&gt;CodeShot&lt;/a&gt; addresses that workflow inside Visual Studio. It takes inspiration from CodeSnap, but its implementation is shaped by the Visual Studio editor and shell.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src="/posts/files/codeshot-workflow_639217263672831843.gif" alt="Creating and resizing a code screenshot in Visual Studio"&gt;
  &lt;figcaption&gt;Select code, open CodeShot, adjust the result, and copy or save the PNG without leaving Visual Studio.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h2&gt;Rebuilding the editor instead of photographing it&lt;/h2&gt;

&lt;p&gt;The basic workflow starts with an editor selection. CodeShot opens a tool window, creates a preview using the current syntax colors, and immediately copies the initial image. The preview can add line numbers, a title bar, backgrounds, padding, shadows, and a configurable export scale.&lt;/p&gt;

&lt;p&gt;The important implementation detail is that the editor screenshot is not a bitmap of the editor. CodeShot reconstructs the selected text as WPF content.&lt;/p&gt;

&lt;p&gt;It obtains the active &lt;code&gt;IVsTextView&lt;/code&gt; through &lt;code&gt;IVsTextManager&lt;/code&gt;, converts it to an &lt;code&gt;IWpfTextView&lt;/code&gt; with &lt;code&gt;IVsEditorAdaptersFactoryService&lt;/code&gt;, and reads the selected &lt;code&gt;SnapshotSpan&lt;/code&gt; values. Common indentation can then be removed without changing the underlying document.&lt;/p&gt;

&lt;p&gt;Syntax colors come from two MEF services:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code&gt;IViewClassifierAggregatorService&lt;/code&gt; supplies the classification spans.&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;IClassificationFormatMapService&lt;/code&gt; maps each classification type to the colors currently used by the editor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rather than asking the classifier about every line, CodeShot classifies the enclosing selection once. It then walks the ordered spans and slices them into WPF &lt;code&gt;Run&lt;/code&gt; elements for each selected line. Unclassified gaps use the editor's default foreground, while classifications can contribute their own foreground and background brushes.&lt;/p&gt;

&lt;p&gt;This approach has two useful properties. The result follows the active Visual Studio color settings, and the capture surface can be laid out independently of the editor viewport. Long selections are not clipped just because the tool window is small.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src="/posts/files/codeshot-tool-window_639217263648067002.png" alt="CodeShot tool window displaying a syntax-colored code screenshot"&gt;
  &lt;figcaption&gt;The preview is reconstructed from editor text and classifications rather than cropped from the screen.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h2&gt;Rendering at an explicit scale&lt;/h2&gt;

&lt;p&gt;The completed WPF surface is rendered through a &lt;code&gt;VisualBrush&lt;/code&gt; into a &lt;code&gt;RenderTargetBitmap&lt;/code&gt;. Export scale is explicit instead of being derived from monitor DPI. That makes the same selection produce the same pixel dimensions on different displays.&lt;/p&gt;

&lt;p&gt;There are practical limits around this step. CodeShot rejects dimensions above 16,384 pixels and caps the total pixel count before allocating the render target. The resulting &lt;code&gt;BitmapSource&lt;/code&gt; is frozen before it crosses thread boundaries.&lt;/p&gt;

&lt;p&gt;The clipboard also needs more care than a call to &lt;code&gt;Clipboard.SetImage&lt;/code&gt;. Some paste targets discard alpha from the normal WPF bitmap format, so CodeShot places both the compatibility bitmap and an encoded PNG on the clipboard. The clipboard data is copied rather than referenced, allowing the image to remain available after Visual Studio closes.&lt;/p&gt;

&lt;p&gt;There is an optional plain-text clipboard representation too. That keeps code searchable when an application prefers text over images. If the screenshot contains a redaction, CodeShot deliberately omits the original text so the clipboard cannot reveal what the PNG hides.&lt;/p&gt;

&lt;h2&gt;Annotations are part of the capture surface&lt;/h2&gt;

&lt;p&gt;Annotations are ordinary WPF elements layered over the preview. Rectangles, arrows, expression highlights, text notes, and opaque redactions share a controller responsible for drawing, selection, moving, resizing, and erasing.&lt;/p&gt;

&lt;p&gt;Crop operations and annotations use the same bounded edit-history model. This keeps undo and redo predictable while preventing an editing session from retaining an unlimited number of image and geometry states.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src="/posts/files/codeshot-annotations_639217263647484300.png" alt="CodeShot preview with annotations highlighting important code"&gt;
  &lt;figcaption&gt;Annotations are rendered into the exported image, with redactions also changing clipboard behavior.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;h2&gt;Capturing a tool window is a different problem&lt;/h2&gt;

&lt;p&gt;Text reconstruction works well for editor selections, but it cannot capture Error List, Test Explorer, Solution Explorer, or a tool window containing Win32 or WebView content. Those surfaces have to be captured from the pixels currently displayed.&lt;/p&gt;

&lt;p&gt;CodeShot adds a command to Visual Studio's docked-window context menu through VSCT. When invoked, it uses &lt;code&gt;IVsMonitorSelection&lt;/code&gt; to find the current &lt;code&gt;IVsWindowFrame&lt;/code&gt; and &lt;code&gt;IVsWindowFrame4.GetWindowScreenRect&lt;/code&gt; to obtain its screen coordinates.&lt;/p&gt;

&lt;figure&gt;
  &lt;img src="/posts/files/codeshot-tool-window-context-menu_639217263646610319.png" alt="Take Screenshot command in a Visual Studio tool window context menu"&gt;
  &lt;figcaption&gt;The command is attached to Visual Studio's native docked-window context menu.&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;Finding the content rectangle is only the beginning. A docked group may contain sibling tabs, rounded shell chrome, transparent corners, and content rendered outside the WPF visual tree.&lt;/p&gt;

&lt;p&gt;CodeShot combines several mechanisms:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;UI Automation locates the selected tab and its docking container.&lt;/li&gt;
  &lt;li&gt;The WPF visual tree supplies a mask for the rounded Visual Studio frame.&lt;/li&gt;
  &lt;li&gt;&lt;code&gt;Graphics.CopyFromScreen&lt;/code&gt; captures the actual visible pixels, including Win32 and WebView content.&lt;/li&gt;
  &lt;li&gt;The mask removes sibling tabs and leaves pixels outside the rounded frame transparent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because UI Automation runs away from the UI thread, the target can change while its geometry is being inspected. Before copying pixels, CodeShot verifies that the selected COM frame, bounds, root window, and visibility still match the original target. Captures are serialized with a &lt;code&gt;SemaphoreSlim&lt;/code&gt; so two commands cannot race over transient shell state.&lt;/p&gt;

&lt;p&gt;This design has an honest constraint: pixel capture can only reproduce what is visible. The target must remain unobscured. That tradeoff is preferable to pretending every Visual Studio surface can be rendered reliably through WPF.&lt;/p&gt;

&lt;h2&gt;Menus and modal dialogs complicate threading&lt;/h2&gt;

&lt;p&gt;Menus, context menus, cascading flyouts, and modal dialogs are even more temporary. CodeShot starts a five-second countdown so the user can open the desired surface, then performs discovery and capture off the Visual Studio UI thread.&lt;/p&gt;

&lt;p&gt;That last part matters for modal dialogs. A modal WPF or Windows Forms dialog owns the UI thread while it is open. Resuming the delayed operation on that thread would postpone capture until the dialog had already closed.&lt;/p&gt;

&lt;p&gt;The capture code examines the foreground window and the window under the pointer. UI Automation identifies visible menu items and expanded flyouts, while Win32 window enumeration finds native &lt;code&gt;#32768&lt;/code&gt; menu windows. DWM extended frame bounds are preferred when available so shadows and invisible borders do not become part of the result.&lt;/p&gt;

&lt;h2&gt;Let Visual Studio own the command experience&lt;/h2&gt;

&lt;p&gt;The tool window itself uses &lt;code&gt;BaseToolWindow&lt;/code&gt; from the Community Visual Studio Toolkit, but its toolbar, menus, context-menu command, icons, and keyboard bindings are declared through VSCT.&lt;/p&gt;

&lt;p&gt;The tool window's persistence GUID also acts as a command scope. Combined with &lt;code&gt;ProvideKeyBindingTable&lt;/code&gt;, this allows familiar shortcuts such as Ctrl+C, Ctrl+S, Ctrl+Z, and Ctrl+Y to operate inside CodeShot without replacing their global Visual Studio behavior.&lt;/p&gt;

&lt;p&gt;That is a broadly useful extension pattern: when a feature belongs to Visual Studio chrome, use Visual Studio commands and command scopes instead of recreating the experience with WPF buttons and global key handlers.&lt;/p&gt;

&lt;h2&gt;Small boundaries make the extension easier to maintain&lt;/h2&gt;

&lt;p&gt;The first version concentrated substantial behavior in the tool-window control. As the capture modes grew, reusable pieces were extracted for selection normalization, PNG encoding, file naming, atomic saving, font discovery, capture sizing, and bounded history.&lt;/p&gt;

&lt;p&gt;Those boundaries also made the non-Visual Studio portions testable with ordinary MSTest tests. Visual Studio integration remains in the package and capture layers, while geometry, naming, indentation, and state transitions can be exercised without launching an experimental instance.&lt;/p&gt;

&lt;p&gt;The main lesson from CodeShot is that image encoding is the easy part. A Visual Studio extension like this lives at the boundaries between editor snapshots, MEF services, WPF layout, COM frames, UI Automation, Win32 windows, the clipboard, and the shell's threading rules. Treating each boundary explicitly makes the workflow feel simple even when the implementation is not.&lt;/p&gt;

&lt;p&gt;You can install &lt;a href="https://marketplace.visualstudio.com/items?itemName=madskristensen.CodeShot"&gt;CodeShot from the Visual Studio Marketplace&lt;/a&gt; or inspect the complete implementation in the &lt;a href="https://github.com/madskristensen/CodeShot"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;</description>
  <author>test@example.com</author>
  <category>visual studio</category>
  <category>vsix</category>
  <guid isPermaLink="false">https://madskristensen.azurewebsites.net/blog/building-codeshot-turning-visual-studio-editor-state-into-a-shareable-png/</guid>
  <pubDate>Fri, 07 Aug 2026 19:07:37 GMT</pubDate>
</item>
<item>
  <title>Schemastore.org: The journey of a quiet revolution</title>
  <link>https://madskristensen.azurewebsites.net/blog/schemastore-journey-of-a-quiet-revolution-in-development/</link>
  <description>&lt;p class="MsoNormal"&gt;Let me take you back to a moment that might feel familiar: you&amp;rsquo;re staring at a JSON configuration file, wrestling with field names and structure, hoping you&amp;rsquo;ve gotten it right. Your IDE is offering no help, and documentation is scattered or nonexistent. Frustrating, right?&lt;/p&gt;
&lt;p class="MsoNormal"&gt;Years ago, I found myself in that exact spot one too many times. As I sat there, tweaking and testing, it hit me: there had to be a better way. Little did I know that moment of frustration would spark an idea that would evolve into what you know today as &lt;strong&gt;&lt;a href="https://www.schemastore.org/json/"&gt;Schemastore.org&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;The Problem&lt;/h2&gt;
&lt;p class="MsoNormal"&gt;Back then, JSON schemas weren&amp;rsquo;t the ubiquitous helpers they are now. Sure, the technology existed, but it wasn&amp;rsquo;t easy to find schemas for the tools you were using. If you needed a schema for your project, you either had to dig through obscure documentation or&amp;mdash;more often&amp;mdash;roll up your sleeves and build it yourself.&lt;/p&gt;
&lt;p class="MsoNormal"&gt;It was a time sink. And worse, it was entirely avoidable.&lt;/p&gt;
&lt;p class="MsoNormal"&gt;The real problem wasn&amp;rsquo;t the complexity of schemas; it was the lack of a centralized place to find them. Developers were solving the same problems over and over, each in isolation, when a shared resource could have made all the difference.&lt;/p&gt;
&lt;h2&gt;The Turning Point&lt;/h2&gt;
&lt;p class="MsoNormal"&gt;So, I decided to create that shared resource. The idea was simple: &lt;strong&gt;a public &lt;a href="https://github.com/schemastore/schemastore/"&gt;repository&lt;/a&gt; where anyone could find and contribute JSON schemas&lt;/strong&gt;. It wouldn&amp;rsquo;t be fancy&amp;mdash;just a straightforward way to save developers from hours of unnecessary frustration.&lt;/p&gt;
&lt;p class="MsoNormal"&gt;When I launched Schemastore.org, I didn&amp;rsquo;t have big expectations. I thought it might help a few people, but I never imagined how quickly it would resonate with the development community.&lt;/p&gt;
&lt;h2&gt;The Rise of a Community&lt;/h2&gt;
&lt;p class="MsoNormal"&gt;What happened next was extraordinary. Developers from all over the world began contributing. They sent pull requests for schemas I&amp;rsquo;d never heard of fixed errors I hadn&amp;rsquo;t spotted, and offered suggestions that made the site better in ways I couldn&amp;rsquo;t have predicted.&lt;/p&gt;
&lt;p class="MsoNormal"&gt;It wasn&amp;rsquo;t just about the schemas anymore; it was about the community.&lt;/p&gt;
&lt;p class="MsoNormal"&gt;Over the years, Schemastore.org has grown into a critical resource, quietly powering tools like Visual Studio Code, GitHub Actions, IntelliJ IDEA, and many others. The stats are humbling: over 1500 contributors, 4000 pull requests, 1000 schema files, and millions of developers who&amp;rsquo;ve benefited from the work.&lt;/p&gt;
&lt;p class="MsoNormal"&gt;That results in 2 TB of schema files being served to IDEs, validators, and other system every single day to support JSON, YAML, and TOML files.&lt;/p&gt;
&lt;p class="MsoNormal"&gt;But the real story isn&amp;rsquo;t in the numbers. It&amp;rsquo;s in the ethos: a shared belief that open collaboration can solve even the most frustrating problems.&lt;/p&gt;
&lt;h2&gt;The Challenges Today&lt;/h2&gt;
&lt;p class="MsoNormal"&gt;Of course, growth comes with challenges. Keeping Schemastore.org running smoothly requires time, effort, and resources. While it&amp;rsquo;s still volunteer-driven, the infrastructure and maintenance costs have grown alongside its popularity.&lt;/p&gt;
&lt;p class="MsoNormal"&gt;And here&amp;rsquo;s the thing: for-profit organizations are among the biggest beneficiaries of Schemastore.org. They use the schemas to validate configurations, streamline workflows, and, ultimately, save time and money.&lt;/p&gt;
&lt;h2&gt;The Ask&lt;/h2&gt;
&lt;p class="MsoNormal"&gt;If you&amp;rsquo;re part of such an organization&amp;mdash;whether you&amp;rsquo;re contributing schemas or relying on them&amp;mdash;I have a request: &lt;strong&gt;consider sponsoring Schemastore.org&lt;/strong&gt;.&lt;/p&gt;
&lt;p class="MsoNormal"&gt;Your support would help cover hosting, maintenance, and development costs, ensuring this resource remains free and open for the entire community. It&amp;rsquo;s a small way to give back to something that likely saves you (and your team) time and effort every day.&lt;/p&gt;
&lt;p class="MsoNormal"&gt;You can find &lt;a href="https://github.com/sponsors/madskristensen"&gt;sponsorship details on the site&lt;/a&gt;, or feel free to reach out to me directly.&lt;/p&gt;
&lt;h2&gt;The Resolution&lt;/h2&gt;
&lt;p class="MsoNormal"&gt;Looking back, it&amp;rsquo;s amazing to see how far Schemastore.org has come. What started as a small project has grown into something much bigger &amp;ndash; a shared resource, built by and for the community, quietly making life easier for developers everywhere.&lt;/p&gt;
&lt;p class="MsoNormal"&gt;This journey isn&amp;rsquo;t over. With your help, we can keep Schemastore.org thriving, growing, and helping developers for years to come.&lt;/p&gt;
&lt;p class="MsoNormal"&gt;Thank you for being part of this story, whether as a user, a contributor, or someone who simply appreciates the effort.&lt;/p&gt;
&lt;p class="MsoNormal"&gt;Here&amp;rsquo;s to solving problems &amp;ndash; one schema at a time.&lt;/p&gt;</description>
  <author>test@example.com</author>
  <category>json</category>
  <category>yaml</category>
  <category>toml</category>
  <guid isPermaLink="false">https://madskristensen.azurewebsites.net/blog/schemastore-journey-of-a-quiet-revolution-in-development/</guid>
  <pubDate>Mon, 09 Dec 2024 21:41:37 GMT</pubDate>
</item>
<item>
  <title>Receive a notification when the dishwasher finishes</title>
  <link>https://madskristensen.azurewebsites.net/blog/receive-a-notification-when-the-dishwasher-finishes/</link>
  <description>&lt;p&gt;At our house, we use home automation for a lot of different things. One of them is the ability to get a quick overview of the state of our appliances such as the washer, dryer and dishwasher. That way we don’t accidentally forget to turn over the laundry or empty the dishwasher. &lt;/p&gt;  &lt;p&gt;A red tile on our dashboard means something is ready for us to take action on – in this case that would be emptying the dishwasher after it finishes. See &lt;a href="https://madskristensen.net/blog/the-dashboard-for-our-smart-home/"&gt;earlier blog post&lt;/a&gt; on how I built the dashboard below.&lt;/p&gt;  &lt;p&gt;&lt;img title="clip_image002" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" border="0" alt="clip_image002" src="https://madskristensen.net/posts/files/clip_image002_637201598987747999.jpg" width="845" height="528" /&gt;&lt;/p&gt;  &lt;p&gt;There are lots of &lt;a href="https://community.smartthings.com/search?q=laundry"&gt;tutorials&lt;/a&gt; on how to hook a “dumb” washer and dryer up to any home automation system, but I haven’t found any for the dishwasher that doesn’t involve figuring out how to hook up a &lt;a href="https://www.amazon.com/Z-Wave-Power-Switch-ZEN15-Humidifiers/dp/B07578W7KY"&gt;power meter&lt;/a&gt; to it. &lt;/p&gt;  &lt;p&gt;So, after a bit of trial and error I was able to do a highly accurate reporting mechanism using only a $20 multipurpose sensor that I had lying around. &lt;/p&gt;  &lt;h4&gt;The sensor&lt;/h4&gt;  &lt;p&gt;You need one or more sensors capable of sensing tilt, acceleration and temperature. I used the $20 &lt;a href="https://www.amazon.com/Samsung-SmartThings-Multipurpose-Sensor-GP-U999SJVLAAA/dp/B07F956F3B"&gt;Samsung Multipurpose sensor&lt;/a&gt; which integrates seamlessly into my SmartThings home automation system. &lt;/p&gt;  &lt;p&gt;Attach the sensor along the side of the dishwasher at a place where it doesn’t get in the way. My wife didn’t notice the sensor until I asked her to look several days later. &lt;/p&gt;  &lt;p&gt;&lt;img title="dishwasher-sensor" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" border="0" alt="dishwasher-sensor" src="https://madskristensen.net/posts/files/dishwasher-sensor_637201600770407300.png" width="600" height="729" /&gt;&lt;/p&gt;  &lt;p&gt;The key is that the sensor needs to be located such that it can measure the raise in temperature from inside the dishwasher as it runs through its heated drying cycle. For your dishwasher, it might be higher up or even underneath. &lt;/p&gt;  &lt;p&gt;Then go into the configuration of the sensor and specify it to be used on a garage door. &lt;/p&gt;  &lt;p&gt;&lt;img title="dishwasher-config" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" border="0" alt="dishwasher-config" src="https://madskristensen.net/posts/files/dishwasher-config_637201600775514433.png" width="600" height="812" /&gt;&lt;/p&gt;  &lt;p&gt;All that does is to use the tilt sensor to determine if it’s open or closed instead of the magnet. That way, you must open the dishwasher door all the way down to horizontal level before it registers as open. That’s what we want.&lt;/p&gt;  &lt;h3&gt;The automation logic&lt;/h3&gt;  &lt;p&gt;I’ve created a virtual switch called &lt;i&gt;Dishwasher&lt;/i&gt; that I use to store the state of the dishwasher cycles which is what I use for the dashboard. It’s turned on when the dishwasher finishes and turned back off once it’s emptied. &lt;/p&gt;  &lt;p&gt;It uses the fact that a bit of vibration occurs early on in the cycle, and temperature rapidly dropping after it finishes. The name of the multipurpose sensor is &lt;i&gt;Dishwasher Sensor&lt;/i&gt;. Here’s the logic:&amp;#160;&amp;#160;&amp;#160; &lt;/p&gt;  &lt;p&gt;&lt;img title="dishwasher-piston" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" border="0" alt="dishwasher-piston" src="https://madskristensen.net/posts/files/dishwasher-piston_637201600780904464.png" width="600" height="783" /&gt;&lt;/p&gt;  &lt;p&gt;Notice how it uses the variable &lt;i&gt;isRunning&lt;/i&gt; to keep track on when the dishwasher is running.&lt;/p&gt;  &lt;p&gt;That’s it. Pretty simple, but it may require a bit of trial and error on where to place the sensor on the dishwasher door.&lt;/p&gt;</description>
  <author>test@example.com</author>
  <category>home automation</category>
  <guid isPermaLink="false">https://madskristensen.azurewebsites.net/blog/receive-a-notification-when-the-dishwasher-finishes/</guid>
  <pubDate>Wed, 18 Mar 2020 20:25:05 GMT</pubDate>
</item>
<item>
  <title>Hiding electronics behind fake books</title>
  <link>https://madskristensen.azurewebsites.net/blog/hiding-electronics-behind-fake-books/</link>
  <description>&lt;p&gt;After showing some pictures of our &lt;a href="https://madskristensen.net/blog/the-dashboard-for-our-smart-home/"&gt;smart home dashboard&lt;/a&gt;, questions started coming in about the cables going in behind the books. Where is the modem and what else is going on? Spoiler alert, the books are fake. &lt;/p&gt;  &lt;p&gt;Here you see a couple of cables going in behind the books on the top shelve.&lt;/p&gt;  &lt;p&gt;&lt;img title="fake books 1" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" border="0" alt="fake books 1" src="https://madskristensen.net/posts/files/fake%20books%201_637161973355565458.jpg" width="1024" height="850" /&gt;&lt;/p&gt;  &lt;p&gt;Removing the books reveal a tangled mess of cords, power bricks, a modem, and a home automation hub.&lt;/p&gt;  &lt;p&gt;&lt;img title="fake books 2" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" border="0" alt="fake books 2" src="https://madskristensen.net/posts/files/fake%20books%202_637161973364015977.jpg" width="1010" height="844" /&gt;&lt;/p&gt;  &lt;p&gt;The fake books are hollow and can hold quite the amount of stuff, as you can see below. Looking at the books from above shows just how simple they’re built.&lt;/p&gt;  &lt;p&gt;&lt;img title="fake books 3" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" border="0" alt="fake books 3" src="https://madskristensen.net/posts/files/fake%20books%203_637161973370084489.jpg" width="1024" height="545" /&gt;&lt;/p&gt;  &lt;p&gt;This is easy to make yourself if you can find some decorative books you like. You can also buy it (&lt;a href="https://www.amazon.com/Covobox-electronics-outlets-documents-original/dp/B074PDZLQX/"&gt;Covobox on Amazon&lt;/a&gt;) and customize the size (width) and the colors of the books. For reference, we got the 14 inch one and that is probably the smallest I would recommend. &lt;/p&gt;  &lt;p&gt;We’ve had this for a couple of years, and it holds up great.&lt;/p&gt;</description>
  <author>test@example.com</author>
  <category>home automation</category>
  <guid isPermaLink="false">https://madskristensen.azurewebsites.net/blog/hiding-electronics-behind-fake-books/</guid>
  <pubDate>Sat, 01 Feb 2020 23:42:17 GMT</pubDate>
</item>
<item>
  <title>The dashboard for our smart home</title>
  <link>https://madskristensen.azurewebsites.net/blog/the-dashboard-for-our-smart-home/</link>
  <description>&lt;p&gt;I’ve had some questions lately about how we keep track of all the home automation devices spread across the house. So, I thought I’d share how our dashboard is set up. The dashboard is the web-based &lt;a href="https://sharptools.io/"&gt;SharpTools&lt;/a&gt; that runs in &lt;a href="https://www.ozerov.de/fully-kiosk-browser/"&gt;Fully Kiosk Browser&lt;/a&gt; on an &lt;a href="https://www.amazon.com/dp/B07KD6BTCZ"&gt;Amazon Fire Tablet HD 10&lt;/a&gt; which is magnetically attached to the wall. &lt;/p&gt;  &lt;p&gt;&lt;a href="https://madskristensen.net/posts/files/20200130_165855165_iOS_637160068674899332.jpg"&gt;&lt;img title="20200130_165855165_iOS" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" border="0" alt="20200130_165855165_iOS" src="https://madskristensen.net/posts/files/20200130_165855165_iOS_thumb_637160068681994799.jpg" width="1280" height="956" /&gt;&lt;/a&gt;&lt;/p&gt;  &lt;p&gt;Since it hangs so close to shelving, the charging cable is nicely hidden, so we didn't need to install additional electrical outlets behind the tablet.&lt;/p&gt;  &lt;p&gt;&lt;a href="https://madskristensen.net/posts/files/20200130_165901366_iOS_637160068686605643.jpg"&gt;&lt;img title="20200130_165901366_iOS" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" border="0" alt="20200130_165901366_iOS" src="https://madskristensen.net/posts/files/20200130_165901366_iOS_thumb_637160068691533235.jpg" width="1280" height="956" /&gt;&lt;/a&gt;&lt;/p&gt;  &lt;p&gt;Though the tiles looked blue above due to lighting and old phone camera, all tiles are actually gray, and red is used for the ones that need our attention when certain events occur - like when the washer is done and we need to move the clothes to the dryer. This helps keep the visual noise to a minimum and the red is really easy to see from across the room.&lt;/p&gt;    &lt;p&gt;The bottom row is half way below the fold and we utilize this for both navigation to other dashboard (the left 3 bottom tiles) and some infrequent commands. The calendar is well suited for being cut off since it easily scrolls to reveal future calendar entries without scrolling the whole screen.&lt;/p&gt;  &lt;p&gt;&lt;a href="https://madskristensen.net/posts/files/20200130_040848334_iOS_637160068695832579.jpg"&gt;&lt;img title="20200130_040848334_iOS" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" border="0" alt="20200130_040848334_iOS" src="https://madskristensen.net/posts/files/20200130_040848334_iOS_thumb_637160068700727722.jpg" width="1280" height="794" /&gt;&lt;/a&gt;&lt;/p&gt;  &lt;p&gt;The first sub-dashboard is for the lights around the house. We use orange as a neutral color instead of green/red or similar. That's because it is neither good nor bad if a light is on or off. So orange in this case just means that something is active or on.&lt;/p&gt;  &lt;p&gt;&lt;a href="https://madskristensen.net/posts/files/20200130_040906613_iOS_637160068706563715.jpg"&gt;&lt;img title="20200130_040906613_iOS" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" border="0" alt="20200130_040906613_iOS" src="https://madskristensen.net/posts/files/20200130_040906613_iOS_thumb_637160068712057096.jpg" width="1280" height="794" /&gt;&lt;/a&gt;&lt;/p&gt;  &lt;p&gt;The second sub-dashboard tell us the status of more security related information. It tells us what external doors and windows are open, if the door is locked, the cameras armed and who's home.&lt;/p&gt;  &lt;p&gt;&lt;a href="https://madskristensen.net/posts/files/20200130_145903231_iOS_637160068716826729.jpg"&gt;&lt;img title="20200130_145903231_iOS" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" border="0" alt="20200130_145903231_iOS" src="https://madskristensen.net/posts/files/20200130_145903231_iOS_thumb_637160068721040256.jpg" width="1280" height="794" /&gt;&lt;/a&gt;&lt;/p&gt;  &lt;p&gt;It also has a link to a sub-sub-dashboard for showing battery status of our various devices. This uses the Battery Tile and will change color as the battery levels get below 30% and again below 10%. None of my devices are low on battery - thanks to this dashboard.&amp;#160;&amp;#160;&amp;#160;&amp;#160; &lt;/p&gt;  &lt;p&gt;&lt;a href="https://madskristensen.net/posts/files/20200130_150015824_iOS_637160068725228694.jpg"&gt;&lt;img title="20200130_150015824_iOS" style="display: inline; background-image: none;" border="0" alt="20200130_150015824_iOS" src="https://madskristensen.net/posts/files/20200130_150015824_iOS_thumb_637160068729956912.jpg" width="1284" height="804" /&gt;&lt;/a&gt;&lt;/p&gt;  &lt;p&gt;The third sub-dashboard is temperature and weather information from around the house.&lt;/p&gt;  &lt;p&gt;&lt;a href="https://madskristensen.net/posts/files/20200130_041009032_iOS_637160068747908242.jpg"&gt;&lt;img title="20200130_041009032_iOS" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" border="0" alt="20200130_041009032_iOS" src="https://madskristensen.net/posts/files/20200130_041009032_iOS_thumb_637160068756270625.jpg" width="1280" height="794" /&gt;&lt;/a&gt;&lt;/p&gt;  &lt;p&gt;That’s it. I tried to keep it simple, but still containing all the information I need. What do you think?&lt;/p&gt;</description>
  <author>test@example.com</author>
  <category>home automation</category>
  <guid isPermaLink="false">https://madskristensen.azurewebsites.net/blog/the-dashboard-for-our-smart-home/</guid>
  <pubDate>Thu, 30 Jan 2020 18:18:23 GMT</pubDate>
</item></channel>
</rss>