Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Scoped deliberately to the bundled fonts rather than declaring rules for the whole repo:
# a repo-wide text/eol policy would rewrite line endings across every existing file, which is
# its own change with its own review. This only guarantees the one thing that would silently
# corrupt an asset — a font is binary, and any newline "normalisation" applied to it produces a
# file that still has the right name and no longer renders.
*.ttf binary
*.otf binary
*.woff binary
*.woff2 binary
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,23 @@ for every approved plan. Desktop's version follows the engine generation, so it
0.15.0.

### Added
- **Nine themes that imitate a physical medium, not just a colour scheme.** Alongside the existing
e-ink and CRT looks, MandoCode Desktop now ships a monochrome amber terminal, a vacuum-fluorescent
panel, a vector scope, a passive-matrix LCD, a Solari split-flap board, a cyanotype blueprint, a
two-ink risograph print, continuous-feed printer paper, and microfiche. Each reproduces how its
medium actually made an image rather than only borrowing its colours — the LCD ghosts instead of
glowing, the split-flap board can only show uppercase because a flap carries one printed
character, and the cyanotype is a negative, so emphasis is a whiter stroke rather than a heavier
one. The theme list is ordered so neighbours share a mechanism, which keeps it skimmable now that
there are twenty-five entries.
- **Your chat background can match the theme.** Themes that imitate a screen or a printing process
also process your background image the way that medium would have reproduced it — quantised to a
panel's few shades, rendered as amber phosphor, or laid down as two-ink halftone. On by default,
with a switch in Settings → Appearance to keep the picture in full colour. It affects only those
themes, and only when a background is set.
- **The MandoCode wordmark has its own typeface.** The name in the top-left is now set in Permanent
Marker, bundled with the app rather than fetched from a font service, so it renders identically
offline. It takes each theme's accent colour, so it re-inks itself as you switch themes.
- **Watch the agent's shell commands run.** The terminal panel gains a read-only tab per agent
showing every command that agent runs — the command and the folder it runs in, its output line by
line as it arrives, and whether it finished, failed, or was killed for taking too long. A long
Expand Down Expand Up @@ -192,6 +209,18 @@ for every approved plan. Desktop's version follows the engine generation, so it
a sticky header, or the suggestion list a field opens when it is filled.

### Fixed
- **Secondary text is readable in every theme.** Timestamps, file paths, status lines and the hints
under headings were below the accessibility contrast floor in half the shipped themes — several
faithfully so, since palettes like Dracula and One Dark ship famously faint comment colours
upstream. Those palettes are unchanged; the text is now lifted just far enough to clear the floor,
blended toward each theme's own text colour so it stays that theme's shade rather than drifting
grey. A theme that was already readable is left exactly as it was. Themes viewed through an
overlay, such as the CRT tube, are held to a higher bar, because scanlines take contrast the
palette's own numbers do not account for.
- **The CRT theme's phosphor glow follows the text it surrounds.** Every glyph glowed the same
blue regardless of its colour, so red errors carried a blue halo. On a real tube the glow is the
phosphor being excited, so error text now glows red and success green. Code blocks keep a tighter
glow, since a full halo turns syntax highlighting into a smear.
- **The "no agents open" message stays readable over a chat background image.** With a background
image set, closing the last agent left the message painted straight onto the picture, where a
busy or light image could make it hard to read. The message now sits in a translucent card — the
Expand Down
34 changes: 34 additions & 0 deletions THIRD-PARTY-NOTICES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ highlight.js — https://highlightjs.org
Copyright (c) 2006, Ivan Sagalaev
License: BSD 3-Clause (Appendix C)

--------------------------------------------------------------------------
Bundled fonts (shipped in Assets/fonts, used for the application wordmark)
--------------------------------------------------------------------------

Permanent Marker — https://fonts.google.com/specimen/Permanent+Marker
Copyright (c) 2010 Font Diner, Inc.
License: Apache License 2.0 (Appendix D)
The unmodified font file ships with its full license text alongside it, at
Assets/fonts/LICENSE.txt. Upstream publishes no NOTICE file for this family,
so section 4(d) of the license imposes no further attribution.

--------------------------------------------------------------------------
.NET package dependencies (compiled into the application)
--------------------------------------------------------------------------
Expand Down Expand Up @@ -152,3 +163,26 @@ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.

--------------------------------------------------------------------------
Appendix D — Apache License 2.0 (Permanent Marker)
--------------------------------------------------------------------------

Licensed under the Apache License, Version 2.0 (the "License"); you may not
use this file except in compliance with the License. You may obtain a copy of
the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
License for the specific language governing permissions and limitations under
the License.

The complete, unabridged license text is distributed with the font itself, at
Assets/fonts/LICENSE.txt, and is installed alongside the application.

Apache-2.0 is a permissive, non-copyleft license. Including this font does not
place any condition on the terms of MandoCode Desktop's own source, which
remains under the MIT License (see LICENSE).
2 changes: 2 additions & 0 deletions src/MandoCode.Desktop.Tests/MandoCode.Desktop.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
<Compile Include="..\MandoCode.Desktop\Services\StableCollection.cs" Link="src\StableCollection.cs" />
<Compile Include="..\MandoCode.Desktop\Services\AgentCommandFormat.cs" Link="src\AgentCommandFormat.cs" />
<Compile Include="..\MandoCode.Desktop\Services\AgentCommandLog.cs" Link="src\AgentCommandLog.cs" />
<Compile Include="..\MandoCode.Desktop\Services\ColorMath.cs" Link="src\ColorMath.cs" />
<Compile Include="..\MandoCode.Desktop\Services\UiTheme.cs" Link="src\UiTheme.cs" />
<Compile Include="..\MandoCode.Desktop\Services\ConversationLog.cs" Link="src\ConversationLog.cs" />
<!-- History's full-text matching + snippet extraction. Pure (text in, match out); the file reads
and caching live in ConversationTextCache, which is NOT compiled here because it touches
Expand Down
99 changes: 99 additions & 0 deletions src/MandoCode.Desktop.Tests/ThemeReadabilityTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using MandoCode.Desktop.Services;
using Xunit;

namespace MandoCode.Desktop.Tests;

/// <summary>
/// Secondary text — timestamps, paths, status lines, the hint under a heading — is still text, and
/// half the shipped palettes authored it below the WCAG AA floor. The palettes stay as authored
/// (most are faithful to upstream schemes); UiTheme.ReadableDim is what makes them legible. The
/// point of the sweep below is the NEXT theme: one added with a too-faint Dim fails here instead of
/// shipping.
/// </summary>
public class ThemeReadabilityTests
{
public static TheoryData<string> ThemeNames()
{
var data = new TheoryData<string>();
foreach (var t in UiTheme.All) data.Add(t.Name);
return data;
}

private static UiTheme Theme(string name) => UiTheme.All.Single(t => t.Name == name);

[Theory]
[MemberData(nameof(ThemeNames))]
public void EveryThemesSecondaryTextClearsTheFloor(string name)
{
var t = Theme(name);
var ratio = ColorMath.Contrast(t.ReadableDim, t.Background);
// A CRT theme is read through its own overlay, so it is held to the higher floor.
var floor = t.Crt ? ColorMath.AaNormalTextThroughGlass : ColorMath.AaNormalText;

Assert.True(ratio >= floor,
$"{name}: dim text is {ratio:F2}:1 against its background (floor {floor}:1)");
}

[Fact]
public void OverlaidThemesAreHeldToAHigherFloorThanPlainOnes()
{
// The point of the CRT floor is that it BINDS — if the tube themes happened to clear the
// ordinary floor anyway, this whole mechanism would be decoration.
var overlaid = UiTheme.All.Where(t => t.Crt).ToList();
Assert.NotEmpty(overlaid);

foreach (var t in overlaid)
{
Assert.True(ColorMath.Contrast(t.ReadableDim, t.Background) >= ColorMath.AaNormalTextThroughGlass,
$"{t.Name}: dim text does not clear the through-glass floor");
}
Assert.True(ColorMath.AaNormalTextThroughGlass > ColorMath.AaNormalText);
}

[Theory]
[MemberData(nameof(ThemeNames))]
public void EveryThemesBodyTextClearsTheFloor(string name)
{
// ReadableDim blends toward Text and stops there, so body text being readable is the
// assumption that makes the lift terminate. Asserted rather than assumed.
var t = Theme(name);
var ratio = ColorMath.Contrast(t.Text, t.Background);

Assert.True(ratio >= ColorMath.AaNormalText,
$"{name}: body text is {ratio:F2}:1 against its background");
}

[Theory]
[MemberData(nameof(ThemeNames))]
public void AlreadyReadablePalettesAreLeftAlone(string name)
{
// The lift is a correction, not a restyle: a theme that authored a legible Dim must come
// through byte-identical, or switching themes would quietly repaint palettes that were fine.
var t = Theme(name);
var floor = t.Crt ? ColorMath.AaNormalTextThroughGlass : ColorMath.AaNormalText;
if (ColorMath.Contrast(t.Dim, t.Background) < floor) return;

Assert.Equal(t.Dim, t.ReadableDim);
}

[Fact]
public void TheLiftIsTheSmallestOneThatWorks()
{
// Walking to the first passing step keeps a nudged palette recognisably itself. A lift that
// overshot would wash every faint theme toward the same grey.
const string bg = "#282A36", text = "#F8F8F2", faint = "#6272A4"; // Dracula
var lifted = ColorMath.Readable(faint, bg, text, ColorMath.AaNormalText);

Assert.True(ColorMath.Contrast(lifted, bg) >= ColorMath.AaNormalText);
Assert.True(ColorMath.Contrast(lifted, bg) < ColorMath.AaNormalText + 1.0,
"overshot: the lift should stop at the first step that clears the floor");
Assert.NotEqual(text, lifted); // nudged toward the text colour, not replaced by it
}

[Fact]
public void ContrastMatchesKnownWcagValues()
{
Assert.Equal(21.0, ColorMath.Contrast("#000000", "#FFFFFF"), 2);
Assert.Equal(1.0, ColorMath.Contrast("#808080", "#808080"), 2);
}
}
Loading
Loading