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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- The Pulls screen opens on an index of the project's pull requests across the whole screen, and a pull request opens in its place instead of beside the list. A back button over its title returns to the index as you left it; beside it, the number opens a menu of the other pull requests, and previous and next walk the index's filter. On the Files tab only the file tree sits beside the diff, which gains the width the list took. The Overview puts the description beside a column with the reviewers, checks, mergeability and the checkout the branch sits in; the tab strip ends with the pull request's size. Open in Session and Merge stay in the header, with Resolve conflicts or Fix checks beside them when the pull request needs one, and Approve, Refresh and Open on GitHub move to the ⋯ menu. The screen lines up on the sidebar's spacing, its buttons are the size of the rest of the window's, and the code a review thread is about sits in a block of its own, apart from the comments. On the index, j and k or the arrows move, Enter opens and / goes to the filter; on a pull request, Escape goes back and Alt with an arrow steps to the previous or next one.
- The bundled Dark theme gives the interface and terminal the same neutral charcoal background, removing the terminal's blue-black cast, and uses a brighter terminal and syntax palette of green, coral, amber, blue, violet and cyan. The Light theme uses darker counterparts of those colors, tuned for contrast on its light terminal background.
- A session's browser is no longer closed without asking. When no pane shows it and nobody has used it for five minutes, a toast asks whether to close it: Close closes it, and Keep open, or swiping the toast away, keeps it for another five minutes. Unanswered, the toast stays up and the browser stays open, so a page left open over lunch is still there when you come back. Using the browser, or the agent using it, takes the toast down. Before, the browser was closed after five minutes away from its session, and its page, tabs and login were lost.

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion docs/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
surface, hairlines only as seams, state as fill. Values are the app's dark tokens (frontend/src/index.css). */
:root {
color-scheme: dark;
--background: oklch(0.141 0.005 285.823);
--background: #101012;
--canvas: color-mix(in oklab, var(--background) 65%, black);
--island: oklch(0.21 0.006 285.885);
--raised: oklch(0.274 0.006 286.033);
Expand Down
78 changes: 65 additions & 13 deletions native/cmd/lich-native/themes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,46 @@ func TestBundledThemesReadIntoTheIndexCSSPalettes(t *testing.T) {
}
}

func TestTerminalColorsFromTheTemplate(t *testing.T) {
func TestTerminalColorsFromBundledAndCustomThemes(t *testing.T) {
c, err := terminalOf(bundledTheme(t, "template"))
if err != nil {
t.Fatal(err)
}
if c.Background != rgb(0x15111f) || c.Cursor != rgb(0xc4a7ff) || c.ANSI[1] != rgb(0xff5c8a) {
t.Errorf("background %v cursor %v red %v", c.Background, c.Cursor, c.ANSI[1])
}
light, _ := terminalOf(bundledTheme(t, themeLight))
wantLightANSI := [16]color.NRGBA{
rgb(0x24292f), rgb(0xb33437), rgb(0x1a6b32), rgb(0x806000),
rgb(0x174ba0), rgb(0x8c3183), rgb(0x126a6f), rgb(0x4e5863),
rgb(0x3f4852), rgb(0xba3439), rgb(0x21753a), rgb(0x826100),
rgb(0x2359af), rgb(0x9b4291), rgb(0x16717a), rgb(0x5f6974),
}
if light.Background != rgb(0xe8e8ea) || light.Foreground != rgb(0x1f2328) || light.ANSI != wantLightANSI {
t.Errorf("Light terminal colors = %+v", light)
}
minimal, err := terminalOf(customTheme("minimal", "Minimal"))
if err != nil {
t.Fatal(err)
}
if minimal.ANSI != ([16]color.NRGBA{}) {
t.Errorf("custom theme without ANSI colors set some: %+v", minimal.ANSI)
}
dark, _ := terminalOf(bundledTheme(t, themeDark))
Comment thread
JoJowAkaGioGiow marked this conversation as resolved.
Resolved
if dark.Cursor.A != 0 || dark.ANSI[1].A != 0 {
t.Errorf("a theme naming no cursor or ANSI colors set some: %+v", dark)
wantANSI := [16]color.NRGBA{
rgb(0x151515), rgb(0xff646d), rgb(0x00cf92), rgb(0xffad33),
rgb(0x65adff), rgb(0xc992ff), rgb(0x50d6df), rgb(0xe7e7ec),
rgb(0x92929e), rgb(0xff8990), rgb(0x54e4b3), rgb(0xffc45f),
rgb(0x94c5ff), rgb(0xddb7ff), rgb(0x83e8ed), rgb(0xffffff),
}
if dark.Cursor.A != 0 {
t.Errorf("Dark cursor = %v, want libghostty's default", dark.Cursor)
}
if dark.Background != rgb(0x101012) || dark.Foreground != rgb(0xf1f1f4) {
t.Errorf("Dark terminal foreground/background = %v/%v", dark.Foreground, dark.Background)
}
if dark.ANSI != wantANSI {
t.Errorf("Dark ANSI palette = %v, want %v", dark.ANSI, wantANSI)
}
}

Expand Down Expand Up @@ -268,21 +297,44 @@ func resolved(h *godemirror.HighlightStyle) map[*tags.Tag]godemirror.TagStyle {
return out
}

// The bundled themes write godemirror's own palettes out tag by tag, so the
// code views look as they did before themes carried syntax.
func TestBundledThemesHighlightAsGodemirrorsDefaults(t *testing.T) {
for id, want := range map[string]godemirror.Theme{themeLight: godemirror.LightTheme, themeDark: godemirror.DarkTheme} {
// Bundled syntax colors override the palette while preserving each tag's other styles.
func TestBundledSyntaxUsesPaletteColorsAndRetainsStyles(t *testing.T) {
for id, baseline := range map[string]godemirror.Theme{themeLight: godemirror.LightTheme, themeDark: godemirror.DarkTheme} {
e, err := readTheme(bundledTheme(t, id))
if err != nil {
t.Fatal(err)
}
got, exp := resolved(e.code.Highlight), resolved(want.Highlight)
if len(got) != len(exp) {
t.Fatalf("%s: %d tags styled, godemirror styles %d", id, len(got), len(exp))
got := resolved(e.code.Highlight)
want := resolved(baseline.Highlight)
if len(got) != len(want) {
t.Fatalf("%s: %d tags styled, want %d", id, len(got), len(want))
}
for tag, style := range exp {
if !reflect.DeepEqual(got[tag], style) {
t.Errorf("%s: %s = %+v, want %+v", id, tag, got[tag], style)
for tag, style := range want {
actual, ok := got[tag]
if !ok {
t.Errorf("%s: tag %s is missing", id, tag)
continue
}
actual.Color = style.Color
if !reflect.DeepEqual(actual, style) {
t.Errorf("%s: %s style = %+v, want non-color styles %+v", id, tag, actual, style)
}
}
samples := map[string]map[string]color.NRGBA{
themeDark: {
"keyword": rgb(0xc992ff), "string": rgb(0x00cf92), "function(variableName)": rgb(0x65adff),
},
themeLight: {
"keyword": rgb(0x8c3183), "string": rgb(0x1a6b32), "function(variableName)": rgb(0x174ba0),
},
}
for name, wantColor := range samples[id] {
tag, err := tags.Parse(name)
if err != nil {
t.Fatal(err)
}
if got[tag].Color != wantColor {
t.Errorf("%s: %s color = %v, want %v", id, name, got[tag].Color, wantColor)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion native/lichdiff/buttons.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ var (
foreground = color.NRGBA{R: 0xfa, G: 0xfa, B: 0xfa, A: 0xff}
mutedForeground = color.NRGBA{R: 0x9f, G: 0x9f, B: 0xa9, A: 0xff}
accent = color.NRGBA{R: 0x27, G: 0x27, B: 0x2a, A: 0xff}
background = color.NRGBA{R: 0x09, G: 0x09, B: 0x0b, A: 0xff}
background = color.NRGBA{R: 0x10, G: 0x10, B: 0x12, A: 0xff}
border = color.NRGBA{R: 0x22, G: 0x22, B: 0x24, A: 0xff}
)

Expand Down
7 changes: 3 additions & 4 deletions native/ui/checkbox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,8 @@ func TestCheckboxCheckedFillsWithPrimary(t *testing.T) {
if img := draw(true); !toggleIs(img, inside, th.Primary) {
t.Errorf("checked: pixel %v, want primary %v", img.RGBAAt(inside.X, inside.Y), th.Primary)
}
// input/30 (white at 15% x 30%) over the page, composited in sRGB as the
// browser does: #09090b + 4.5% of the way to white.
if img, want := draw(false), (color.NRGBA{R: 20, G: 20, B: 22, A: 255}); !toggleIs(img, inside, want) {
// input/30 (white at 15% x 30%) over #101012, composited in sRGB.
if img, want := draw(false), (color.NRGBA{R: 26, G: 26, B: 28, A: 255}); !toggleIs(img, inside, want) {
t.Errorf("unchecked: pixel %v, want input/30 over the page %v", img.RGBAAt(inside.X, inside.Y), want)
}
}
Expand All @@ -95,7 +94,7 @@ func TestCheckboxDisabledFadesOntoThePage(t *testing.T) {
cb := th.Checkbox(&widget.Bool{Value: true})
cb.Disabled = true
img := toggleRender(t, image.Pt(16, 16), time.Time{}, cb.Layout)
if want := (color.NRGBA{R: 119, G: 119, B: 121, A: 255}); !toggleIs(img, image.Pt(3, 13), want) {
if want := (color.NRGBA{R: 122, G: 122, B: 124, A: 255}); !toggleIs(img, image.Pt(3, 13), want) {
t.Errorf("disabled checked: pixel %v, want primary at 50%% over the page %v", img.RGBAAt(3, 13), want)
}
}
16 changes: 8 additions & 8 deletions native/ui/input_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ func TestInputFocusRingDrawsOnlyWhenFocused(t *testing.T) {
}
}

// The box of an input over the page (#09090b), as the browser composites it:
// The box of an input over the page (#101012), as the browser composites it:
// bg-input/30 for the fill, border-input over the fill, ring-ring/50 over the
// page. Gio would blend each about twice as bright.
func TestInputBoxPaintsTheBrowsersColors(t *testing.T) {
Expand All @@ -188,17 +188,17 @@ func TestInputBoxPaintsTheBrowsersColors(t *testing.T) {
rig := newInputRig(t, 160, 36, in.Layout)
img := rig.frame()
at := func(x, y int) image.Point { return image.Pt(rig.margin+x, rig.margin+y) }
if want := (color.NRGBA{R: 20, G: 20, B: 22, A: 255}); !toggleIs(img, at(4, 18), want) {
if want := (color.NRGBA{R: 26, G: 26, B: 28, A: 255}); !toggleIs(img, at(4, 18), want) {
t.Errorf("fill %v, want input/30 over the page %v", img.RGBAAt(at(4, 18).X, at(4, 18).Y), want)
}
if want := (color.NRGBA{R: 55, G: 55, B: 57, A: 255}); !toggleIs(img, at(80, 0), want) {
if want := (color.NRGBA{R: 60, G: 60, B: 62, A: 255}); !toggleIs(img, at(80, 0), want) {
t.Errorf("border %v, want input over the fill %v", img.RGBAAt(at(80, 0).X, at(80, 0).Y), want)
}

rig.router.Source().Execute(key.FocusCmd{Tag: ed})
rig.frame()
img = rig.frame()
if want := (color.NRGBA{R: 61, G: 61, B: 66, A: 255}); !toggleIs(img, at(-2, 18), want) {
if want := (color.NRGBA{R: 64, G: 64, B: 70, A: 255}); !toggleIs(img, at(-2, 18), want) {
t.Errorf("focus ring %v, want ring/50 over the page %v", img.RGBAAt(at(-2, 18).X, at(-2, 18).Y), want)
}
if want := th.Ring; !toggleIs(img, at(80, 0), want) {
Expand All @@ -224,15 +224,15 @@ func TestBareInputDrawsNoBox(t *testing.T) {
}
}

// disabled:opacity-50 fades the finished box: the fill (#14141 6) at half over
// disabled:opacity-50 fades the finished box: the fill (#1a1a1c) at half over
// the page.
func TestInputDisabledFadesOntoThePage(t *testing.T) {
th := testTheme()
in := th.Input(new(widget.Editor), "Name")
in.Disabled = true
rig := newInputRig(t, 160, 36, in.Layout)
p := image.Pt(rig.margin+4, rig.margin+18)
if img, want := rig.frame(), (color.NRGBA{R: 14, G: 14, B: 16, A: 255}); !toggleIs(img, p, want) {
if img, want := rig.frame(), (color.NRGBA{R: 21, G: 21, B: 23, A: 255}); !toggleIs(img, p, want) {
t.Errorf("fill %v, want input/30 at half over the page %v", img.RGBAAt(p.X, p.Y), want)
}
}
Expand All @@ -241,11 +241,11 @@ func TestInputDisabledFadesOntoThePage(t *testing.T) {
func TestInputGroupInnerDrawsOnTheFill(t *testing.T) {
th := testTheme()
g := th.InputGroup(new(widget.Editor), "Filter")
if want := (color.NRGBA{R: 20, G: 20, B: 22, A: 255}); !near2(g.Inner().Surface, want) {
if want := (color.NRGBA{R: 26, G: 26, B: 28, A: 255}); !near2(g.Inner().Surface, want) {
t.Errorf("surface %v, want input/30 over the page %v", g.Inner().Surface, want)
}
g.Disabled = true
if want := (color.NRGBA{R: 14, G: 14, B: 16, A: 255}); !near2(g.Inner().Surface, want) {
if want := (color.NRGBA{R: 21, G: 21, B: 23, A: 255}); !near2(g.Inner().Surface, want) {
t.Errorf("disabled surface %v, want the fill at half over the page %v", g.Inner().Surface, want)
}
}
Expand Down
4 changes: 2 additions & 2 deletions native/ui/label_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ func labelInk(t *testing.T, l FormLabelStyle) color.RGBA {
return ink
}

// opacity-50 over the page: foreground (#fafafa) halfway to #09090b.
// opacity-50 over the page: foreground (#fafafa) halfway to #101012.
func TestLabelDisabledIsHalfOpacity(t *testing.T) {
th := testTheme()
on, off := th.Label("Subject"), th.Label("Subject")
off.Disabled = true
wantOn, wantOff := color.RGBA{R: 250, G: 250, B: 250, A: 255}, color.RGBA{R: 130, G: 130, B: 131, A: 255}
wantOn, wantOff := color.RGBA{R: 250, G: 250, B: 250, A: 255}, color.RGBA{R: 133, G: 133, B: 134, A: 255}
within := func(a, b color.RGBA) bool {
d := func(x, y uint8) bool { return int(x)-int(y) <= 2 && int(y)-int(x) <= 2 }
return d(a.R, b.R) && d(a.G, b.G) && d(a.B, b.B) && a.A == b.A
Expand Down
6 changes: 3 additions & 3 deletions native/ui/skeleton_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,11 @@ func skeletonAt(t *testing.T, at time.Duration) color.NRGBA {
}

// animate-pulse: full opacity at both ends of its 2s, half of it at 1s. The
// browser lays bg-foreground/20 (#fafafa at 20%) over the page (#09090b) in
// sRGB: #39393b, and half of that opacity, #212123.
// browser lays bg-foreground/20 (#fafafa at 20%) over the page (#101012) in
// sRGB: #3f3f40, and half of that opacity, #272729.
func TestSkeletonPulsesBetweenFullAndHalf(t *testing.T) {
full, half, again := skeletonAt(t, 0), skeletonAt(t, time.Second), skeletonAt(t, 2*time.Second)
wantFull, wantHalf := color.NRGBA{R: 57, G: 57, B: 59, A: 255}, color.NRGBA{R: 33, G: 33, B: 35, A: 255}
wantFull, wantHalf := color.NRGBA{R: 63, G: 63, B: 64, A: 255}, color.NRGBA{R: 39, G: 39, B: 41, A: 255}
close := func(a, b color.NRGBA) bool {
d := func(x, y uint8) bool { return int(x)-int(y) <= 1 && int(y)-int(x) <= 1 }
return d(a.R, b.R) && d(a.G, b.G) && d(a.B, b.B) && a.A == b.A
Expand Down
4 changes: 2 additions & 2 deletions native/ui/switch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func TestSwitchDisabledThumbSitsOnThePageNotTheTrack(t *testing.T) {
s := testTheme().Switch(state)
s.Disabled = true
img := toggleRender(t, image.Pt(32, 18), time.Unix(1000, 0), s.Layout)
thumb, track := color.NRGBA{R: 17, G: 17, B: 19, A: 255}, color.NRGBA{R: 119, G: 119, B: 121, A: 255}
thumb, track := color.NRGBA{R: 20, G: 20, B: 22, A: 255}, color.NRGBA{R: 122, G: 122, B: 124, A: 255}
if !toggleIs(img, image.Pt(23, 9), thumb) {
t.Errorf("thumb pixel %v, want primary-foreground at half over the page %v", img.RGBAAt(23, 9), thumb)
}
Expand All @@ -115,7 +115,7 @@ func TestSwitchDisabledThumbSitsOnThePageNotTheTrack(t *testing.T) {
// dark:data-unchecked:bg-input/80 is white at 12% over the page.
func TestSwitchOffTrackIsInputOverThePage(t *testing.T) {
img := toggleRender(t, image.Pt(32, 18), time.Unix(1000, 0), testTheme().Switch(new(SwitchState)).Layout)
if want := (color.NRGBA{R: 38, G: 38, B: 40, A: 255}); !toggleIs(img, image.Pt(28, 9), want) {
if want := (color.NRGBA{R: 44, G: 44, B: 46, A: 255}); !toggleIs(img, image.Pt(28, 9), want) {
t.Errorf("off track pixel %v, want input/80 over the page %v", img.RGBAAt(28, 9), want)
}
}
4 changes: 2 additions & 2 deletions native/ui/theme.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ type Palette struct {

// Dark is the dark theme (themes/dark.json).
var Dark = Palette{
Background: OKLCH(0.141, 0.005, 285.823, 1),
Background: color.NRGBA{R: 0x10, G: 0x10, B: 0x12, A: 0xff},
Foreground: OKLCH(0.985, 0, 0, 1),
// color-mix(in oklab, var(--background) 65%, black).
Canvas: MixOKLab(OKLCH(0.141, 0.005, 285.823, 1), color.NRGBA{A: 255}, 0.35),
Canvas: MixOKLab(color.NRGBA{R: 0x10, G: 0x10, B: 0x12, A: 0xff}, color.NRGBA{A: 255}, 0.35),
Card: OKLCH(0.21, 0.006, 285.885, 1),
CardForeground: OKLCH(0.985, 0, 0, 1),
Popover: OKLCH(0.21, 0.006, 285.885, 1),
Expand Down
2 changes: 1 addition & 1 deletion native/ui/toggle_group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func TestToggleGroupTrackBorderIsBorderOverThePage(t *testing.T) {
group := testTheme().ToggleGroup(new(widget.Enum), toggleGroupItems...)
size := layoutAt(group.Layout, 0, 500).Size
img := toggleRender(t, size, time.Time{}, group.Layout)
if want := (color.NRGBA{R: 33, G: 33, B: 35, A: 255}); !toggleIs(img, image.Pt(size.X/2, 0), want) {
if want := (color.NRGBA{R: 40, G: 40, B: 42, A: 255}); !toggleIs(img, image.Pt(size.X/2, 0), want) {
t.Errorf("top edge %v, want border over the page %v", img.RGBAAt(size.X/2, 0), want)
}
}
Expand Down
6 changes: 3 additions & 3 deletions native/ui/toggle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,9 @@ func TestTogglePressedIsAccentFill(t *testing.T) {
if img := draw(false, nil); img.RGBAAt(3, 18).A != 0 {
t.Errorf("unpressed: pixel %v, want transparent", img.RGBAAt(3, 18))
}
// opacity-50 over the page: accent (#27272a) halfway to #09090b.
// opacity-50 over the page: accent (#27272a) halfway to #101012.
disabled := draw(true, func(tg *ToggleStyle) { tg.Disabled = true })
if want := (color.NRGBA{R: 24, G: 24, B: 27, A: 255}); !toggleIs(disabled, image.Pt(3, 18), want) {
if want := (color.NRGBA{R: 27, G: 27, B: 30, A: 255}); !toggleIs(disabled, image.Pt(3, 18), want) {
t.Errorf("disabled pressed: pixel %v, want accent at half over the page %v", disabled.RGBAAt(3, 18), want)
}
}
Expand All @@ -189,7 +189,7 @@ func TestToggleOutlineHasBorderDefaultHasNone(t *testing.T) {
return toggleRender(t, image.Pt(80, 36), time.Time{}, tg.Layout).RGBAAt(40, 0)
}
// border-input is white at 15% over the page.
if p, want := edge(ToggleVariantOutline), (color.NRGBA{R: 45, G: 45, B: 47, A: 255}); !near2(color.NRGBA(p), want) {
if p, want := edge(ToggleVariantOutline), (color.NRGBA{R: 52, G: 52, B: 53, A: 255}); !near2(color.NRGBA(p), want) {
t.Errorf("outline: top edge %v, want input over the page %v", p, want)
}
if p := edge(ToggleVariantDefault); p.A != 0 {
Expand Down
Loading
Loading