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
10 changes: 7 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,9 @@ happens. A probe sent to explain a black screen must not be able to cause one.

### The start-page harness

Any change to `Store` or `HomePage` — what gets saved as a visit or favourite, and
the page built from it — is exercised off-device first:
Any change to `Store`, `HomePage` or `StartPage` — what gets saved as a visit or
favourite, the page built from it, and which of the three things the browser opens
at launch — is exercised off-device first:

```sh
tools/startpage/run.sh
Expand All @@ -123,7 +124,10 @@ a row, recording each the way the NUI engine reports it (a `data:` URL carrying
page). Issue #53 was that loop with no guard: the start screen recorded itself as a
visit on every launch, roughly doubled each time, and after nine launches was past
the 2 MB Chromium will load — a black screen with no line anywhere, on a view that
was fine. Needs only the .NET 6 SDK under `~/.dotnet-local`.
was fine. It also holds `StartPage` (issue #79) to the three states, to "where I
left off" landing on the last real visit and never on the start screen, and to an
install made before the mode existed still opening where it always did. Needs only
the .NET 6 SDK under `~/.dotnet-local`.

### The site-rules harness

Expand Down
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,43 @@ browser simply won't start. Newer sets don't have this restriction.
Not every remote sends the colour buttons, and the slim ones have none. **Switch
site** is the second row of the menu, so hold OK and it is one press away.

Every digit is spoken for, so these live in the menu only (**hold OK**):

| Menu row | What it does |
| --- | --- |
| **Keep an address…** | Type a URL and keep *that* as a tile, without going to it |
| **Open where I left off** | Launch straight back into the last page you were on |
| **Forget this site's settings** | Take a site back to your everywhere settings |
| **Pointer style** | Who draws the pointer — Overscan (keeps up) or the page (an arrow) |
| **Ad blocking on/off** | 2025+ package only |

### Keeping a page you can't land on

**Keep an address…** in the menu is for URLs that redirect. `https://www.instagram.com/reel`
sends you to one particular reel, so pressing `8` there would save that clip
for ever; type the address instead and the tile is the address. It's prefilled
with wherever you are, so it's usually a matter of deleting the end of it.

### Where it opens

By default, the start screen. Two ways to change that:

- **Open where I left off** in the menu — it launches back into the last page you
were on.
- On the keyboard, **start** makes whatever you typed the fixed opening page. Press
it with nothing typed to go back to the start screen.

Turning "where I left off" off again returns to your fixed address if you set one,
so you never have to type it twice.

### The pointer

On a heavy site the pointer used to slow down, because it was drawn *by the page*
and could only move as fast as the page would let it. Overscan now draws it itself
on the 2025+ package, so it keeps up whatever the site is doing. **Pointer style**
in the menu (key **2** on the older packages) switches back to the page-drawn
arrow, which looks nicer and is only as quick as the page.

### Each site keeps its own settings

Images and identity are remembered **for the site you set them on**. Turn images
Expand Down
124 changes: 116 additions & 8 deletions docs/INTERNALS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,55 @@ Details that matter:
`input`/`change`, otherwise React-style frameworks ignore it. Enter is sent as
real key events, falling back to `form.requestSubmit()`.

### A pointer inside the page moves at the page's speed

The same reporter again, in #78: "if we load a heavy site cursor start to become
less responsive, when we load light site it is faster, default browser cursor
perform same on all sites." It was ours, not the engine's.

Everything above describes a pointer that *lives in the page*, and the bill for
that had never been read. Each D-pad step was one script evaluation, and the
script it ran did an `elementFromPoint` and dispatched a `mousemove` — a forced
layout plus whatever the site's own move handlers do, which on Instagram or
Spotify is a great deal. So the pointer could only move as fast as the page's main
thread was willing to run our script, and that thread is the busiest object on the
television. The TV's own browser draws its pointer in the compositor, which is why
his comparison holds and why it always would have.

Two halves, and neither is sufficient alone:

- **The app draws the pointer.** DALi views over the web view on NUI, Evas
rectangles on ElmSharp, positioned from the same viewport fraction the script
was being sent. The ElmSharp build has had this behind key `2` since early on
and it was nobody's default; NUI never had it at all, and its old header said an
overlay "would need to track scroll and zoom itself". That was simply wrong —
the position is a fraction of the *viewport*, and the viewport is the view's own
rectangle, which is the entire reason it is kept as a fraction rather than as a
document point.
- **The page hears about it once a tick, not once a key press.** Both builds
already run a 150 ms timer, so a move marks itself pending and the tick flushes
it. A held key that used to be twenty hover updates is now six, and none of them
is in front of the drawing.

The rule the second half must not break is that **the page and the pointer agree at
the moment of a click**. `PageScript.click()` hit-tests wherever `move()` last left
it, so a click that overtook its own pending move would land where the pointer used
to be. Both builds therefore put the pending move *into the click's own script*,
one evaluation, rather than sending it as a call before — two evaluations are two
chances for that ordering to be wrong, and the failure would be a click that
occasionally hits the wrong thing, which is the worst kind to be sent from a sofa.

What this costs is up to 150 ms of hover lag behind the drawn pointer, and the
arrow: neither toolkit draws a triangle, so the app-drawn pointer is the ringed dot
the ElmSharp build has always used for its native style, while the page's arrow is
CSS borders. A dot that keeps up beats an arrow that does not, so **NUI defaults to
drawing it itself** and a menu row switches back. **The ElmSharp default is
unchanged**: #78 is a report from a 2025 set, the ewk packages are the ones where a
change cannot be tested before somebody installs it, and `src/nui` not being able
to break them is the whole reason the source is split this way. They get the
coalescing, which is the half that is safe everywhere, and key `2` was already
there for the rest.

### The one thing a script cannot click: another origin's frame

`elementFromPoint` stops at an `<iframe>`, and a click dispatched on the frame
Expand Down Expand Up @@ -1503,16 +1552,60 @@ issue #15.

### Where the browser opens

`startupUrl` in `settings.tsv`, set by the keyboard's `start` key: type an address,
press `start` instead of `GO`, and that is what the app loads at launch. Pressing
`start` with an empty entry clears it and the generated start screen comes back.
Three states, and `StartPage` owns which: the generated start screen, one fixed
address, or wherever the last session got to.

It lives on the keyboard rather than on a remote key because every digit was
already taken, and because the thing being saved is exactly what you have just
typed. `Store.Init` therefore has to run **before** the keyboard is constructed —
The fixed address came from issue #15 and is set by the keyboard's `start` key:
type an address, press `start` instead of `GO`, and that is what the app loads at
launch; press `start` with an empty entry and the start screen comes back. It
lives on the keyboard rather than on a remote key because every digit was already
taken, and because the thing being saved is exactly what you have just typed.
`Store.Init` therefore has to run **before** the keyboard is constructed —
`KeyboardLayouts` resolves its remembered layout the first time it is touched, and
both builds used to initialise the store afterwards, silently discarding the user's
layout choice.
both builds used to initialise the store afterwards, silently discarding the
user's layout choice.

**"Open where I left off" is issue #79**, and it is one menu row because there was
no key left. It resolves to the first entry of the history, which is the right
answer only because of what `Store.RecordVisit` already refuses: this app's own
generated pages in both shapes (#53) and the sign-in steps a flow passes through
(#53's follow-up). Without those two guards "where I left off" would be the start
screen you closed the app from, or the captcha you went through an hour earlier.
An empty history resolves to the start screen, which is a fresh install saying so
rather than a blank page.

Three things about the shape of it:

- **The mode is stored beside the address, not inside it.** A sentinel URL meaning
"not a URL" reads fine on the day it is written and becomes a site nobody can
visit the first time it turns up in somebody's history.
- **An install from before #79 has no mode key**, so the mode is derived from
whether an address was ever set — which is exactly what those builds did. Nobody's
start page moves under them on upgrade, and the harness holds that.
- **Turning "where I left off" off goes back to the address**, if one was ever set,
rather than to the start screen. The address is still in the file and making
somebody retype it on a remote to get it back would be its own small cruelty.

### Keeping an address you cannot land on

Issue #80: "i like to set `https://www.instagram.com/reel` as favourite but when i
go to the url it opens a reel so it saves url of reel instead when open it plays
same reel always."

Key `8` keeps *the page you are on*, which is the whole vocabulary the favourites
had, and it has no answer for an address that redirects. The fix is a third
`KeyboardTarget` — `Favourite` — so the same on-screen keyboard can finish into
`Store.ToggleFavourite` instead of into `Navigate`. It is prefilled with the
current address, because what somebody wants to keep is usually what they are
looking at with a few segments taken off the end, and the entry line says
`Keep as a tile` rather than `Go to`, since the keyboard is otherwise identical
whichever of the three it was opened for and that is not a mistake anybody should
make silently.

Two details worth keeping: it **toggles**, exactly as `8` does, and says which of
the two it just did — from a sofa, "kept" and "removed" are the same screen
otherwise. And the tile's name is the site's own name (`SiteRules.KeyFor`), because
there is no page title to take from a page nobody opened.

## Settings that belong to a site, not to the browser

Expand Down Expand Up @@ -2444,6 +2537,21 @@ the one its report has to come from. The state is:
answers #75 either way, so a button that never arrives is not a reason to hold
the issue open.

- **The pointer, the start page and keeping an address — #78, #79 and #80, built
together.** All three arrived the day after `build-d526114`, and all three are
what a browser gets asked for once it has stopped breaking. #78 is the one with
a fault behind it: the pointer lived in the page, so it moved at the page's
speed, and NUI now draws it itself and tells the page where it is once a tick
instead of once a key press — see *A pointer inside the page moves at the
page's speed* above. #79 adds "open where I left off" beside the fixed address
from #15, and #80 lets an address be kept as a tile without going to it, which
is the only way to keep one that redirects. **Waiting on:** his report from the
build that ships them. The one that decides something is #78 — whether the
pointer keeps up on Instagram and Spotify now. If it does not, the `pointer`
line on the report says which of the two is drawing it, and the answer that
would matter is "drawn by Overscan and still slow", because that is the one
saying the lag was never our script.

Five things about that set are settled and should not be re-derived: **key `5` is
his, not ours** — the engine's overlay path is the only one that gives him a
picture, so a report of black or silent video is the in-page path failing and not a
Expand Down
23 changes: 23 additions & 0 deletions src/common/CursorVisual.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Overscan
{
/// <summary>
/// Who draws the D-pad pointer. Shared because both builds now offer the
/// choice and it means the same thing in each — see <see cref="NuiCursor"/>
/// for what issue #78 made of it.
/// </summary>
internal enum CursorVisual
{
/// <summary>
/// Drawn by the injected page script, as an arrow. Better looking, and only
/// as quick as the page's main thread is willing to be.
/// </summary>
Dom,

/// <summary>
/// Drawn by the app, over the web view. A ringed dot rather than an arrow,
/// because neither toolkit will draw a triangle, and it owes the page
/// nothing at all.
/// </summary>
Native,
}
}
9 changes: 9 additions & 0 deletions src/common/KeyboardTarget.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,14 @@ internal enum KeyboardTarget

/// <summary>Type the text into whatever field the page has focused.</summary>
PageField,

/// <summary>
/// Keep the typed address as a favourite without going to it (issue #80).
/// The reporter wanted `instagram.com/reel` in his tiles, and that address
/// redirects to one particular reel the moment it is opened — so the only
/// favourite he could make was of the reel, which then played the same clip
/// for ever. An address you cannot land on has no other way in.
/// </summary>
Favourite,
}
}
2 changes: 2 additions & 0 deletions src/common/RemoteMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ public Item(string id, string label, string shortcut)
public const string ActionAddress = "address";
public const string ActionHome = "home";
public const string ActionBookmark = "bookmark";
public const string ActionKeepAddress = "keepaddress";
public const string ActionResumeLast = "resumelast";
public const string ActionIdentity = "identity";
public const string ActionSwitchSite = "switchsite";
public const string ActionForgetSite = "forgetsite";
Expand Down
135 changes: 135 additions & 0 deletions src/common/StartPage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
using System;
using System.Collections.Generic;

namespace Overscan
{
/// <summary>
/// What the browser opens when it launches.
///
/// Issue #15 gave this one answer — a fixed address, set from the keyboard's
/// <c>start</c> key — because opening on the generated start screen means
/// typing the same address on a remote control every evening. Issue #79 asked
/// for the other one: "set start page as the last page we left off". Both are
/// the same setting with three states, so they live here rather than as two
/// flags in two apps that could disagree about which of them wins.
///
/// The mode is stored beside the address rather than encoded into it. A
/// sentinel URL meaning "not a URL" is the kind of thing that reads fine on
/// the day it is written and turns into a site nobody can visit the first time
/// somebody's history contains it.
/// </summary>
internal static class StartPage
{
/// <summary>The generated start screen — favourites and recent tiles.</summary>
public const string Home = "home";

/// <summary>Wherever the last session got to.</summary>
public const string Last = "last";

/// <summary>One fixed address, set from the keyboard's <c>start</c> key.</summary>
public const string Url = "url";

private const string ModeKey = "startupMode";
private const string UrlKey = "startupUrl";

/// <summary>
/// The mode in force. An install from before #79 has no mode key at all, so
/// it is derived from whether an address was ever set — which is exactly
/// what those builds did, and means nobody's start page changes under them
/// on upgrade.
/// </summary>
public static string Mode()
{
string stored = Store.Get(ModeKey, null);
if (stored == Home || stored == Last || stored == Url)
{
return stored;
}

return string.IsNullOrEmpty(Address) ? Home : Url;
}

/// <summary>The fixed address, or empty when none was set.</summary>
public static string Address
{
get { return Store.Get(UrlKey, string.Empty) ?? string.Empty; }
}

/// <summary>
/// Remembers a fixed address, and switches to it. An empty address goes
/// back to the start screen, which is what pressing <c>start</c> with
/// nothing typed has always done.
/// </summary>
public static void SetAddress(string url)
{
if (string.IsNullOrEmpty(url))
{
Store.Set(UrlKey, string.Empty);
Store.Set(ModeKey, Home);
return;
}

Store.Set(UrlKey, url);
Store.Set(ModeKey, Url);
}

/// <summary>
/// Turns "open where I left off" on and off. Off goes back to the fixed
/// address if one was ever set and to the start screen otherwise — so the
/// address somebody chose is still there when they change their mind, and
/// nobody has to type it again to get it back.
/// </summary>
public static bool ToggleLast()
{
bool on = Mode() != Last;
Store.Set(ModeKey, on ? Last : (string.IsNullOrEmpty(Address) ? Home : Url));
return on;
}

/// <summary>
/// The address to open at launch, or null for the start screen.
///
/// The history is most-recent-first and already refuses this app's own
/// generated pages and the sign-in steps a flow passes through
/// (<see cref="Store.RecordVisit"/>), so its first entry is the last page
/// the user was actually on — not the start screen they closed the app
/// from, and not the captcha they went through an hour earlier.
/// </summary>
public static string Resolve(IList<Bookmark> history)
{
switch (Mode())
{
case Url:
string url = Address;
return string.IsNullOrEmpty(url) ? null : url;

case Last:
// No history is a fresh install, or one whose history was
// cleared. The start screen is the honest answer, and it is
// the screen that lists what there is.
return history != null && history.Count > 0 ? history[0].Url : null;

default:
return null;
}
}

/// <summary>What the diagnostics report says about all this.</summary>
public static string Describe(IList<Bookmark> history)
{
switch (Mode())
{
case Url:
return "fixed address — " + Address;

case Last:
string last = Resolve(history);
return "where you left off — " +
(last ?? "(nothing visited yet, so the start screen)");

default:
return "the start screen";
}
}
}
}
Loading
Loading