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
19 changes: 11 additions & 8 deletions assets/brand/tokens.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
{
"brand_neon": "#67D5FF",
"favicon_background": "#0C1017",
"text_on_brand": "#062A38",
"brand_text_on_light": "#006B87",
"dark_background": "#0C1017",
"dark_surface": "#101721",
"dark_text": "#DCE7F5",
"dark_muted_text": "#80A4C3"
"brand_neon": "#12F7D6",
"brand_secondary": "#00D1FF",
"electric_blue": "#007BFF",
"favicon_background": "#0B1320",
"text_on_brand": "#0B1320",
"brand_text_on_light": "#0058B8",
"dark_background": "#0B1320",
"dark_surface": "#0B1320",
"dark_text": "#F8FAFF",
"dark_muted_text": "#00D1FF",
"light_foreground": "#F8FAFF"
}
2 changes: 1 addition & 1 deletion docs/assets/brand/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/assets/brand/r10n.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 20 additions & 14 deletions docs/stylesheets/extra.css
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
:root {
--r10n-neon: #67d5ff;
--r10n-neon-on-light: #006b87;
--r10n-dark-ink: #062a38;
--r10n-dark-background: #0c1017;
--r10n-dark-surface: #101721;
--r10n-dark-text: #dce7f5;
--r10n-dark-muted: #80a4c3;
--r10n-primary: #12f7d6;
--r10n-secondary: #00d1ff;
--r10n-electric-blue: #007bff;
--r10n-light-link: #0058b8;
--r10n-dark-ink: #0b1320;
--r10n-dark-background: #0b1320;
--r10n-light-background: #f8faff;
--r10n-dark-surface: #0b1320;
--r10n-dark-text: #f8faff;
--r10n-dark-muted: #00d1ff;
}

[data-md-color-scheme="default"] {
--md-primary-fg-color: var(--r10n-neon-on-light);
--md-accent-fg-color: var(--r10n-neon-on-light);
--md-typeset-a-color: var(--r10n-neon-on-light);
--r10n-focus: var(--r10n-neon-on-light);
--md-default-bg-color: var(--r10n-light-background);
--md-default-fg-color: var(--r10n-dark-ink);
--md-primary-fg-color: var(--r10n-dark-ink);
--md-primary-bg-color: var(--r10n-light-background);
--md-accent-fg-color: var(--r10n-electric-blue);
--md-typeset-a-color: var(--r10n-light-link);
--r10n-focus: var(--r10n-electric-blue);
}

[data-md-color-scheme="slate"] {
--md-default-bg-color: var(--r10n-dark-background);
--md-default-fg-color: var(--r10n-dark-text);
--md-primary-fg-color: var(--r10n-dark-background);
--md-primary-bg-color: var(--r10n-dark-text);
--md-accent-fg-color: var(--r10n-neon);
--md-typeset-a-color: var(--r10n-neon);
--r10n-focus: var(--r10n-neon);
--md-accent-fg-color: var(--r10n-primary);
--md-typeset-a-color: var(--r10n-secondary);
--r10n-focus: var(--r10n-primary);
}

.md-header__button.md-logo {
Expand Down
2 changes: 1 addition & 1 deletion install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ download_binary_asset() {
destination="$2"
if [ "${brand_displayed:-0}" -eq 0 ]; then
if [ -t 2 ] && [ -z "${NO_COLOR:-}" ] && [ "${R10N_ACCESSIBLE:-}" != "1" ]; then
printf '\033[96m' >&2
printf '\033[38;2;18;247;214m' >&2
fi
if [ "${R10N_ACCESSIBLE:-}" != "1" ]; then
# BEGIN GENERATED R10N ASCII
Expand Down
33 changes: 26 additions & 7 deletions internal/brand/brand.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,17 @@ import (
)

const (
// Neon is the r10n wordmark's accessible foreground on dark surfaces.
Neon = "#67D5FF"
// LightAccent is its text/link variant for light backgrounds.
LightAccent = "#006B87"
// Neon is r10n's primary foreground from the user-supplied palette.
Neon = "#12F7D6"
// Secondary is the palette's electric-cyan supporting accent.
Secondary = "#00D1FF"
// ElectricBlue is the palette's focus and active-control accent.
ElectricBlue = "#007BFF"
// DarkBackground and LightForeground are the palette's surface/text pair.
DarkBackground = "#0B1320"
LightForeground = "#F8FAFF"
// LightAccent is a contrast-safe text/link shade derived from ElectricBlue.
LightAccent = "#0058B8"
)

// Accent returns the configured terminal-safe accent or adapts to the terminal.
Expand All @@ -28,15 +35,27 @@ func Accent() lipgloss.TerminalColor {
}
}

// FocusAccent returns a high-visibility control indicator for the current theme.
func FocusAccent() lipgloss.TerminalColor {
switch os.Getenv("R10N_THEME") {
case "dark":
return lipgloss.Color(Neon)
case "light":
return lipgloss.Color(ElectricBlue)
default:
return lipgloss.AdaptiveColor{Light: ElectricBlue, Dark: Neon}
}
}

// OnAccent returns readable text for a brand-colored button.
func OnAccent() lipgloss.TerminalColor {
switch os.Getenv("R10N_THEME") {
case "dark":
return lipgloss.Color("#062A38")
return lipgloss.Color(DarkBackground)
case "light":
return lipgloss.Color("#FFFFFF")
return lipgloss.Color(LightForeground)
default:
return lipgloss.AdaptiveColor{Light: "#FFFFFF", Dark: "#062A38"}
return lipgloss.AdaptiveColor{Light: LightForeground, Dark: DarkBackground}
}
}

Expand Down
20 changes: 20 additions & 0 deletions internal/brand/brand_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package brand

import (
"encoding/json"
"os"
"strings"
"testing"
Expand All @@ -21,3 +22,22 @@ func TestAccessibleBannerUsesReadableText(t *testing.T) {
t.Fatalf("accessible banner = %q", got)
}
}

func TestAccentColorsMatchCanonicalTokens(t *testing.T) {
data, err := os.ReadFile("../../assets/brand/tokens.json")
if err != nil {
t.Fatal(err)
}
var tokens map[string]string
if err := json.Unmarshal(data, &tokens); err != nil {
t.Fatal(err)
}
if tokens["brand_neon"] != Neon ||
tokens["brand_secondary"] != Secondary ||
tokens["electric_blue"] != ElectricBlue ||
tokens["dark_background"] != DarkBackground ||
tokens["light_foreground"] != LightForeground ||
tokens["brand_text_on_light"] != LightAccent {
t.Fatalf("terminal accents differ from canonical tokens: %#v", tokens)
}
}
40 changes: 38 additions & 2 deletions internal/cli/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"runtime"
"strconv"
"strings"
"sync/atomic"
"time"

"github.com/charmbracelet/huh"
Expand All @@ -28,8 +29,17 @@ import (

var releasesAPI = "https://api.github.com/repos/pruthivithejan/r10n/releases"
var upgradeHTTP = &http.Client{Timeout: 60 * time.Second}
var upgradeDownloadHTTP = newStreamingDownloadClient()
var executablePath = os.Executable

const downloadIdleTimeout = 2 * time.Minute

func newStreamingDownloadClient() *http.Client {
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.ResponseHeaderTimeout = 60 * time.Second
return &http.Client{Transport: transport}
}

type release struct {
Tag string `json:"tag_name"`
Assets []struct {
Expand Down Expand Up @@ -142,14 +152,29 @@ func (r release) asset(name string) (string, int64, error) {
}

func download(ctx context.Context, address, destination string, limit, expectedSize int64, label string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, address, nil)
downloadCtx, cancel := context.WithCancel(ctx)
defer cancel()
var idleTimeout atomic.Bool
idleTimer := time.AfterFunc(downloadIdleTimeout, func() {
idleTimeout.Store(true)
cancel()
})
defer idleTimer.Stop()

req, err := http.NewRequestWithContext(downloadCtx, http.MethodGet, address, nil)
if err != nil {
return err
}
req.Header.Set("User-Agent", "r10n-upgrade")
// Never send GITHUB_TOKEN to an asset host.
res, err := upgradeHTTP.Do(req)
res, err := upgradeDownloadHTTP.Do(req)
if err != nil {
if idleTimeout.Load() {
return fmt.Errorf("download stalled before receiving %s", tui.SafeText(label))
}
if ctx.Err() != nil {
return ctx.Err()
}
return err
}
defer res.Body.Close()
Expand Down Expand Up @@ -177,6 +202,7 @@ func download(ctx context.Context, address, destination string, limit, expectedS
for {
read, readErr := res.Body.Read(buffer)
if read > 0 {
idleTimer.Reset(downloadIdleTimeout)
n += int64(read)
if n > limit {
_ = out.Close()
Expand Down Expand Up @@ -210,6 +236,16 @@ func download(ctx context.Context, address, destination string, limit, expectedS
if readErr != nil {
_ = out.Close()
_ = os.Remove(destination)
if idleTimeout.Load() {
return fmt.Errorf(
"download stalled while receiving %s for %s",
tui.SafeText(label),
downloadIdleTimeout,
)
}
if ctx.Err() != nil {
return ctx.Err()
}
return readErr
}
}
Expand Down
47 changes: 45 additions & 2 deletions internal/cli/upgrade_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"runtime"
"strings"
"testing"
"time"
)

type archiveEntry struct {
Expand Down Expand Up @@ -234,6 +235,43 @@ func TestReleaseAssetCarriesItsAdvertisedByteSize(t *testing.T) {
}
}

func TestDownloadUsesStreamingTimeoutNotMetadataDeadline(t *testing.T) {
oldMetadataClient, oldDownloadClient := upgradeHTTP, upgradeDownloadHTTP
defer func() {
upgradeHTTP, upgradeDownloadHTTP = oldMetadataClient, oldDownloadClient
}()
upgradeHTTP = &http.Client{Timeout: 10 * time.Millisecond}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Length", "3")
flusher, ok := w.(http.Flusher)
if !ok {
t.Error("test server does not support streaming")
return
}
for _, chunk := range []byte("abc") {
if _, err := w.Write([]byte{chunk}); err != nil {
return
}
flusher.Flush()
time.Sleep(30 * time.Millisecond)
}
}))
defer server.Close()
upgradeDownloadHTTP = server.Client()
destination := filepath.Join(t.TempDir(), "asset")

if err := download(context.Background(), server.URL, destination, 1024, 3, "asset"); err != nil {
t.Fatal(err)
}
contents, err := os.ReadFile(destination)
if err != nil {
t.Fatal(err)
}
if string(contents) != "abc" {
t.Fatalf("downloaded bytes = %q, want %q", contents, "abc")
}
}

func TestPlatformVersionChecksum(t *testing.T) {
for _, test := range []struct{ os, arch, want string }{
{"linux", "amd64", "r10n-linux-x86_64.tar.gz"}, {"darwin", "arm64", "r10n-macos-arm64.tar.gz"}, {"windows", "amd64", "r10n-windows-x86_64.exe"},
Expand Down Expand Up @@ -283,8 +321,12 @@ func TestUpgradeMetadataCheckAndInstall(t *testing.T) {
t.Skip(err)
}
_, native := installedFixture(t)
oldExecutable, oldAPI, oldClient, oldOutput := executablePath, releasesAPI, upgradeHTTP, stdout
defer func() { executablePath, releasesAPI, upgradeHTTP, stdout = oldExecutable, oldAPI, oldClient, oldOutput }()
oldExecutable, oldAPI, oldClient, oldDownloadClient, oldOutput :=
executablePath, releasesAPI, upgradeHTTP, upgradeDownloadHTTP, stdout
defer func() {
executablePath, releasesAPI, upgradeHTTP, upgradeDownloadHTTP, stdout =
oldExecutable, oldAPI, oldClient, oldDownloadClient, oldOutput
}()
executablePath = func() (string, error) { return native, nil }
stdout = io.Discard
t.Setenv("R10N_SOURCE", "")
Expand Down Expand Up @@ -317,6 +359,7 @@ func TestUpgradeMetadataCheckAndInstall(t *testing.T) {
defer server.Close()
releasesAPI = server.URL + "/releases"
upgradeHTTP = server.Client()
upgradeDownloadHTTP = server.Client()
if err := upgrade(context.Background(), []string{"--check", "--version", "99.0.0"}); err != nil {
t.Fatal(err)
}
Expand Down
11 changes: 6 additions & 5 deletions internal/tui/forms.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,16 +187,17 @@ func runForm(ctx context.Context, form *huh.Form) error {

func theme() *huh.Theme {
colors := brand.Accent()
focus := brand.FocusAccent()
onAccent := brand.OnAccent()
t := huh.ThemeCharm()
t.Focused.Base = t.Focused.Base.BorderForeground(colors)
t.Focused.Base = t.Focused.Base.BorderForeground(focus)
t.Focused.Title = t.Focused.Title.Foreground(colors).Bold(true)
t.Focused.NoteTitle = t.Focused.NoteTitle.Foreground(colors).Bold(true)
t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(colors)
t.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(colors)
t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(focus)
t.Focused.MultiSelectSelector = t.Focused.MultiSelectSelector.Foreground(focus)
t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(colors)
t.Focused.SelectedPrefix = t.Focused.SelectedPrefix.Foreground(colors)
t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(colors)
t.Focused.SelectedPrefix = t.Focused.SelectedPrefix.Foreground(focus)
t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(focus)
t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(colors)
t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(onAccent).Background(colors)
t.Group.Title = t.Focused.Title
Expand Down
2 changes: 1 addition & 1 deletion scripts/generate_brand_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def render_wordmark(lines: list[str], color: str) -> str:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 '
f'{width} {height}" role="img" aria-labelledby="title desc">'
'<title id="title">r10n routine automation</title>'
'<desc id="desc">Neon cyan block-letter r10n wordmark.</desc>'
'<desc id="desc">Electric-blue block-letter r10n wordmark.</desc>'
f'<g fill="{color}" stroke="{color}" stroke-width="0.13" '
f'stroke-linecap="square">{rectangles}{strokes}</g></svg>\n'
)
Expand Down
4 changes: 2 additions & 2 deletions zensical.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ repo = "fontawesome/brands/github"
# Light Mode (Default)
[[project.theme.palette]]
scheme = "default"
primary = "light-blue"
primary = "blue"
accent = "cyan"
toggle.icon = "material/brightness-7"
toggle.name = "Switch to dark mode"
Expand All @@ -63,7 +63,7 @@ toggle.name = "Switch to dark mode"
[[project.theme.palette]]
scheme = "slate"
primary = "cyan"
accent = "light-blue"
accent = "cyan"
toggle.icon = "material/brightness-4"
toggle.name = "Switch to light mode"

Expand Down
Loading