Add notifications.
This commit is contained in:
@@ -119,10 +119,10 @@ didn't touch, unless the change made it newly wrong. In rough priority order:
|
|||||||
leaves state half-updated, panics on input the caller controls.
|
leaves state half-updated, panics on input the caller controls.
|
||||||
3. **Resources and lifetimes** — leaks, missing close/cancel, work that outlives
|
3. **Resources and lifetimes** — leaks, missing close/cancel, work that outlives
|
||||||
what it belongs to.
|
what it belongs to.
|
||||||
4. **Concurrency** — data races, state mutated without the lock its neighbours
|
4. **Concurrency** — data races, state mutated without the lock its neighbors
|
||||||
take, deadlock ordering.
|
take, deadlock ordering.
|
||||||
5. **Interface and contract** — a caller that can now be silently wrong; a
|
5. **Interface and contract** — a caller that can now be silently wrong; a
|
||||||
behaviour change not reflected in the doc comment right above it.
|
behavior change not reflected in the doc comment right above it.
|
||||||
6. **Tests** — a new branch with real failure modes and no test; a test that
|
6. **Tests** — a new branch with real failure modes and no test; a test that
|
||||||
would pass with the bug still in.
|
would pass with the bug still in.
|
||||||
7. **Fit** — code that ignores an existing helper, layering, or naming pattern
|
7. **Fit** — code that ignores an existing helper, layering, or naming pattern
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ just the VT core.
|
|||||||
|
|
||||||
```
|
```
|
||||||
main.zig AdwApplication startup
|
main.zig AdwApplication startup
|
||||||
appearance.zig the colour scheme: preference -> libadwaita, CSS, palette
|
appearance.zig the color scheme: preference -> libadwaita, CSS, palette
|
||||||
Window.zig sidebar + GtkStack of views, tab management, reordering, shortcuts
|
Window.zig sidebar + GtkStack of views, tab management, reordering, shortcuts
|
||||||
View.zig one tab's content: its panes, their layout, and drag handling
|
View.zig one tab's content: its panes, their layout, and drag handling
|
||||||
Layout.zig the split tree: nodes, rearranging, GtkPaned materialization
|
Layout.zig the split tree: nodes, rearranging, GtkPaned materialization
|
||||||
@@ -141,11 +141,12 @@ Session.zig libghostty-vt Terminal + parser, fed by the PTY
|
|||||||
Pty.zig openpt/fork/exec, controlling terminal setup
|
Pty.zig openpt/fork/exec, controlling terminal setup
|
||||||
script.zig runs a command for `$(...)` in a layout's directory
|
script.zig runs a command for `$(...)` in a layout's directory
|
||||||
key.zig GDK keyval -> libghostty-vt key mapping
|
key.zig GDK keyval -> libghostty-vt key mapping
|
||||||
palette.zig every colour by name, its default per scheme, and the CSS for it
|
notify.zig desktop notifications on a finish, and the mutes that stop them
|
||||||
|
palette.zig every color by name, its default per scheme, and the CSS for it
|
||||||
theme.zig the palette resolved for the terminal renderer, which reads numbers
|
theme.zig the palette resolved for the terminal renderer, which reads numbers
|
||||||
Settings.zig preferences: theme, palette overrides, startup tabs, JSON on disk
|
Settings.zig preferences: theme, palette overrides, startup tabs, JSON on disk
|
||||||
SettingsDialog.zig the settings page, including the startup list editor
|
SettingsDialog.zig the settings page, including the startup list editor
|
||||||
PaletteEditor.zig the colour editor inside it: a swatch per palette entry
|
PaletteEditor.zig the color editor inside it: a swatch per palette entry
|
||||||
TabSettingsDialog.zig one tab's own settings, and the emoji picker
|
TabSettingsDialog.zig one tab's own settings, and the emoji picker
|
||||||
emoji.zig generated: every emoji the picker offers, and the search over them
|
emoji.zig generated: every emoji the picker offers, and the search over them
|
||||||
```
|
```
|
||||||
@@ -182,7 +183,7 @@ Running terminals keep their scrollback and processes straight through a move.
|
|||||||
**Drags rearrange live.** Each time the drop target changes, the move is
|
**Drags rearrange live.** Each time the drop target changes, the move is
|
||||||
applied for real, so the layout under the cursor is always the layout you will
|
applied for real, so the layout under the cursor is always the layout you will
|
||||||
get — there is no separate drop indicator because the view itself is the
|
get — there is no separate drop indicator because the view itself is the
|
||||||
preview. Cancelling a drag puts the pane back: only the dragged pane ever
|
preview. Canceling a drag puts the pane back: only the dragged pane ever
|
||||||
moves, so the rest of the tree is unchanged and re-inserting it beside its
|
moves, so the rest of the tree is unchanged and re-inserting it beside its
|
||||||
original sibling restores the original shape.
|
original sibling restores the original shape.
|
||||||
|
|
||||||
@@ -225,7 +226,7 @@ save it, and replace the literal paths with `{{parameters}}`.
|
|||||||
name, parameters and per-pane scripts can be changed without opening it.
|
name, parameters and per-pane scripts can be changed without opening it.
|
||||||
Renaming moves the layout rather than copying it, and renaming onto a name
|
Renaming moves the layout rather than copying it, and renaming onto a name
|
||||||
another layout already has is refused instead of quietly replacing it. Nothing
|
another layout already has is refused instead of quietly replacing it. Nothing
|
||||||
is written until you confirm, so cancelling leaves the layout untouched. To
|
is written until you confirm, so canceling leaves the layout untouched. To
|
||||||
change the *shape* of a layout, open it, rearrange the tab, and save over it
|
change the *shape* of a layout, open it, rearrange the tab, and save over it
|
||||||
under the same name — the same tools you used to build it in the first place.
|
under the same name — the same tools you used to build it in the first place.
|
||||||
|
|
||||||
@@ -269,7 +270,7 @@ means `string`:
|
|||||||
{ "name": "path", "description": "Project directory", "type": "directory" }
|
{ "name": "path", "description": "Project directory", "type": "directory" }
|
||||||
```
|
```
|
||||||
|
|
||||||
A type this build doesn't recognise is asked for in a text box rather than
|
A type this build doesn't recognize is asked for in a text box rather than
|
||||||
refusing the file — unlike a pane `kind`, which is a pane it cannot build. The
|
refusing the file — unlike a pane `kind`, which is a pane it cannot build. The
|
||||||
startup list asks for its values the same way, so a directory parameter gets the
|
startup list asks for its values the same way, so a directory parameter gets the
|
||||||
same field there.
|
same field there.
|
||||||
@@ -451,7 +452,7 @@ to notice you are about to close the wrong window.
|
|||||||
|
|
||||||
**Keep Working** is the default response, and Escape lands on it too: the guard
|
**Keep Working** is the default response, and Escape lands on it too: the guard
|
||||||
should not itself be a keystroke to fumble. Pressing the compositor's close
|
should not itself be a keystroke to fumble. Pressing the compositor's close
|
||||||
binding again while the question is up dismisses it, which counts as cancelling
|
binding again while the question is up dismisses it, which counts as canceling
|
||||||
— leaning on the key never costs the window.
|
— leaning on the key never costs the window.
|
||||||
|
|
||||||
Two closes don't ask, because both are already an answer: accepting the dialog,
|
Two closes don't ask, because both are already an answer: accepting the dialog,
|
||||||
@@ -511,6 +512,11 @@ to closing the moment the window manager says so:
|
|||||||
- **Pane status in the tab strip**, driven by OSC 9;4, so a tab can say whether
|
- **Pane status in the tab strip**, driven by OSC 9;4, so a tab can say whether
|
||||||
it is working, waiting on you, or finished and still unanswered. See
|
it is working, waiting on you, or finished and still unanswered. See
|
||||||
[Agent status](#agent-status)
|
[Agent status](#agent-status)
|
||||||
|
- **A desktop notification when a pane finishes**, for the tabs you are not
|
||||||
|
looking at — on by default, switchable from **Settings → Notifications**, and
|
||||||
|
mutable one tab at a time from its right-click menu, for fifteen minutes, an
|
||||||
|
hour, eight hours, or until you say otherwise. See
|
||||||
|
[Notifications](#notifications)
|
||||||
- **Renaming a tab**: `Ctrl+Shift+R`, double-click a tab row, or **Rename** in
|
- **Renaming a tab**: `Ctrl+Shift+R`, double-click a tab row, or **Rename** in
|
||||||
its right-click menu. A typed name pins the label; clearing it hands the
|
its right-click menu. A typed name pins the label; clearing it hands the
|
||||||
label back to the panes
|
label back to the panes
|
||||||
@@ -531,11 +537,11 @@ to closing the moment the window manager says so:
|
|||||||
- **Light and dark schemes**, following the desktop by default and pinnable
|
- **Light and dark schemes**, following the desktop by default and pinnable
|
||||||
from the settings page, applying to open tabs immediately — terminal palette
|
from the settings page, applying to open tabs immediately — terminal palette
|
||||||
included. See [Theme](#theme)
|
included. See [Theme](#theme)
|
||||||
- **Every colour in the palette editable**, per scheme, from **Settings →
|
- **Every color in the palette editable**, per scheme, from **Settings →
|
||||||
Appearance → Customise**: the surfaces, the text on them, the accents, the
|
Appearance → Customize**: the surfaces, the text on them, the accents, the
|
||||||
status colours, and the terminal's own background, cursor, selection and 16
|
status colors, and the terminal's own background, cursor, selection and 16
|
||||||
ANSI colours. Each change repaints as you make it. See
|
ANSI colors. Each change repaints as you make it. See
|
||||||
[Customising the palette](#customising-the-palette)
|
[Customizing the palette](#customizing-the-palette)
|
||||||
|
|
||||||
### Shortcuts
|
### Shortcuts
|
||||||
|
|
||||||
@@ -725,7 +731,7 @@ table, and add to its `SYNONYMS` when a glyph should answer to a word the data
|
|||||||
files don't know. Two things it leaves out. Skin-tone variants, because just over
|
files don't know. Two things it leaves out. Skin-tone variants, because just over
|
||||||
half of Unicode's 3944 sequences are the same gesture in five tones and a grid of
|
half of Unicode's 3944 sequences are the same gesture in five tones and a grid of
|
||||||
them is longer to look through rather than more complete — every base glyph is
|
them is longer to look through rather than more complete — every base glyph is
|
||||||
there. And anything newer than emoji 16.0, because colour fonts trail Unicode by
|
there. And anything newer than emoji 16.0, because color fonts trail Unicode by
|
||||||
a year or two and a glyph the font has never heard of draws as a hex-digit box,
|
a year or two and a glyph the font has never heard of draws as a hex-digit box,
|
||||||
which in a picker reads as a bug; `--max-version` raises the cutoff once fonts
|
which in a picker reads as a bug; `--max-version` raises the cutoff once fonts
|
||||||
have caught up.
|
have caught up.
|
||||||
@@ -923,11 +929,11 @@ So a tab can carry a dot:
|
|||||||
| red | stopped on an error |
|
| red | stopped on an error |
|
||||||
| none | idle |
|
| none | idle |
|
||||||
|
|
||||||
Each colour is carried at three sizes, so it survives being glanced at rather
|
Each color is carried at three sizes, so it survives being glanced at rather
|
||||||
than read: an 11px dot on the row, a bar down the row's leading edge, and a wash
|
than read: an 11px dot on the row, a bar down the row's leading edge, and a wash
|
||||||
behind the whole row. Working gets the bar without the wash — it is the resting
|
behind the whole row. Working gets the bar without the wash — it is the resting
|
||||||
state of a busy afternoon, and tinting half the sidebar all day would only teach
|
state of a busy afternoon, and tinting half the sidebar all day would only teach
|
||||||
you to stop looking. Inside a tab, a pane that is asking colours its own frame
|
you to stop looking. Inside a tab, a pane that is asking colors its own frame
|
||||||
and header the same way, which is what picks it out of a four-way split.
|
and header the same way, which is what picks it out of a four-way split.
|
||||||
|
|
||||||
The green one is the point of the feature. A session that has gone back to idle
|
The green one is the point of the feature. A session that has gone back to idle
|
||||||
@@ -959,6 +965,48 @@ so once the row has brought you to the tab, they say which pane inside it was
|
|||||||
the one asking; the row stays lit until every finished pane in it has been
|
the one asking; the row stays lit until every finished pane in it has been
|
||||||
answered.
|
answered.
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
|
||||||
|
The dots answer "which of these needs me?" for a window that is in front of you.
|
||||||
|
When it isn't — behind an editor, on another workspace, or on the other monitor —
|
||||||
|
a pane finishing also posts a **desktop notification**, titled with the tab's
|
||||||
|
name and carrying that pane's title as its body. That title is the useful half:
|
||||||
|
the hook sets it from your prompt, so what the popup says is the task that just
|
||||||
|
finished rather than "playpen".
|
||||||
|
|
||||||
|
It is on by default and **Settings → Notifications** switches it off, which
|
||||||
|
leaves the sidebar dots as the only word you get:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"notifications": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Nothing is posted for a tab you were already looking at — the window has the
|
||||||
|
focus and that tab is the one on screen — because the dot has already said it
|
||||||
|
and a popup about the pane in front of you is the fastest way to make someone
|
||||||
|
switch the whole feature off. Coming back to a tab also takes down whatever
|
||||||
|
popup it left in the tray, the same visit that clears its row.
|
||||||
|
|
||||||
|
**One tab at a time can be muted** from its right-click menu, for 15 minutes, an
|
||||||
|
hour, 8 hours, or until you unmute it. That is the answer for the one session
|
||||||
|
that finishes every ninety seconds while the other three are worth hearing
|
||||||
|
about; reopening the menu says how long the mute has left and offers the way out
|
||||||
|
of it. A mute is session state and is not written to the settings file — a tab
|
||||||
|
does not outlive the app, and neither should a decision about the next hour.
|
||||||
|
Neither mute touches the dots. Muting is about being interrupted; the row is
|
||||||
|
still how you find out what happened while you were away.
|
||||||
|
|
||||||
|
The notification is sent as a `GNotification`, so GIO picks whichever backend
|
||||||
|
the session has and the popup gets Playpen's name and icon from the installed
|
||||||
|
desktop entry — which means it looks right once `mise run install` has put that
|
||||||
|
entry in place. Nothing is attached to a click: the app is deliberately
|
||||||
|
non-unique, so no process owns `dev.greyson.playpen` on the session bus and a
|
||||||
|
daemon calling an action back would find nobody home. Clicking closes the popup;
|
||||||
|
getting back to the window is the compositor's job, and it already has a binding
|
||||||
|
for that.
|
||||||
|
|
||||||
### How it gets there
|
### How it gets there
|
||||||
|
|
||||||
Programs report state with **OSC 9;4** — the ConEmu progress protocol, the same
|
Programs report state with **OSC 9;4** — the ConEmu progress protocol, the same
|
||||||
@@ -1049,8 +1097,8 @@ light and a dark scheme. `Ctrl+,` or the gear at the foot of the sidebar opens
|
|||||||
**Settings**, whose Appearance section holds the choice: light, dark, or
|
**Settings**, whose Appearance section holds the choice: light, dark, or
|
||||||
**system**, which is the default and follows the desktop.
|
**system**, which is the default and follows the desktop.
|
||||||
|
|
||||||
Every colour it resolves to can be changed — see
|
Every color it resolves to can be changed — see
|
||||||
[Customising the palette](#customising-the-palette).
|
[Customizing the palette](#customizing-the-palette).
|
||||||
|
|
||||||
The preference lives in `~/.config/playpen/settings.json` (or
|
The preference lives in `~/.config/playpen/settings.json` (or
|
||||||
`$XDG_CONFIG_HOME`), beside `layouts.json`, alongside the
|
`$XDG_CONFIG_HOME`), beside `layouts.json`, alongside the
|
||||||
@@ -1061,6 +1109,7 @@ The preference lives in `~/.config/playpen/settings.json` (or
|
|||||||
"version": 1,
|
"version": 1,
|
||||||
"theme": "system",
|
"theme": "system",
|
||||||
"confirm_quit": true,
|
"confirm_quit": true,
|
||||||
|
"notifications": true,
|
||||||
"colors": {},
|
"colors": {},
|
||||||
"startup": []
|
"startup": []
|
||||||
}
|
}
|
||||||
@@ -1069,20 +1118,20 @@ The preference lives in `~/.config/playpen/settings.json` (or
|
|||||||
Switching applies immediately to every open tab — nothing needs restarting, and
|
Switching applies immediately to every open tab — nothing needs restarting, and
|
||||||
a shell that has been running all day repaints along with everything else.
|
a shell that has been running all day repaints along with everything else.
|
||||||
|
|
||||||
Three separate colour systems have to agree for that to be true, which is what
|
Three separate color systems have to agree for that to be true, which is what
|
||||||
`appearance.zig` exists to arrange:
|
`appearance.zig` exists to arrange:
|
||||||
|
|
||||||
- **libadwaita's style manager** colours the stock widgets — popovers, entries,
|
- **libadwaita's style manager** colors the stock widgets — popovers, entries,
|
||||||
dialog chrome. It is told to force a scheme, or left on `default` to follow
|
dialog chrome. It is told to force a scheme, or left on `default` to follow
|
||||||
the desktop.
|
the desktop.
|
||||||
- **`style.css`** colours everything the app draws itself. It is written
|
- **`style.css`** colors everything the app draws itself. It is written
|
||||||
entirely against named colours; `palette.zig` writes the `@define-color`
|
entirely against named colors; `palette.zig` writes the `@define-color`
|
||||||
block that defines them for the scheme in force, and the two are loaded as a
|
block that defines them for the scheme in force, and the two are loaded as a
|
||||||
single `GtkCssProvider`. No rule in `style.css` may hardcode a colour — a
|
single `GtkCssProvider`. No rule in `style.css` may hardcode a color — a
|
||||||
literal hex is a rule that looks right in whichever scheme you happened to be
|
literal hex is a rule that looks right in whichever scheme you happened to be
|
||||||
testing in, and one the colour editor cannot reach.
|
testing in, and one the color editor cannot reach.
|
||||||
- **`theme.zig`** holds what Cairo draws the terminal grid from: the default
|
- **`theme.zig`** holds what Cairo draws the terminal grid from: the default
|
||||||
background, foreground and cursor, plus the 16 ANSI colours. The style tree
|
background, foreground and cursor, plus the 16 ANSI colors. The style tree
|
||||||
is never consulted there, so a CSS reload alone would leave every terminal
|
is never consulted there, so a CSS reload alone would leave every terminal
|
||||||
painted in the scheme it started in. It resolves the same table `style.css`
|
painted in the scheme it started in. It resolves the same table `style.css`
|
||||||
is fed from, and caches the result — the renderer asks for the background
|
is fed from, and caches the result — the renderer asks for the background
|
||||||
@@ -1092,7 +1141,7 @@ Three separate colour systems have to agree for that to be true, which is what
|
|||||||
The ANSI palette is the part that is easy to skip and shouldn't be. The
|
The ANSI palette is the part that is easy to skip and shouldn't be. The
|
||||||
standard xterm yellow is `#cdcd00`, which on a white background is close to
|
standard xterm yellow is `#cdcd00`, which on a white background is close to
|
||||||
invisible — and prompts and build tools use it constantly, so a light scheme
|
invisible — and prompts and build tools use it constantly, so a light scheme
|
||||||
without a light palette is a light scheme you can't read. The 240 colours above
|
without a light palette is a light scheme you can't read. The 240 colors above
|
||||||
index 16 are fixed by spec and left alone; only the 16 named ones change.
|
index 16 are fixed by spec and left alone; only the 16 named ones change.
|
||||||
|
|
||||||
They are swapped through libghostty-vt's `DynamicPalette.changeDefault`, which
|
They are swapped through libghostty-vt's `DynamicPalette.changeDefault`, which
|
||||||
@@ -1106,20 +1155,20 @@ its `dark` property rather than the stored preference, and the preference only
|
|||||||
decides what the style manager is told. A desktop that switches at sunset takes
|
decides what the style manager is told. A desktop that switches at sunset takes
|
||||||
this app with it, with no extra machinery and no second source of truth.
|
this app with it, with no extra machinery and no second source of truth.
|
||||||
|
|
||||||
### Customising the palette
|
### Customizing the palette
|
||||||
|
|
||||||
**Settings → Appearance → Customise** opens the palette itself: a swatch for
|
**Settings → Appearance → Customize** opens the palette itself: a swatch for
|
||||||
every colour named above, in sections — surfaces, text, tab rows, accent,
|
every color named above, in sections — surfaces, text, tab rows, accent,
|
||||||
status, then the terminal's own background, foreground, cursor and selection,
|
status, then the terminal's own background, foreground, cursor and selection,
|
||||||
and the 16 ANSI colours as a strip. Beside each row is the hex it currently
|
and the 16 ANSI colors as a strip. Beside each row is the hex it currently
|
||||||
resolves to, so a palette can be read off as text rather than only picked at,
|
resolves to, so a palette can be read off as text rather than only picked at,
|
||||||
and a button that puts that one colour back. Each section heading has the same
|
and a button that puts that one color back. Each section heading has the same
|
||||||
button for the section, and the foot of the list has one for the whole scheme.
|
button for the section, and the foot of the list has one for the whole scheme.
|
||||||
|
|
||||||
Three things about how it behaves, all following from the same decision — that
|
Three things about how it behaves, all following from the same decision — that
|
||||||
the editor edits **the palette you can see**:
|
the editor edits **the palette you can see**:
|
||||||
|
|
||||||
- **A change applies as you make it.** Every colour in the list paints something
|
- **A change applies as you make it.** Every color in the list paints something
|
||||||
behind the dialog, so the swatch is its own preview; there is no OK button
|
behind the dialog, so the swatch is its own preview; there is no OK button
|
||||||
because there is nothing pending to confirm.
|
because there is nothing pending to confirm.
|
||||||
- **It edits the scheme in force.** Which is why the way to edit the other one is
|
- **It edits the scheme in force.** Which is why the way to edit the other one is
|
||||||
@@ -1128,9 +1177,9 @@ the editor edits **the palette you can see**:
|
|||||||
you the dark palette you built.
|
you the dark palette you built.
|
||||||
- **The terminal follows the window.** Left alone, the terminal's background *is*
|
- **The terminal follows the window.** Left alone, the terminal's background *is*
|
||||||
the pane surface, its foreground *is* the body text, and its selection *is* the
|
the pane surface, its foreground *is* the body text, and its selection *is* the
|
||||||
muted accent — so recolouring the app recolours the terminal drawn inside it,
|
muted accent — so recoloring the app recolors the terminal drawn inside it,
|
||||||
and the frame around a terminal keeps matching its contents. Setting one of
|
and the frame around a terminal keeps matching its contents. Setting one of
|
||||||
those explicitly breaks the link for that colour and only that colour.
|
those explicitly breaks the link for that color and only that color.
|
||||||
|
|
||||||
Only what has been changed is stored, per scheme, under `colors` in
|
Only what has been changed is stored, per scheme, under `colors` in
|
||||||
`settings.json`:
|
`settings.json`:
|
||||||
@@ -1150,12 +1199,12 @@ short and hand-editable, it is what lets a row know whether it has been changed,
|
|||||||
and it means the shipped defaults can be retuned in a later version and still
|
and it means the shipped defaults can be retuned in a later version and still
|
||||||
reach everyone who never touched them — rather than only the people who had
|
reach everyone who never touched them — rather than only the people who had
|
||||||
never opened the editor. A name this build doesn't know, or a value that isn't
|
never opened the editor. A name this build doesn't know, or a value that isn't
|
||||||
`#rrggbb`, costs that one colour and is logged; the rest of the file is read
|
`#rrggbb`, costs that one color and is logged; the rest of the file is read
|
||||||
normally.
|
normally.
|
||||||
|
|
||||||
`palette.zig` is where the names, the defaults and the grouping live, which is
|
`palette.zig` is where the names, the defaults and the grouping live, which is
|
||||||
why there is no longer a `palette-dark.css`. An editor needs to know what the
|
why there is no longer a `palette-dark.css`. An editor needs to know what the
|
||||||
colours are called, what they started as, and which ones belong together, and
|
colors are called, what they started as, and which ones belong together, and
|
||||||
none of that can be read back out of a stylesheet without parsing it — so the
|
none of that can be read back out of a stylesheet without parsing it — so the
|
||||||
table answers all of it and the CSS is generated from the table.
|
table answers all of it and the CSS is generated from the table.
|
||||||
|
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ pub fn build(b: *std.Build) void {
|
|||||||
paths_tests.root_module.addImport("glib", gobject.module("glib2"));
|
paths_tests.root_module.addImport("glib", gobject.module("glib2"));
|
||||||
test_step.dependOn(&b.addRunArtifact(paths_tests).step);
|
test_step.dependOn(&b.addRunArtifact(paths_tests).step);
|
||||||
|
|
||||||
// The palette is the third root: a table of colours, the rules for
|
// The palette is the third root: a table of colors, the rules for
|
||||||
// resolving one, and the CSS it is written out as. It reaches libghostty-vt
|
// resolving one, and the CSS it is written out as. It reaches libghostty-vt
|
||||||
// for the terminal's own default palette and nothing else, so it tests
|
// for the terminal's own default palette and nothing else, so it tests
|
||||||
// without a display in the same way the two above do.
|
// without a display in the same way the two above do.
|
||||||
@@ -225,6 +225,23 @@ pub fn build(b: *std.Build) void {
|
|||||||
});
|
});
|
||||||
test_step.dependOn(&b.addRunArtifact(store_tests).step);
|
test_step.dependOn(&b.addRunArtifact(store_tests).step);
|
||||||
|
|
||||||
|
// The mute rules are their own root. Whether a tab is silent at this instant,
|
||||||
|
// and what its row menu says about that, is arithmetic on a deadline — and
|
||||||
|
// it is the half of notifications that can be wrong without anyone noticing,
|
||||||
|
// since the symptom is a popup that didn't arrive. It reaches gio to hand a
|
||||||
|
// notification to the session, which is the part these tests leave alone.
|
||||||
|
const notify_tests = b.addTest(.{
|
||||||
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/notify.zig"),
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
.link_libc = true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
notify_tests.root_module.addImport("gio", gobject.module("gio2"));
|
||||||
|
notify_tests.root_module.addImport("glib", gobject.module("glib2"));
|
||||||
|
test_step.dependOn(&b.addRunArtifact(notify_tests).step);
|
||||||
|
|
||||||
const emoji_tests = b.addTest(.{
|
const emoji_tests = b.addTest(.{
|
||||||
.root_module = b.createModule(.{
|
.root_module = b.createModule(.{
|
||||||
.root_source_file = b.path("src/emoji.zig"),
|
.root_source_file = b.path("src/emoji.zig"),
|
||||||
|
|||||||
Vendored
+2
-2
@@ -4,12 +4,12 @@
|
|||||||
edge. Shapes are kept large and high-contrast so the sidebar is still
|
edge. Shapes are kept large and high-contrast so the sidebar is still
|
||||||
readable when the icon is scaled down to 32px in a launcher.
|
readable when the icon is scaled down to 32px in a launcher.
|
||||||
|
|
||||||
Colours track the app's dark palette — the navy surfaces of
|
Colors track the app's dark palette — the navy surfaces of
|
||||||
`palette-dark.css` and Signal's ultramarine as the accent. An icon is the
|
`palette-dark.css` and Signal's ultramarine as the accent. An icon is the
|
||||||
one part of the app that is looked at next to a row of other icons rather
|
one part of the app that is looked at next to a row of other icons rather
|
||||||
than on its own, so it stays on the dark scheme in both: a launcher shelf
|
than on its own, so it stays on the dark scheme in both: a launcher shelf
|
||||||
where one icon repaints itself with the desktop theme is a worse result than
|
where one icon repaints itself with the desktop theme is a worse result than
|
||||||
one that is simply recognisable.
|
one that is simply recognizable.
|
||||||
-->
|
-->
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" width="128" height="128">
|
||||||
<defs>
|
<defs>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
+1
-1
@@ -315,7 +315,7 @@ fn assemble(node: *Node) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The side `node` sits on within its parent split, and the parent's ratio.
|
/// The side `node` sits on within its parent split, and the parent's ratio.
|
||||||
/// Used to put a pane back where it started when a drag is cancelled.
|
/// Used to put a pane back where it started when a drag is canceled.
|
||||||
pub fn positionOf(node: *Node) ?struct { sibling: *Node, side: Side, ratio: f64 } {
|
pub fn positionOf(node: *Node) ?struct { sibling: *Node, side: Side, ratio: f64 } {
|
||||||
const parent = node.parent orelse return null;
|
const parent = node.parent orelse return null;
|
||||||
const s = parent.kind.split;
|
const s = parent.kind.split;
|
||||||
|
|||||||
+1
-1
@@ -303,7 +303,7 @@ fn parseNode(self: *Layouts, raw: std.json.Value) ParseError!*Node {
|
|||||||
|
|
||||||
/// A parameter's type, defaulting to a plain string.
|
/// A parameter's type, defaulting to a plain string.
|
||||||
///
|
///
|
||||||
/// An unrecognised type is a string rather than a parse error, which is the
|
/// An unrecognized type is a string rather than a parse error, which is the
|
||||||
/// opposite of how a leaf's `kind` is treated — and deliberately. A `kind` this
|
/// opposite of how a leaf's `kind` is treated — and deliberately. A `kind` this
|
||||||
/// build has never heard of is a pane it cannot build; a *type* it has never
|
/// build has never heard of is a pane it cannot build; a *type* it has never
|
||||||
/// heard of is only a prompt it cannot improve on, and asking for it in a text
|
/// heard of is only a prompt it cannot improve on, and asking for it in a text
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ pub fn present(
|
|||||||
for (layout.parameters, 0..) |param, i| {
|
for (layout.parameters, 0..) |param, i| {
|
||||||
const label = gtk.Label.new(null);
|
const label = gtk.Label.new(null);
|
||||||
// The description is what the author wrote for a human; the bare name
|
// The description is what the author wrote for a human; the bare name
|
||||||
// is the fallback so a parameter is never unlabelled.
|
// is the fallback so a parameter is never unlabeled.
|
||||||
var label_buf: [128]u8 = undefined;
|
var label_buf: [128]u8 = undefined;
|
||||||
const text = if (param.description.len > 0) param.description else param.name;
|
const text = if (param.description.len > 0) param.description else param.name;
|
||||||
label.setText(std.fmt.bufPrintZ(&label_buf, "{s}", .{text}) catch "parameter");
|
label.setText(std.fmt.bufPrintZ(&label_buf, "{s}", .{text}) catch "parameter");
|
||||||
|
|||||||
+54
-54
@@ -1,30 +1,30 @@
|
|||||||
//! The colour editor: one swatch per name in the palette, for the scheme that
|
//! The color editor: one swatch per name in the palette, for the scheme that
|
||||||
//! is on screen.
|
//! is on screen.
|
||||||
//!
|
//!
|
||||||
//! It edits **the palette you can see**, and that is the whole design. Every
|
//! It edits **the palette you can see**, and that is the whole design. Every
|
||||||
//! colour here paints something behind this window, so a change is its own
|
//! color here paints something behind this window, so a change is its own
|
||||||
//! preview — pick a sidebar colour and the sidebar is that colour before the
|
//! preview — pick a sidebar color and the sidebar is that color before the
|
||||||
//! dialog has closed. Editing the *other* scheme's palette from here would mean
|
//! dialog has closed. Editing the *other* scheme's palette from here would mean
|
||||||
//! a page full of swatches whose effect you have to take on faith, so the
|
//! a page full of swatches whose effect you have to take on faith, so the
|
||||||
//! answer to "I want to change the light one" is the theme picker above: switch
|
//! answer to "I want to change the light one" is the theme picker above: switch
|
||||||
//! to light, and this section switches with it.
|
//! to light, and this section switches with it.
|
||||||
//!
|
//!
|
||||||
//! Overrides are stored per colour rather than as a whole palette, which is what
|
//! Overrides are stored per color rather than as a whole palette, which is what
|
||||||
//! lets a row say whether it has been changed and put itself back, and what lets
|
//! lets a row say whether it has been changed and put itself back, and what lets
|
||||||
//! the defaults be retuned in a later version for everyone who never touched
|
//! the defaults be retuned in a later version for everyone who never touched
|
||||||
//! them. The terminal's colours go one step further: left alone they *follow*
|
//! them. The terminal's colors go one step further: left alone they *follow*
|
||||||
//! the surface and text colours above them (see `palette.Key.inherits`), so
|
//! the surface and text colors above them (see `palette.Key.inherits`), so
|
||||||
//! recolouring the app recolours the terminal drawn on it without asking anyone
|
//! recoloring the app recolors the terminal drawn on it without asking anyone
|
||||||
//! to set the same colour twice — which is also why every row is re-read after
|
//! to set the same color twice — which is also why every row is re-read after
|
||||||
//! any change, not just the row that changed.
|
//! any change, not just the row that changed.
|
||||||
//!
|
//!
|
||||||
//! The swatches start collapsed, and the two controls above them do not. That
|
//! The swatches start collapsed, and the two controls above them do not. That
|
||||||
//! split is the point of the base colour: thirty-nine swatches is a fair way to
|
//! split is the point of the base color: thirty-nine swatches is a fair way to
|
||||||
//! *correct* a theme and a miserable way to *choose* one, so the path that is
|
//! *correct* a theme and a miserable way to *choose* one, so the path that is
|
||||||
//! always on offer is "pick a colour, get a palette" (`tint.zig`), and the
|
//! always on offer is "pick a color, get a palette" (`tint.zig`), and the
|
||||||
//! wall of swatches is behind a disclosure for the times that isn't enough. The
|
//! wall of swatches is behind a disclosure for the times that isn't enough. The
|
||||||
//! two compose — a swatch that has been set by hand outranks the generated
|
//! two compose — a swatch that has been set by hand outranks the generated
|
||||||
//! colour underneath it — which is why picking a new base does not clear the
|
//! color underneath it — which is why picking a new base does not clear the
|
||||||
//! corrections someone made to the last one.
|
//! corrections someone made to the last one.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
@@ -49,7 +49,7 @@ pub const Options = struct {
|
|||||||
ctx: ?*anyopaque = null,
|
ctx: ?*anyopaque = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// One colour: its swatch, and — for the ones listed as rows rather than packed
|
/// One color: its swatch, and — for the ones listed as rows rather than packed
|
||||||
/// into the ANSI grid — the hex it currently resolves to and the button that
|
/// into the ANSI grid — the hex it currently resolves to and the button that
|
||||||
/// puts it back.
|
/// puts it back.
|
||||||
const Row = struct {
|
const Row = struct {
|
||||||
@@ -60,7 +60,7 @@ const Row = struct {
|
|||||||
revert: ?*gtk.Button = null,
|
revert: ?*gtk.Button = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// One section heading, kept only so its reset button can be greyed out when
|
/// One section heading, kept only so its reset button can be grayed out when
|
||||||
/// the section has nothing to reset.
|
/// the section has nothing to reset.
|
||||||
const GroupHeader = struct {
|
const GroupHeader = struct {
|
||||||
editor: *PaletteEditor,
|
editor: *PaletteEditor,
|
||||||
@@ -81,14 +81,14 @@ body: *gtk.Box,
|
|||||||
/// Says which scheme is being edited, and is rewritten when that changes.
|
/// Says which scheme is being edited, and is rewritten when that changes.
|
||||||
scheme_hint: *gtk.Label,
|
scheme_hint: *gtk.Label,
|
||||||
|
|
||||||
/// The colour the rest of the palette is generated from, the hex it currently
|
/// The color the rest of the palette is generated from, the hex it currently
|
||||||
/// stands at, and the button that drops it. Assigned in `build`, since the
|
/// stands at, and the button that drops it. Assigned in `build`, since the
|
||||||
/// swatch has to be handed its dialog at construction.
|
/// swatch has to be handed its dialog at construction.
|
||||||
base: *gtk.ColorDialogButton,
|
base: *gtk.ColorDialogButton,
|
||||||
base_hex: *gtk.Label,
|
base_hex: *gtk.Label,
|
||||||
base_clear: *gtk.Button,
|
base_clear: *gtk.Button,
|
||||||
|
|
||||||
/// How far apart to spread the colours built from the base. Insensitive until
|
/// How far apart to spread the colors built from the base. Insensitive until
|
||||||
/// there is a base, since on its own it has nothing to spread.
|
/// there is a base, since on its own it has nothing to spread.
|
||||||
contrast: *gtk.Scale,
|
contrast: *gtk.Scale,
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ contrast: *gtk.Scale,
|
|||||||
reset: *gtk.Button,
|
reset: *gtk.Button,
|
||||||
|
|
||||||
/// Indexed by `@intFromEnum(key)`, so a row is found without searching. Inline
|
/// Indexed by `@intFromEnum(key)`, so a row is found without searching. Inline
|
||||||
/// in the struct rather than allocated: there is exactly one row per colour, the
|
/// in the struct rather than allocated: there is exactly one row per color, the
|
||||||
/// count is known at compile time, and the addresses have to be stable because
|
/// count is known at compile time, and the addresses have to be stable because
|
||||||
/// every swatch's callback holds one.
|
/// every swatch's callback holds one.
|
||||||
rows: [palette.count]Row,
|
rows: [palette.count]Row,
|
||||||
@@ -124,7 +124,7 @@ pub fn create(alloc: std.mem.Allocator, opts: Options) !*PaletteEditor {
|
|||||||
// The step is what the arrow keys move by; the range is the one
|
// The step is what the arrow keys move by; the range is the one
|
||||||
// `palette.Tint.contrast` is defined over.
|
// `palette.Tint.contrast` is defined over.
|
||||||
.contrast = gtk.Scale.newWithRange(.horizontal, -1, 1, 0.05),
|
.contrast = gtk.Scale.newWithRange(.horizontal, -1, 1, 0.05),
|
||||||
.reset = gtk.Button.newWithLabel("Reset every colour"),
|
.reset = gtk.Button.newWithLabel("Reset every color"),
|
||||||
.rows = undefined,
|
.rows = undefined,
|
||||||
.groups = undefined,
|
.groups = undefined,
|
||||||
.on_report = opts.on_report,
|
.on_report = opts.on_report,
|
||||||
@@ -160,8 +160,8 @@ pub fn refresh(self: *PaletteEditor) void {
|
|||||||
fn build(self: *PaletteEditor) void {
|
fn build(self: *PaletteEditor) void {
|
||||||
self.root.append(self.buildHeader());
|
self.root.append(self.buildHeader());
|
||||||
|
|
||||||
// Above the base colour rather than inside the disclosure with the
|
// Above the base color rather than inside the disclosure with the
|
||||||
// swatches, because it is true of the base colour too: both palettes can be
|
// swatches, because it is true of the base color too: both palettes can be
|
||||||
// given one, and this is the only thing on the page that says which of them
|
// given one, and this is the only thing on the page that says which of them
|
||||||
// the control below is about to change.
|
// the control below is about to change.
|
||||||
self.scheme_hint.setXalign(0);
|
self.scheme_hint.setXalign(0);
|
||||||
@@ -176,8 +176,8 @@ fn build(self: *PaletteEditor) void {
|
|||||||
self.reset.as(gtk.Widget).addCssClass("flat");
|
self.reset.as(gtk.Widget).addCssClass("flat");
|
||||||
self.reset.as(gtk.Widget).setHalign(.start);
|
self.reset.as(gtk.Widget).setHalign(.start);
|
||||||
self.reset.as(gtk.Widget).setTooltipText(
|
self.reset.as(gtk.Widget).setTooltipText(
|
||||||
"Put every colour in this scheme back to the one Playpen ships with, " ++
|
"Put every color in this scheme back to the one Playpen ships with, " ++
|
||||||
"and drop the base colour above",
|
"and drop the base color above",
|
||||||
);
|
);
|
||||||
_ = gtk.Button.signals.clicked.connect(self.reset, *PaletteEditor, &onReset, self, .{});
|
_ = gtk.Button.signals.clicked.connect(self.reset, *PaletteEditor, &onReset, self, .{});
|
||||||
self.body.append(self.reset.as(gtk.Widget));
|
self.body.append(self.reset.as(gtk.Widget));
|
||||||
@@ -201,7 +201,7 @@ fn buildHeader(self: *PaletteEditor) *gtk.Widget {
|
|||||||
name.as(gtk.Widget).addCssClass("playpen-dialog-label");
|
name.as(gtk.Widget).addCssClass("playpen-dialog-label");
|
||||||
labels.append(name.as(gtk.Widget));
|
labels.append(name.as(gtk.Widget));
|
||||||
|
|
||||||
const sub = gtk.Label.new("Every colour the window and the terminal are painted from.");
|
const sub = gtk.Label.new("Every color the window and the terminal are painted from.");
|
||||||
sub.setXalign(0);
|
sub.setXalign(0);
|
||||||
sub.setWrap(1);
|
sub.setWrap(1);
|
||||||
sub.as(gtk.Widget).addCssClass("playpen-dialog-sublabel");
|
sub.as(gtk.Widget).addCssClass("playpen-dialog-sublabel");
|
||||||
@@ -217,7 +217,7 @@ fn buildHeader(self: *PaletteEditor) *gtk.Widget {
|
|||||||
wrap.as(gtk.Widget).addCssClass("playpen-settings-choice");
|
wrap.as(gtk.Widget).addCssClass("playpen-settings-choice");
|
||||||
wrap.as(gtk.Widget).setValign(.center);
|
wrap.as(gtk.Widget).setValign(.center);
|
||||||
|
|
||||||
const toggle = gtk.ToggleButton.newWithLabel("Customise");
|
const toggle = gtk.ToggleButton.newWithLabel("Customize");
|
||||||
_ = gtk.ToggleButton.signals.toggled.connect(
|
_ = gtk.ToggleButton.signals.toggled.connect(
|
||||||
toggle,
|
toggle,
|
||||||
*PaletteEditor,
|
*PaletteEditor,
|
||||||
@@ -232,7 +232,7 @@ fn buildHeader(self: *PaletteEditor) *gtk.Widget {
|
|||||||
return row.as(gtk.Widget);
|
return row.as(gtk.Widget);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The two controls that are always on offer: the colour the palette is built
|
/// The two controls that are always on offer: the color the palette is built
|
||||||
/// out of, and how far apart to spread what gets built.
|
/// out of, and how far apart to spread what gets built.
|
||||||
///
|
///
|
||||||
/// Outside the disclosure below on purpose. This is the answer for someone who
|
/// Outside the disclosure below on purpose. This is the answer for someone who
|
||||||
@@ -244,16 +244,16 @@ fn buildBase(self: *PaletteEditor) *gtk.Widget {
|
|||||||
const group = gtk.Box.new(.vertical, 6);
|
const group = gtk.Box.new(.vertical, 6);
|
||||||
|
|
||||||
// The same class the swatch sections below carry, which is where the
|
// The same class the swatch sections below carry, which is where the
|
||||||
// stylesheet hangs the "a colour button is a rectangle of the colour, not a
|
// stylesheet hangs the "a color button is a rectangle of the color, not a
|
||||||
// button holding one" rule. This row has a colour button in it and wants to
|
// button holding one" rule. This row has a color button in it and wants to
|
||||||
// look like the ones under Customise.
|
// look like the ones under Customize.
|
||||||
group.as(gtk.Widget).addCssClass("playpen-color-group");
|
group.as(gtk.Widget).addCssClass("playpen-color-group");
|
||||||
|
|
||||||
// ---- the colour ----
|
// ---- the color ----
|
||||||
const row = gtk.Box.new(.horizontal, 8);
|
const row = gtk.Box.new(.horizontal, 8);
|
||||||
row.as(gtk.Widget).addCssClass("playpen-color-row");
|
row.as(gtk.Widget).addCssClass("playpen-color-row");
|
||||||
|
|
||||||
const label = gtk.Label.new("Base colour");
|
const label = gtk.Label.new("Base color");
|
||||||
label.setXalign(0);
|
label.setXalign(0);
|
||||||
label.as(gtk.Widget).setHexpand(1);
|
label.as(gtk.Widget).setHexpand(1);
|
||||||
label.as(gtk.Widget).addCssClass("playpen-dialog-label");
|
label.as(gtk.Widget).addCssClass("playpen-dialog-label");
|
||||||
@@ -265,7 +265,7 @@ fn buildBase(self: *PaletteEditor) *gtk.Widget {
|
|||||||
const dialog = gtk.ColorDialog.new();
|
const dialog = gtk.ColorDialog.new();
|
||||||
dialog.setWithAlpha(0);
|
dialog.setWithAlpha(0);
|
||||||
dialog.setModal(1);
|
dialog.setModal(1);
|
||||||
dialog.setTitle("Base colour");
|
dialog.setTitle("Base color");
|
||||||
|
|
||||||
self.base = gtk.ColorDialogButton.new(dialog);
|
self.base = gtk.ColorDialogButton.new(dialog);
|
||||||
self.base.as(gtk.Widget).setValign(.center);
|
self.base.as(gtk.Widget).setValign(.center);
|
||||||
@@ -280,7 +280,7 @@ fn buildBase(self: *PaletteEditor) *gtk.Widget {
|
|||||||
row.append(self.base.as(gtk.Widget));
|
row.append(self.base.as(gtk.Widget));
|
||||||
|
|
||||||
self.base_clear.as(gtk.Widget).addCssClass("flat");
|
self.base_clear.as(gtk.Widget).addCssClass("flat");
|
||||||
self.base_clear.as(gtk.Widget).setTooltipText("Back to Playpen's own colours");
|
self.base_clear.as(gtk.Widget).setTooltipText("Back to Playpen's own colors");
|
||||||
_ = gtk.Button.signals.clicked.connect(
|
_ = gtk.Button.signals.clicked.connect(
|
||||||
self.base_clear,
|
self.base_clear,
|
||||||
*PaletteEditor,
|
*PaletteEditor,
|
||||||
@@ -293,15 +293,15 @@ fn buildBase(self: *PaletteEditor) *gtk.Widget {
|
|||||||
group.append(row.as(gtk.Widget));
|
group.append(row.as(gtk.Widget));
|
||||||
group.append(hint(
|
group.append(hint(
|
||||||
"Pick one and the rest follows: a very dark version of it behind the window, " ++
|
"Pick one and the rest follows: a very dark version of it behind the window, " ++
|
||||||
"lighter ones for the panes and the text on them, and the colour itself as the accent. " ++
|
"lighter ones for the panes and the text on them, and the color itself as the accent. " ++
|
||||||
"Anything you set under Customise stays where you put it.",
|
"Anything you set under Customize stays where you put it.",
|
||||||
));
|
));
|
||||||
|
|
||||||
// ---- how far apart ----
|
// ---- how far apart ----
|
||||||
const contrast_row = gtk.Box.new(.horizontal, 8);
|
const contrast_row = gtk.Box.new(.horizontal, 8);
|
||||||
contrast_row.as(gtk.Widget).addCssClass("playpen-color-row");
|
contrast_row.as(gtk.Widget).addCssClass("playpen-color-row");
|
||||||
contrast_row.as(gtk.Widget).setTooltipText(
|
contrast_row.as(gtk.Widget).setTooltipText(
|
||||||
"How far apart to spread the colours built from the base.\n" ++
|
"How far apart to spread the colors built from the base.\n" ++
|
||||||
"Starker pulls the backdrop darker and the text brighter; softer draws them together.",
|
"Starker pulls the backdrop darker and the text brighter; softer draws them together.",
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -324,7 +324,7 @@ fn buildBase(self: *PaletteEditor) *gtk.Widget {
|
|||||||
self.contrast.as(gtk.Range).setIncrements(0.05, 0.25);
|
self.contrast.as(gtk.Range).setIncrements(0.05, 0.25);
|
||||||
self.contrast.as(gtk.Range).setRoundDigits(2);
|
self.contrast.as(gtk.Range).setRoundDigits(2);
|
||||||
|
|
||||||
// The unlabelled middle mark is what makes the shipped spacing findable
|
// The unlabeled middle mark is what makes the shipped spacing findable
|
||||||
// again after a drag: GTK snaps the handle to a mark it passes near.
|
// again after a drag: GTK snaps the handle to a mark it passes near.
|
||||||
self.contrast.addMark(-1, .bottom, "Soft");
|
self.contrast.addMark(-1, .bottom, "Soft");
|
||||||
self.contrast.addMark(0, .bottom, null);
|
self.contrast.addMark(0, .bottom, null);
|
||||||
@@ -350,8 +350,8 @@ fn buildGroups(self: *PaletteEditor) void {
|
|||||||
var current: ?palette.Group = null;
|
var current: ?palette.Group = null;
|
||||||
var section: *gtk.Box = undefined;
|
var section: *gtk.Box = undefined;
|
||||||
|
|
||||||
// The ANSI colours are a grid of bare swatches rather than sixteen labelled
|
// The ANSI colors are a grid of bare swatches rather than sixteen labeled
|
||||||
// rows: they are a palette people recognise by position — eight normal, then
|
// rows: they are a palette people recognize by position — eight normal, then
|
||||||
// eight bright — and a list of their names says less than the strip does.
|
// eight bright — and a list of their names says less than the strip does.
|
||||||
var ansi: *gtk.Grid = undefined;
|
var ansi: *gtk.Grid = undefined;
|
||||||
|
|
||||||
@@ -396,7 +396,7 @@ fn buildGroupHeader(self: *PaletteEditor, group: palette.Group) *gtk.Widget {
|
|||||||
|
|
||||||
const reset = gtk.Button.newFromIconName("edit-undo-symbolic");
|
const reset = gtk.Button.newFromIconName("edit-undo-symbolic");
|
||||||
reset.as(gtk.Widget).addCssClass("flat");
|
reset.as(gtk.Widget).addCssClass("flat");
|
||||||
reset.as(gtk.Widget).setTooltipText("Reset the colours in this section");
|
reset.as(gtk.Widget).setTooltipText("Reset the colors in this section");
|
||||||
|
|
||||||
const header = &self.groups[@intFromEnum(group)];
|
const header = &self.groups[@intFromEnum(group)];
|
||||||
header.* = .{ .editor = self, .group = group, .reset = reset };
|
header.* = .{ .editor = self, .group = group, .reset = reset };
|
||||||
@@ -407,7 +407,7 @@ fn buildGroupHeader(self: *PaletteEditor, group: palette.Group) *gtk.Widget {
|
|||||||
return row.as(gtk.Widget);
|
return row.as(gtk.Widget);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One colour as a row: what it is, what it currently is, and the two controls
|
/// One color as a row: what it is, what it currently is, and the two controls
|
||||||
/// for changing that.
|
/// for changing that.
|
||||||
fn buildRow(self: *PaletteEditor, key: palette.Key) *gtk.Widget {
|
fn buildRow(self: *PaletteEditor, key: palette.Key) *gtk.Widget {
|
||||||
const row = gtk.Box.new(.horizontal, 8);
|
const row = gtk.Box.new(.horizontal, 8);
|
||||||
@@ -419,7 +419,7 @@ fn buildRow(self: *PaletteEditor, key: palette.Key) *gtk.Widget {
|
|||||||
label.as(gtk.Widget).addCssClass("playpen-dialog-label");
|
label.as(gtk.Widget).addCssClass("playpen-dialog-label");
|
||||||
row.append(label.as(gtk.Widget));
|
row.append(label.as(gtk.Widget));
|
||||||
|
|
||||||
// The tooltip carries what the colour is *for*, plus the name it goes by in
|
// The tooltip carries what the color is *for*, plus the name it goes by in
|
||||||
// the settings file, since anyone hand-editing that file is looking at this
|
// the settings file, since anyone hand-editing that file is looking at this
|
||||||
// list to find out what to call things.
|
// list to find out what to call things.
|
||||||
var tip: [256]u8 = undefined;
|
var tip: [256]u8 = undefined;
|
||||||
@@ -454,8 +454,8 @@ fn buildRow(self: *PaletteEditor, key: palette.Key) *gtk.Widget {
|
|||||||
/// A swatch, and the row record that goes with it.
|
/// A swatch, and the row record that goes with it.
|
||||||
///
|
///
|
||||||
/// Alpha is off: `style.css` derives every wash it needs from `alpha()` on the
|
/// Alpha is off: `style.css` derives every wash it needs from `alpha()` on the
|
||||||
/// colour it belongs to, so a translucent palette entry would be a second way
|
/// color it belongs to, so a translucent palette entry would be a second way
|
||||||
/// of saying the same thing, and one the terminal renderer could not honour.
|
/// of saying the same thing, and one the terminal renderer could not honor.
|
||||||
fn newSwatch(
|
fn newSwatch(
|
||||||
self: *PaletteEditor,
|
self: *PaletteEditor,
|
||||||
key: palette.Key,
|
key: palette.Key,
|
||||||
@@ -507,7 +507,7 @@ fn reload(self: *PaletteEditor) void {
|
|||||||
|
|
||||||
// With no base of its own, the swatch shows the accent this scheme is
|
// With no base of its own, the swatch shows the accent this scheme is
|
||||||
// already painted with — so the first click on it starts from the theme in
|
// already painted with — so the first click on it starts from the theme in
|
||||||
// front of you rather than from whatever colour a picker opens on.
|
// front of you rather than from whatever color a picker opens on.
|
||||||
const base = if (changes.tint) |tint|
|
const base = if (changes.tint) |tint|
|
||||||
tint.base
|
tint.base
|
||||||
else
|
else
|
||||||
@@ -524,9 +524,9 @@ fn reload(self: *PaletteEditor) void {
|
|||||||
self.contrast.as(gtk.Widget).setSensitive(@intFromBool(changes.tint != null));
|
self.contrast.as(gtk.Widget).setSensitive(@intFromBool(changes.tint != null));
|
||||||
|
|
||||||
for (&self.rows) |*row| {
|
for (&self.rows) |*row| {
|
||||||
// The colour, not the override: a terminal colour that is following the
|
// The color, not the override: a terminal color that is following the
|
||||||
// surface above it, or any colour at all under a base, has to show the
|
// surface above it, or any color at all under a base, has to show the
|
||||||
// colour it is actually painted in.
|
// color it is actually painted in.
|
||||||
const color = palette.resolve(row.key, scheme, changes);
|
const color = palette.resolve(row.key, scheme, changes);
|
||||||
|
|
||||||
var rgba = toRgba(color);
|
var rgba = toRgba(color);
|
||||||
@@ -538,7 +538,7 @@ fn reload(self: *PaletteEditor) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Whether there is anything to revert *to*, which is a different
|
// Whether there is anything to revert *to*, which is a different
|
||||||
// question: neither an inherited colour nor a generated one is a colour
|
// question: neither an inherited color nor a generated one is a color
|
||||||
// that has been set.
|
// that has been set.
|
||||||
if (row.revert) |button| {
|
if (row.revert) |button| {
|
||||||
button.as(gtk.Widget).setSensitive(@intFromBool(changes.overrides.get(row.key) != null));
|
button.as(gtk.Widget).setSensitive(@intFromBool(changes.overrides.get(row.key) != null));
|
||||||
@@ -565,7 +565,7 @@ fn groupIsSet(group: palette.Group, overrides: *const palette.Overrides) bool {
|
|||||||
///
|
///
|
||||||
/// In that order on purpose: repainting is what the user asked for and cannot
|
/// In that order on purpose: repainting is what the user asked for and cannot
|
||||||
/// fail, while the write can — and a settings file that couldn't be written is
|
/// fail, while the write can — and a settings file that couldn't be written is
|
||||||
/// worth saying so about without also refusing the colour for this session.
|
/// worth saying so about without also refusing the color for this session.
|
||||||
fn applied(self: *PaletteEditor) void {
|
fn applied(self: *PaletteEditor) void {
|
||||||
appearance.refresh();
|
appearance.refresh();
|
||||||
self.reload();
|
self.reload();
|
||||||
@@ -580,7 +580,7 @@ fn applied(self: *PaletteEditor) void {
|
|||||||
/// Show the change and leave the file for later.
|
/// Show the change and leave the file for later.
|
||||||
///
|
///
|
||||||
/// For the contrast slider alone. Every other control here settles on a value
|
/// For the contrast slider alone. Every other control here settles on a value
|
||||||
/// once — a colour dialog reports when it is dismissed, a button when it is
|
/// once — a color dialog reports when it is dismissed, a button when it is
|
||||||
/// clicked — but a slider reports on every pixel of a drag, and writing the
|
/// clicked — but a slider reports on every pixel of a drag, and writing the
|
||||||
/// settings file at that rate would be an fsync per frame of an animation. The
|
/// settings file at that rate would be an fsync per frame of an animation. The
|
||||||
/// settings page writes on close, so nothing is lost by waiting; see the note at
|
/// settings page writes on close, so nothing is lost by waiting; see the note at
|
||||||
@@ -608,7 +608,7 @@ fn onColorPicked(
|
|||||||
// The scheme is read here rather than remembered from the last reload: a
|
// The scheme is read here rather than remembered from the last reload: a
|
||||||
// desktop that flips to light under a `system` preference while this page is
|
// desktop that flips to light under a `system` preference while this page is
|
||||||
// open changes which palette an edit belongs to, and writing to the one the
|
// open changes which palette an edit belongs to, and writing to the one the
|
||||||
// rows were last filled from would put the colour in the palette that isn't
|
// rows were last filled from would put the color in the palette that isn't
|
||||||
// on screen.
|
// on screen.
|
||||||
const scheme = theme.currentScheme();
|
const scheme = theme.currentScheme();
|
||||||
Settings.get().colors.of(scheme).overrides.set(row.key, fromRgba(swatch.getRgba()));
|
Settings.get().colors.of(scheme).overrides.set(row.key, fromRgba(swatch.getRgba()));
|
||||||
@@ -660,9 +660,9 @@ fn onBasePicked(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn onBaseCleared(_: *gtk.Button, self: *PaletteEditor) callconv(.c) void {
|
fn onBaseCleared(_: *gtk.Button, self: *PaletteEditor) callconv(.c) void {
|
||||||
// Only the generated palette goes. Colours set by hand were set against
|
// Only the generated palette goes. Colors set by hand were set against
|
||||||
// what was underneath them and are still what their owner asked for, so
|
// what was underneath them and are still what their owner asked for, so
|
||||||
// dropping those too is `onReset`'s job and is labelled as such.
|
// dropping those too is `onReset`'s job and is labeled as such.
|
||||||
Settings.get().colors.of(theme.currentScheme()).tint = null;
|
Settings.get().colors.of(theme.currentScheme()).tint = null;
|
||||||
self.applied();
|
self.applied();
|
||||||
}
|
}
|
||||||
@@ -702,10 +702,10 @@ fn toRgba(color: palette.Rgb) gdk.RGBA {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// GTK works in floats and the palette works in bytes, so a colour that came
|
/// GTK works in floats and the palette works in bytes, so a color that came
|
||||||
/// out of the picker's own HSV wheel or its eyedropper has to be rounded to
|
/// out of the picker's own HSV wheel or its eyedropper has to be rounded to
|
||||||
/// something a hex triplet can hold. Rounding rather than truncating, so that a
|
/// something a hex triplet can hold. Rounding rather than truncating, so that a
|
||||||
/// colour typed in as hex comes back as the same hex.
|
/// color typed in as hex comes back as the same hex.
|
||||||
fn fromRgba(rgba: *const gdk.RGBA) palette.Rgb {
|
fn fromRgba(rgba: *const gdk.RGBA) palette.Rgb {
|
||||||
return .{
|
return .{
|
||||||
.r = channel(rgba.f_red),
|
.r = channel(rgba.f_red),
|
||||||
|
|||||||
+10
-10
@@ -136,7 +136,7 @@ const all_sides = [_]Side{ .left, .right, .top, .bottom };
|
|||||||
// Status dots
|
// Status dots
|
||||||
//
|
//
|
||||||
// Both the pane header and the sidebar row show the same small filled circle,
|
// Both the pane header and the sidebar row show the same small filled circle,
|
||||||
// recoloured by CSS. Symbolic icons take their colour from the `color`
|
// recolored by CSS. Symbolic icons take their color from the `color`
|
||||||
// property, so the state is carried entirely by which class is applied and
|
// property, so the state is carried entirely by which class is applied and
|
||||||
// there is no second icon to keep in sync.
|
// there is no second icon to keep in sync.
|
||||||
|
|
||||||
@@ -159,8 +159,8 @@ pub const status_classes = [_][:0]const u8{
|
|||||||
|
|
||||||
/// Point a dot at one of `status_classes`, or pass null to hide it.
|
/// Point a dot at one of `status_classes`, or pass null to hide it.
|
||||||
///
|
///
|
||||||
/// Hiding rather than clearing the colour matters: an idle pane should show
|
/// Hiding rather than clearing the color matters: an idle pane should show
|
||||||
/// nothing at all, not a dot in the background colour that still takes up
|
/// nothing at all, not a dot in the background color that still takes up
|
||||||
/// space and still catches the eye when the row is hovered.
|
/// space and still catches the eye when the row is hovered.
|
||||||
pub fn setDot(dot: *gtk.Image, class: ?[:0]const u8) void {
|
pub fn setDot(dot: *gtk.Image, class: ?[:0]const u8) void {
|
||||||
const w = dot.as(gtk.Widget);
|
const w = dot.as(gtk.Widget);
|
||||||
@@ -174,7 +174,7 @@ pub fn setDot(dot: *gtk.Image, class: ?[:0]const u8) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a dot is signalling: the four states content can report, plus one it
|
/// What a dot is signaling: the four states content can report, plus one it
|
||||||
/// cannot know about on its own.
|
/// cannot know about on its own.
|
||||||
///
|
///
|
||||||
/// `done` is the whole reason this is a separate type from `Status`. Content
|
/// `done` is the whole reason this is a separate type from `Status`. Content
|
||||||
@@ -212,7 +212,7 @@ pub const Attention = enum {
|
|||||||
/// carrying it — a sidebar row, or a pane's frame.
|
/// carrying it — a sidebar row, or a pane's frame.
|
||||||
///
|
///
|
||||||
/// Returned as a pair so the two can never drift into disagreeing about
|
/// Returned as a pair so the two can never drift into disagreeing about
|
||||||
/// what colour a state is. Null means idle, which paints nothing at all.
|
/// what color a state is. Null means idle, which paints nothing at all.
|
||||||
pub fn classes(self: Attention) ?struct {
|
pub fn classes(self: Attention) ?struct {
|
||||||
dot: [:0]const u8,
|
dot: [:0]const u8,
|
||||||
surface: [:0]const u8,
|
surface: [:0]const u8,
|
||||||
@@ -255,8 +255,8 @@ pub const attention_classes = [_][:0]const u8{
|
|||||||
"playpen-attn-failed",
|
"playpen-attn-failed",
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Bring a dot in line with what it should be signalling. The tooltip comes
|
/// Bring a dot in line with what it should be signaling. The tooltip comes
|
||||||
/// along with the colour, since a colour on its own doesn't say what it means.
|
/// along with the color, since a color on its own doesn't say what it means.
|
||||||
pub fn applyDot(dot: *gtk.Image, state: Attention) void {
|
pub fn applyDot(dot: *gtk.Image, state: Attention) void {
|
||||||
setDot(dot, state.class());
|
setDot(dot, state.class());
|
||||||
dot.as(gtk.Widget).setTooltipText(state.tooltip());
|
dot.as(gtk.Widget).setTooltipText(state.tooltip());
|
||||||
@@ -267,7 +267,7 @@ pub fn applyDot(dot: *gtk.Image, state: Attention) void {
|
|||||||
/// A dot alone is a few pixels, which is fine once you know where to look and
|
/// A dot alone is a few pixels, which is fine once you know where to look and
|
||||||
/// no use at all for the thing this is for: sweeping a sidebar and picking out
|
/// no use at all for the thing this is for: sweeping a sidebar and picking out
|
||||||
/// the row that wants you. The surface class is what lets the row and the pane
|
/// the row that wants you. The surface class is what lets the row and the pane
|
||||||
/// frame carry the same colour at a size you can catch out of the corner of
|
/// frame carry the same color at a size you can catch out of the corner of
|
||||||
/// your eye.
|
/// your eye.
|
||||||
pub fn applyAttention(surface: *gtk.Widget, dot: *gtk.Image, state: Attention) void {
|
pub fn applyAttention(surface: *gtk.Widget, dot: *gtk.Image, state: Attention) void {
|
||||||
applyDot(dot, state);
|
applyDot(dot, state);
|
||||||
@@ -435,7 +435,7 @@ pub fn titleSlice(self: *const Pane) [:0]const u8 {
|
|||||||
return std.mem.sliceTo(&self.title, 0);
|
return std.mem.sliceTo(&self.title, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What this pane's dot is signalling right now.
|
/// What this pane's dot is signaling right now.
|
||||||
pub fn attention(self: *const Pane) Attention {
|
pub fn attention(self: *const Pane) Attention {
|
||||||
return .of(self.status, self.done_unanswered);
|
return .of(self.status, self.done_unanswered);
|
||||||
}
|
}
|
||||||
@@ -608,7 +608,7 @@ fn installDropTarget(self: *Pane) void {
|
|||||||
/// Work out where a drop at this point would put the dragged pane.
|
/// Work out where a drop at this point would put the dragged pane.
|
||||||
///
|
///
|
||||||
/// The side is whichever edge of this pane the pointer is nearest, so every
|
/// The side is whichever edge of this pane the pointer is nearest, so every
|
||||||
/// point maps to some edge and there is no dead centre. If that point is also
|
/// point maps to some edge and there is no dead center. If that point is also
|
||||||
/// close to the matching border of the view itself, the drop is promoted to
|
/// close to the matching border of the view itself, the drop is promoted to
|
||||||
/// span the whole layout instead of splitting just this pane — dropping along
|
/// span the whole layout instead of splitting just this pane — dropping along
|
||||||
/// the bottom of the window gives a pane stretching across the bottom, while
|
/// the bottom of the window gives a pane stretching across the bottom, while
|
||||||
|
|||||||
+2
-2
@@ -12,7 +12,7 @@
|
|||||||
//! the matches agree, the arrow keys pick out of the list, and `Enter` takes
|
//! the matches agree, the arrow keys pick out of the list, and `Enter` takes
|
||||||
//! the highlighted one or, with nothing highlighted, means what it means
|
//! the highlighted one or, with nothing highlighted, means what it means
|
||||||
//! everywhere else in the dialog. `GtkEntryCompletion` would have been the
|
//! everywhere else in the dialog. `GtkEntryCompletion` would have been the
|
||||||
//! stock answer and is deprecated as of GTK 4.10, and its inline behaviour was
|
//! stock answer and is deprecated as of GTK 4.10, and its inline behavior was
|
||||||
//! never this: it completes to the *first* match rather than to the longest
|
//! never this: it completes to the *first* match rather than to the longest
|
||||||
//! common prefix, which in a directory of siblings guesses wrong more often
|
//! common prefix, which in a directory of siblings guesses wrong more often
|
||||||
//! than it helps.
|
//! than it helps.
|
||||||
@@ -455,7 +455,7 @@ fn onFolderChosen(
|
|||||||
var err: ?*glib.Error = null;
|
var err: ?*glib.Error = null;
|
||||||
const file = dialog.selectFolderFinish(result, &err) orelse {
|
const file = dialog.selectFolderFinish(result, &err) orelse {
|
||||||
// Dismissing the chooser is how most trips through it end, and
|
// Dismissing the chooser is how most trips through it end, and
|
||||||
// cancelling it is how a field that has gone away ends one, so neither
|
// canceling it is how a field that has gone away ends one, so neither
|
||||||
// is worth a word.
|
// is worth a word.
|
||||||
if (err) |e| e.free();
|
if (err) |e| e.free();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
//!
|
//!
|
||||||
//! The tree is allocated in the layout store's arena in both cases, so saving
|
//! The tree is allocated in the layout store's arena in both cases, so saving
|
||||||
//! writes the edited strings straight back into its leaves. Nothing is written
|
//! writes the edited strings straight back into its leaves. Nothing is written
|
||||||
//! until the confirm button, so cancelling an edit leaves the layout alone.
|
//! until the confirm button, so canceling an edit leaves the layout alone.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const gio = @import("gio");
|
const gio = @import("gio");
|
||||||
|
|||||||
+2
-2
@@ -317,7 +317,7 @@ fn EffectReply(comptime name: []const u8) type {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Effect callback: the child asked what kind of terminal this is (CSI c and
|
/// Effect callback: the child asked what kind of terminal this is (CSI c and
|
||||||
/// friends). The defaults describe a VT220 that does ANSI colour, which is
|
/// friends). The defaults describe a VT220 that does ANSI color, which is
|
||||||
/// what this actually is — the answer matters less than there being one.
|
/// what this actually is — the answer matters less than there being one.
|
||||||
fn effectDeviceAttributes(
|
fn effectDeviceAttributes(
|
||||||
_: *vt.TerminalStream.Handler,
|
_: *vt.TerminalStream.Handler,
|
||||||
@@ -326,7 +326,7 @@ fn effectDeviceAttributes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Effect callback: the child asked whether it is drawing on a light or dark
|
/// Effect callback: the child asked whether it is drawing on a light or dark
|
||||||
/// background (CSI ? 996 n), so it can pick readable colours. Answered from
|
/// background (CSI ? 996 n), so it can pick readable colors. Answered from
|
||||||
/// the scheme the grid is actually painted in.
|
/// the scheme the grid is actually painted in.
|
||||||
fn effectColorScheme(
|
fn effectColorScheme(
|
||||||
_: *vt.TerminalStream.Handler,
|
_: *vt.TerminalStream.Handler,
|
||||||
|
|||||||
+69
-27
@@ -4,8 +4,8 @@
|
|||||||
//! something changes. It carries a version from the start, so adding the next
|
//! something changes. It carries a version from the start, so adding the next
|
||||||
//! setting is not also the day we invent a format.
|
//! setting is not also the day we invent a format.
|
||||||
//!
|
//!
|
||||||
//! The settings are three different shapes. The colour scheme is one word. The
|
//! The settings are three different shapes. The color scheme is one word. The
|
||||||
//! **palette changes** are a base colour per scheme, plus a colour per name for
|
//! **palette changes** are a base color per scheme, plus a color per name for
|
||||||
//! the names someone has gone on to change by hand. The **startup tabs** are a
|
//! the names someone has gone on to change by hand. The **startup tabs** are a
|
||||||
//! list: which
|
//! list: which
|
||||||
//! saved layout to open, what to fill its parameters in with, and what to call
|
//! saved layout to open, what to fill its parameters in with, and what to call
|
||||||
@@ -15,9 +15,9 @@
|
|||||||
//! field.
|
//! field.
|
||||||
//!
|
//!
|
||||||
//! The palette changes deliberately stay out of that arena. They are a base
|
//! The palette changes deliberately stay out of that arena. They are a base
|
||||||
//! colour and a fixed-size array of optionals, which needs no allocation at
|
//! color and a fixed-size array of optionals, which needs no allocation at
|
||||||
//! all — and, more to the
|
//! all — and, more to the
|
||||||
//! point, `setStartup` replaces the arena wholesale, so anything colour-shaped
|
//! point, `setStartup` replaces the arena wholesale, so anything color-shaped
|
||||||
//! living in it would be freed by an unrelated edit to the startup list. The
|
//! living in it would be freed by an unrelated edit to the startup list. The
|
||||||
//! **shortcut overrides** are the same shape for the same reason: a chord is a
|
//! **shortcut overrides** are the same shape for the same reason: a chord is a
|
||||||
//! key and four flags once it has been parsed, so nothing about it needs to
|
//! key and four flags once it has been parsed, so nothing about it needs to
|
||||||
@@ -73,8 +73,8 @@ pub const Theme = enum {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// What someone has done to the palette, per scheme: the colour it is generated
|
/// What someone has done to the palette, per scheme: the color it is generated
|
||||||
/// from, and the individual colours they have set by hand.
|
/// from, and the individual colors they have set by hand.
|
||||||
///
|
///
|
||||||
/// Both schemes are kept, not just the one in force: the editor changes the
|
/// Both schemes are kept, not just the one in force: the editor changes the
|
||||||
/// palette you can see, and pinning the app light for an afternoon shouldn't
|
/// palette you can see, and pinning the app light for an afternoon shouldn't
|
||||||
@@ -137,8 +137,17 @@ theme: Theme = .system,
|
|||||||
/// a mistyped chord and every shell in every tab exiting.
|
/// a mistyped chord and every shell in every tab exiting.
|
||||||
confirm_quit: bool = true,
|
confirm_quit: bool = true,
|
||||||
|
|
||||||
/// Changes to the palette. Empty means every colour is at its default, which is
|
/// Whether a pane finishing posts a desktop notification.
|
||||||
/// the state anyone who never opens the colour editor stays in — and it is the
|
///
|
||||||
|
/// On by default. The whole shape of this app is "set something going in a tab
|
||||||
|
/// and go and do something else", and a sidebar dot can only tell you that
|
||||||
|
/// while you are looking at the sidebar. Someone who does not want to be told
|
||||||
|
/// has a switch in the settings and a per-tab mute in every row menu; someone
|
||||||
|
/// who does want to be told should not have to find a setting first.
|
||||||
|
notifications: bool = true,
|
||||||
|
|
||||||
|
/// Changes to the palette. Empty means every color is at its default, which is
|
||||||
|
/// the state anyone who never opens the color editor stays in — and it is the
|
||||||
/// reason the defaults can be retuned in a later version and still reach them.
|
/// reason the defaults can be retuned in a later version and still reach them.
|
||||||
colors: Colors = .{},
|
colors: Colors = .{},
|
||||||
|
|
||||||
@@ -183,7 +192,7 @@ pub fn deinit() void {
|
|||||||
|
|
||||||
/// Read the settings file, falling back to defaults for anything missing.
|
/// Read the settings file, falling back to defaults for anything missing.
|
||||||
///
|
///
|
||||||
/// Every failure below lands on the same behaviour — carry on with defaults —
|
/// Every failure below lands on the same behavior — carry on with defaults —
|
||||||
/// because there is no useful alternative: this runs before there is a window
|
/// because there is no useful alternative: this runs before there is a window
|
||||||
/// to report an error in, and refusing to start over an unreadable preferences
|
/// to report an error in, and refusing to start over an unreadable preferences
|
||||||
/// file would be a worse outcome than ignoring it. A malformed file is logged
|
/// file would be a worse outcome than ignoring it. A malformed file is logged
|
||||||
@@ -259,6 +268,10 @@ fn parse(self: *Settings, text: []const u8) ParseError!void {
|
|||||||
if (value == .bool) self.confirm_quit = value.bool;
|
if (value == .bool) self.confirm_quit = value.bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (root.get("notifications")) |value| {
|
||||||
|
if (value == .bool) self.notifications = value.bool;
|
||||||
|
}
|
||||||
|
|
||||||
if (root.get("colors")) |value| self.parseColors(value);
|
if (root.get("colors")) |value| self.parseColors(value);
|
||||||
|
|
||||||
if (root.get("tint")) |value| self.parseTint(value);
|
if (root.get("tint")) |value| self.parseTint(value);
|
||||||
@@ -270,8 +283,8 @@ fn parse(self: *Settings, text: []const u8) ParseError!void {
|
|||||||
|
|
||||||
/// Read the palette overrides: `{"dark": {"accent": "#ff8800"}, "light": {…}}`.
|
/// Read the palette overrides: `{"dark": {"accent": "#ff8800"}, "light": {…}}`.
|
||||||
///
|
///
|
||||||
/// Nothing here can fail the file. A colour is one word of a palette, and the
|
/// Nothing here can fail the file. A color is one word of a palette, and the
|
||||||
/// answer to a word we don't recognise or can't parse is the default for it —
|
/// answer to a word we don't recognize or can't parse is the default for it —
|
||||||
/// which is exactly what an absent key already means, so an unknown name from a
|
/// which is exactly what an absent key already means, so an unknown name from a
|
||||||
/// newer version and a typo in a hand-edited file cost the same nothing.
|
/// newer version and a typo in a hand-edited file cost the same nothing.
|
||||||
fn parseColors(self: *Settings, raw: std.json.Value) void {
|
fn parseColors(self: *Settings, raw: std.json.Value) void {
|
||||||
@@ -298,7 +311,7 @@ fn parseColors(self: *Settings, raw: std.json.Value) void {
|
|||||||
var it = colors.iterator();
|
var it = colors.iterator();
|
||||||
while (it.next()) |kv| {
|
while (it.next()) |kv| {
|
||||||
const key = std.meta.stringToEnum(palette.Key, kv.key_ptr.*) orelse {
|
const key = std.meta.stringToEnum(palette.Key, kv.key_ptr.*) orelse {
|
||||||
std.log.warn("settings: no colour called \"{s}\"; ignoring it", .{kv.key_ptr.*});
|
std.log.warn("settings: no color called \"{s}\"; ignoring it", .{kv.key_ptr.*});
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -313,7 +326,7 @@ fn parseColors(self: *Settings, raw: std.json.Value) void {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const color = palette.Rgb.parse(text) orelse {
|
const color = palette.Rgb.parse(text) orelse {
|
||||||
std.log.warn("settings: \"{s}\" is not a #rrggbb colour", .{text});
|
std.log.warn("settings: \"{s}\" is not a #rrggbb color", .{text});
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -325,7 +338,7 @@ fn parseColors(self: *Settings, raw: std.json.Value) void {
|
|||||||
/// Read the generated palettes: `{"dark": {"base": "#3a76f0", "contrast": 0.5}}`.
|
/// Read the generated palettes: `{"dark": {"base": "#3a76f0", "contrast": 0.5}}`.
|
||||||
///
|
///
|
||||||
/// A scheme without a `base` has no generated palette at all, whatever else the
|
/// A scheme without a `base` has no generated palette at all, whatever else the
|
||||||
/// entry says — the contrast is how far apart to spread the colours built from
|
/// entry says — the contrast is how far apart to spread the colors built from
|
||||||
/// the base, so on its own it describes nothing. That is also why a malformed
|
/// the base, so on its own it describes nothing. That is also why a malformed
|
||||||
/// contrast costs only the contrast: the base is the setting, and dropping the
|
/// contrast costs only the contrast: the base is the setting, and dropping the
|
||||||
/// whole theme over one bad number would be the wrong trade.
|
/// whole theme over one bad number would be the wrong trade.
|
||||||
@@ -356,7 +369,7 @@ fn parseTint(self: *Settings, raw: std.json.Value) void {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const base = palette.Rgb.parse(text) orelse {
|
const base = palette.Rgb.parse(text) orelse {
|
||||||
std.log.warn("settings: \"{s}\" is not a #rrggbb colour", .{text});
|
std.log.warn("settings: \"{s}\" is not a #rrggbb color", .{text});
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -386,7 +399,7 @@ fn parseTint(self: *Settings, raw: std.json.Value) void {
|
|||||||
/// list — or `null` — is how a shortcut is turned off, which is worth having:
|
/// list — or `null` — is how a shortcut is turned off, which is worth having:
|
||||||
/// the chords here are ones a program running in the terminal may want back.
|
/// the chords here are ones a program running in the terminal may want back.
|
||||||
///
|
///
|
||||||
/// Nothing here can fail the file, on the same grounds as the colours above. An
|
/// Nothing here can fail the file, on the same grounds as the colors above. An
|
||||||
/// action this build doesn't have, or a chord that doesn't parse, costs that one
|
/// action this build doesn't have, or a chord that doesn't parse, costs that one
|
||||||
/// binding and is logged; what it falls back to is the shipped default, which is
|
/// binding and is logged; what it falls back to is the shipped default, which is
|
||||||
/// exactly what an absent entry already means.
|
/// exactly what an absent entry already means.
|
||||||
@@ -610,8 +623,10 @@ fn serialize(self: *Settings) SaveError![]u8 {
|
|||||||
try json.write(@tagName(self.theme));
|
try json.write(@tagName(self.theme));
|
||||||
try json.objectField("confirm_quit");
|
try json.objectField("confirm_quit");
|
||||||
try json.write(self.confirm_quit);
|
try json.write(self.confirm_quit);
|
||||||
|
try json.objectField("notifications");
|
||||||
|
try json.write(self.notifications);
|
||||||
|
|
||||||
// Only the colours that have been changed, and only the schemes that have
|
// Only the colors that have been changed, and only the schemes that have
|
||||||
// any. The object itself is written even when empty, for the same reason
|
// any. The object itself is written even when empty, for the same reason
|
||||||
// the startup list below is: a file you have opened to see what you can put
|
// the startup list below is: a file you have opened to see what you can put
|
||||||
// in it should name what it accepts.
|
// in it should name what it accepts.
|
||||||
@@ -636,7 +651,7 @@ fn serialize(self: *Settings) SaveError![]u8 {
|
|||||||
}
|
}
|
||||||
try json.endObject();
|
try json.endObject();
|
||||||
|
|
||||||
// And the base colour each scheme's palette is generated from, for the
|
// And the base color each scheme's palette is generated from, for the
|
||||||
// schemes that have one. Written after the overrides rather than before
|
// schemes that have one. Written after the overrides rather than before
|
||||||
// because that is the order they apply in, and a settings file is read by
|
// because that is the order they apply in, and a settings file is read by
|
||||||
// people as well as by this.
|
// people as well as by this.
|
||||||
@@ -657,7 +672,7 @@ fn serialize(self: *Settings) SaveError![]u8 {
|
|||||||
try json.endObject();
|
try json.endObject();
|
||||||
|
|
||||||
// Only the shortcuts that have been rebound, for the same reasons as the
|
// Only the shortcuts that have been rebound, for the same reasons as the
|
||||||
// colours — and with the same consequence, which is the one that matters
|
// colors — and with the same consequence, which is the one that matters
|
||||||
// here: saving rewrites the whole file, so a hand-edited shortcut has to
|
// here: saving rewrites the whole file, so a hand-edited shortcut has to
|
||||||
// come back out of this function or changing the theme would delete it.
|
// come back out of this function or changing the theme would delete it.
|
||||||
// Chords are written from the parsed form rather than the text they came
|
// Chords are written from the parsed form rather than the text they came
|
||||||
@@ -738,7 +753,7 @@ fn configDir(buf: []u8) ?[:0]const u8 {
|
|||||||
//
|
//
|
||||||
// Reading and writing the file, minus the file: `parse` and `serialize` are
|
// Reading and writing the file, minus the file: `parse` and `serialize` are
|
||||||
// where the on-disk shape actually lives, and they need nothing but an
|
// where the on-disk shape actually lives, and they need nothing but an
|
||||||
// allocator. The palette overrides are what these are here for — a colour is a
|
// allocator. The palette overrides are what these are here for — a color is a
|
||||||
// name and a hex string in a nested object, which is three chances to write out
|
// name and a hex string in a nested object, which is three chances to write out
|
||||||
// something the next launch reads back as a different palette.
|
// something the next launch reads back as a different palette.
|
||||||
|
|
||||||
@@ -768,6 +783,33 @@ test "confirming a quit is on unless the file turns it off" {
|
|||||||
try std.testing.expect(std.mem.indexOf(u8, text, "\"confirm_quit\": false") != null);
|
try std.testing.expect(std.mem.indexOf(u8, text, "\"confirm_quit\": false") != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "notifications are on unless the file turns them off" {
|
||||||
|
var settings = forTesting();
|
||||||
|
defer settings.arena.deinit();
|
||||||
|
|
||||||
|
try std.testing.expect(settings.notifications);
|
||||||
|
|
||||||
|
try settings.parse(
|
||||||
|
\\{"notifications": false}
|
||||||
|
);
|
||||||
|
try std.testing.expect(!settings.notifications);
|
||||||
|
|
||||||
|
const text = try settings.serialize();
|
||||||
|
defer std.testing.allocator.free(text);
|
||||||
|
|
||||||
|
try std.testing.expect(std.mem.indexOf(u8, text, "\"notifications\": false") != null);
|
||||||
|
|
||||||
|
// And unreadable is the same as absent, which for this flag means on: the
|
||||||
|
// failure someone would report is "it stopped telling me", not "it told me
|
||||||
|
// and I had typed nonsense into the file".
|
||||||
|
var typo = forTesting();
|
||||||
|
defer typo.arena.deinit();
|
||||||
|
try typo.parse(
|
||||||
|
\\{"notifications": "off"}
|
||||||
|
);
|
||||||
|
try std.testing.expect(typo.notifications);
|
||||||
|
}
|
||||||
|
|
||||||
test "a confirm_quit we can't read leaves the confirmation up" {
|
test "a confirm_quit we can't read leaves the confirmation up" {
|
||||||
var settings = forTesting();
|
var settings = forTesting();
|
||||||
defer settings.arena.deinit();
|
defer settings.arena.deinit();
|
||||||
@@ -797,19 +839,19 @@ test "palette overrides are read per scheme" {
|
|||||||
try std.testing.expectEqual(palette.Rgb.parse("#112233").?, settings.colors.dark.overrides.get(.ansi_red).?);
|
try std.testing.expectEqual(palette.Rgb.parse("#112233").?, settings.colors.dark.overrides.get(.ansi_red).?);
|
||||||
try std.testing.expectEqual(palette.Rgb.parse("#fafafa").?, settings.colors.light.overrides.get(.sidebar).?);
|
try std.testing.expectEqual(palette.Rgb.parse("#fafafa").?, settings.colors.light.overrides.get(.sidebar).?);
|
||||||
|
|
||||||
// A scheme that says nothing about a colour leaves it at its default, which
|
// A scheme that says nothing about a color leaves it at its default, which
|
||||||
// is what an absent override means everywhere else.
|
// is what an absent override means everywhere else.
|
||||||
try std.testing.expectEqual(@as(?palette.Rgb, null), settings.colors.light.overrides.get(.accent));
|
try std.testing.expectEqual(@as(?palette.Rgb, null), settings.colors.light.overrides.get(.accent));
|
||||||
try std.testing.expectEqual(@as(?palette.Rgb, null), settings.colors.dark.overrides.get(.sidebar));
|
try std.testing.expectEqual(@as(?palette.Rgb, null), settings.colors.dark.overrides.get(.sidebar));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "a colour we can't read costs that colour and nothing else" {
|
test "a color we can't read costs that color and nothing else" {
|
||||||
var settings = forTesting();
|
var settings = forTesting();
|
||||||
defer settings.arena.deinit();
|
defer settings.arena.deinit();
|
||||||
|
|
||||||
try settings.parse(
|
try settings.parse(
|
||||||
\\{"theme": "dark", "colors": {"dark": {
|
\\{"theme": "dark", "colors": {"dark": {
|
||||||
\\ "not_a_colour": "#123456",
|
\\ "not_a_color": "#123456",
|
||||||
\\ "border": "rebeccapurple",
|
\\ "border": "rebeccapurple",
|
||||||
\\ "sidebar": 12,
|
\\ "sidebar": 12,
|
||||||
\\ "accent": "#ff8800"
|
\\ "accent": "#ff8800"
|
||||||
@@ -819,8 +861,8 @@ test "a colour we can't read costs that colour and nothing else" {
|
|||||||
try std.testing.expectEqual(@as(?palette.Rgb, null), settings.colors.dark.overrides.get(.border));
|
try std.testing.expectEqual(@as(?palette.Rgb, null), settings.colors.dark.overrides.get(.border));
|
||||||
try std.testing.expectEqual(@as(?palette.Rgb, null), settings.colors.dark.overrides.get(.sidebar));
|
try std.testing.expectEqual(@as(?palette.Rgb, null), settings.colors.dark.overrides.get(.sidebar));
|
||||||
|
|
||||||
// The whole file survives, including the theme above the colours and the
|
// The whole file survives, including the theme above the colors and the
|
||||||
// one colour in the list that was well formed.
|
// one color in the list that was well formed.
|
||||||
try std.testing.expectEqual(Theme.dark, settings.theme);
|
try std.testing.expectEqual(Theme.dark, settings.theme);
|
||||||
try std.testing.expectEqual(palette.Rgb.parse("#ff8800").?, settings.colors.dark.overrides.get(.accent).?);
|
try std.testing.expectEqual(palette.Rgb.parse("#ff8800").?, settings.colors.dark.overrides.get(.accent).?);
|
||||||
}
|
}
|
||||||
@@ -835,7 +877,7 @@ test "colors is written even when nothing has been changed" {
|
|||||||
try std.testing.expect(std.mem.indexOf(u8, text, "\"colors\": {}") != null);
|
try std.testing.expect(std.mem.indexOf(u8, text, "\"colors\": {}") != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "only the changed colours are written, and they come back" {
|
test "only the changed colors are written, and they come back" {
|
||||||
var settings = forTesting();
|
var settings = forTesting();
|
||||||
defer settings.arena.deinit();
|
defer settings.arena.deinit();
|
||||||
|
|
||||||
@@ -864,7 +906,7 @@ test "only the changed colours are written, and they come back" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test "a base colour and its contrast survive the file" {
|
test "a base color and its contrast survive the file" {
|
||||||
var settings = forTesting();
|
var settings = forTesting();
|
||||||
defer settings.arena.deinit();
|
defer settings.arena.deinit();
|
||||||
|
|
||||||
|
|||||||
+77
-3
@@ -109,6 +109,9 @@ colors: *PaletteEditor,
|
|||||||
/// one place and read in another; nothing re-checks it.
|
/// one place and read in another; nothing re-checks it.
|
||||||
confirm_quit: *gtk.Switch,
|
confirm_quit: *gtk.Switch,
|
||||||
|
|
||||||
|
/// The notifications toggle, held for the same reason.
|
||||||
|
notifications: *gtk.Switch,
|
||||||
|
|
||||||
/// Container the startup rows live in, so rows can be added and removed after
|
/// Container the startup rows live in, so rows can be added and removed after
|
||||||
/// the page is already on screen.
|
/// the page is already on screen.
|
||||||
startup_box: *gtk.Box,
|
startup_box: *gtk.Box,
|
||||||
@@ -145,6 +148,7 @@ pub fn present(alloc: std.mem.Allocator, parent: *gtk.Window, opts: Options) !vo
|
|||||||
.theme_buttons = undefined,
|
.theme_buttons = undefined,
|
||||||
.colors = undefined,
|
.colors = undefined,
|
||||||
.confirm_quit = gtk.Switch.new(),
|
.confirm_quit = gtk.Switch.new(),
|
||||||
|
.notifications = gtk.Switch.new(),
|
||||||
.startup_box = gtk.Box.new(.vertical, 6),
|
.startup_box = gtk.Box.new(.vertical, 6),
|
||||||
.error_label = gtk.Label.new(null),
|
.error_label = gtk.Label.new(null),
|
||||||
};
|
};
|
||||||
@@ -165,6 +169,7 @@ pub fn present(alloc: std.mem.Allocator, parent: *gtk.Window, opts: Options) !vo
|
|||||||
content.as(gtk.Widget).addCssClass("playpen-dialog-content");
|
content.as(gtk.Widget).addCssClass("playpen-dialog-content");
|
||||||
|
|
||||||
content.append(self.buildAppearance());
|
content.append(self.buildAppearance());
|
||||||
|
content.append(self.buildNotifications());
|
||||||
content.append(self.buildQuitting());
|
content.append(self.buildQuitting());
|
||||||
content.append(self.buildStartup());
|
content.append(self.buildStartup());
|
||||||
|
|
||||||
@@ -219,7 +224,7 @@ pub fn close() void {
|
|||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Appearance
|
// Appearance
|
||||||
|
|
||||||
/// The Appearance section: the colour scheme, and the palette it resolves to.
|
/// The Appearance section: the color scheme, and the palette it resolves to.
|
||||||
fn buildAppearance(self: *SettingsDialog) *gtk.Widget {
|
fn buildAppearance(self: *SettingsDialog) *gtk.Widget {
|
||||||
const group = gtk.Box.new(.vertical, 10);
|
const group = gtk.Box.new(.vertical, 10);
|
||||||
group.as(gtk.Widget).addCssClass("playpen-settings-group");
|
group.as(gtk.Widget).addCssClass("playpen-settings-group");
|
||||||
@@ -266,7 +271,7 @@ fn onEditorReport(ctx: ?*anyopaque, message: ?[:0]const u8) void {
|
|||||||
/// The scheme picker: one toggle per choice, drawn as a single linked control.
|
/// The scheme picker: one toggle per choice, drawn as a single linked control.
|
||||||
///
|
///
|
||||||
/// They are not put in a GTK radio group. Grouped toggles can't be unchecked by
|
/// They are not put in a GTK radio group. Grouped toggles can't be unchecked by
|
||||||
/// clicking the active one, which is the behaviour we want, but the group also
|
/// clicking the active one, which is the behavior we want, but the group also
|
||||||
/// emits `toggled` twice per change — once off, once on — and the off half
|
/// emits `toggled` twice per change — once off, once on — and the off half
|
||||||
/// would apply whichever scheme happened to be next in the list. Handling the
|
/// would apply whichever scheme happened to be next in the list. Handling the
|
||||||
/// exclusivity here instead makes each click exactly one decision.
|
/// exclusivity here instead makes each click exactly one decision.
|
||||||
@@ -326,13 +331,82 @@ fn onThemeToggled(button: *gtk.ToggleButton, self: *SettingsDialog) callconv(.c)
|
|||||||
appearance.setTheme(values[index]);
|
appearance.setTheme(values[index]);
|
||||||
|
|
||||||
// The palette editor edits whichever scheme is on screen, and that is what
|
// The palette editor edits whichever scheme is on screen, and that is what
|
||||||
// just changed: its swatches now stand for the other palette's colours.
|
// just changed: its swatches now stand for the other palette's colors.
|
||||||
self.colors.refresh();
|
self.colors.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Quitting
|
// Quitting
|
||||||
|
|
||||||
|
/// The Notifications section: whether a finish reaches the desktop.
|
||||||
|
///
|
||||||
|
/// The whole-app half of the mute. The per-tab half lives in each row's menu,
|
||||||
|
/// because it is a thing you decide about one tab while looking at it, and
|
||||||
|
/// walking to a settings window to say "not this one, for the next hour" would
|
||||||
|
/// mean nobody ever did.
|
||||||
|
fn buildNotifications(self: *SettingsDialog) *gtk.Widget {
|
||||||
|
const group = gtk.Box.new(.vertical, 10);
|
||||||
|
group.as(gtk.Widget).addCssClass("playpen-settings-group");
|
||||||
|
|
||||||
|
const title = gtk.Label.new("Notifications");
|
||||||
|
title.setXalign(0);
|
||||||
|
title.as(gtk.Widget).addCssClass("playpen-settings-title");
|
||||||
|
group.append(title.as(gtk.Widget));
|
||||||
|
|
||||||
|
const row = gtk.Box.new(.horizontal, 12);
|
||||||
|
|
||||||
|
const labels = gtk.Box.new(.vertical, 2);
|
||||||
|
labels.as(gtk.Widget).setHexpand(1);
|
||||||
|
labels.as(gtk.Widget).setValign(.center);
|
||||||
|
|
||||||
|
const name = gtk.Label.new("Tell me when a pane finishes");
|
||||||
|
name.setXalign(0);
|
||||||
|
name.as(gtk.Widget).addCssClass("playpen-dialog-label");
|
||||||
|
labels.append(name.as(gtk.Widget));
|
||||||
|
|
||||||
|
const hint_label = gtk.Label.new(
|
||||||
|
"A desktop notification when something running in a tab goes quiet, " ++
|
||||||
|
"for the tabs you aren't looking at. Off, the sidebar dots are the " ++
|
||||||
|
"only word you get. One tab at a time can be muted from its row menu.",
|
||||||
|
);
|
||||||
|
hint_label.setXalign(0);
|
||||||
|
hint_label.setWrap(1);
|
||||||
|
hint_label.as(gtk.Widget).addCssClass("playpen-dialog-sublabel");
|
||||||
|
labels.append(hint_label.as(gtk.Widget));
|
||||||
|
|
||||||
|
row.append(labels.as(gtk.Widget));
|
||||||
|
|
||||||
|
self.notifications.setActive(@intFromBool(Settings.get().notifications));
|
||||||
|
self.notifications.as(gtk.Widget).setValign(.center);
|
||||||
|
_ = gobject.Object.signals.notify.connect(
|
||||||
|
self.notifications,
|
||||||
|
*SettingsDialog,
|
||||||
|
&onNotificationsChanged,
|
||||||
|
self,
|
||||||
|
.{ .detail = "active" },
|
||||||
|
);
|
||||||
|
row.append(self.notifications.as(gtk.Widget));
|
||||||
|
|
||||||
|
group.append(row.as(gtk.Widget));
|
||||||
|
|
||||||
|
return group.as(gtk.Widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Straight to the file, for the same reason `onConfirmQuitChanged` is.
|
||||||
|
fn onNotificationsChanged(
|
||||||
|
toggle: *gtk.Switch,
|
||||||
|
_: *gobject.ParamSpec,
|
||||||
|
self: *SettingsDialog,
|
||||||
|
) callconv(.c) void {
|
||||||
|
Settings.get().notifications = toggle.getActive() != 0;
|
||||||
|
|
||||||
|
Settings.get().save() catch {
|
||||||
|
self.showError("Could not write the settings file.");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.showError(null);
|
||||||
|
}
|
||||||
|
|
||||||
/// The Quitting section: whether closing the window asks first.
|
/// The Quitting section: whether closing the window asks first.
|
||||||
///
|
///
|
||||||
/// Above the startup list rather than below it. That list grows with the number
|
/// Above the startup list rather than below it. That list grows with the number
|
||||||
|
|||||||
+1
-1
@@ -712,7 +712,7 @@ fn selectPress(self: *Terminal, x: f64, y: f64) void {
|
|||||||
const term = &self.session.term;
|
const term = &self.session.term;
|
||||||
const pin = self.pinAt(x, y) orelse return;
|
const pin = self.pinAt(x, y) orelse return;
|
||||||
|
|
||||||
// The gesture counts the clicks and picks the behaviour: one selects by
|
// The gesture counts the clicks and picks the behavior: one selects by
|
||||||
// cell, two by word, three by line.
|
// cell, two by word, three by line.
|
||||||
const sel = self.gesture.press(term, .{
|
const sel = self.gesture.press(term, .{
|
||||||
.time = now(),
|
.time = now(),
|
||||||
|
|||||||
+16
-12
@@ -3,7 +3,7 @@
|
|||||||
//!
|
//!
|
||||||
//! Dragging a pane rearranges the view live rather than on release. Each time
|
//! Dragging a pane rearranges the view live rather than on release. Each time
|
||||||
//! the drop target changes, the move is applied for real, so what you see
|
//! the drop target changes, the move is applied for real, so what you see
|
||||||
//! during the drag is the layout you will get. If the drag is cancelled the
|
//! during the drag is the layout you will get. If the drag is canceled the
|
||||||
//! pane goes back where it came from, which is possible because only the
|
//! pane goes back where it came from, which is possible because only the
|
||||||
//! dragged pane ever moves: the rest of the tree is unchanged by a move, so
|
//! dragged pane ever moves: the rest of the tree is unchanged by a move, so
|
||||||
//! re-inserting it next to its original sibling restores the original shape.
|
//! re-inserting it next to its original sibling restores the original shape.
|
||||||
@@ -40,7 +40,7 @@ pub const Target = struct {
|
|||||||
const Drag = struct {
|
const Drag = struct {
|
||||||
pane: *Pane,
|
pane: *Pane,
|
||||||
|
|
||||||
/// Where the pane sat when the drag began, so a cancelled drag can put
|
/// Where the pane sat when the drag began, so a canceled drag can put
|
||||||
/// it back.
|
/// it back.
|
||||||
origin_sibling: *Layout.Node,
|
origin_sibling: *Layout.Node,
|
||||||
origin_side: Side,
|
origin_side: Side,
|
||||||
@@ -50,7 +50,7 @@ const Drag = struct {
|
|||||||
previewed: ?Target = null,
|
previewed: ?Target = null,
|
||||||
|
|
||||||
/// Set once a drop has been accepted; a drag that ends without this was
|
/// Set once a drop has been accepted; a drag that ends without this was
|
||||||
/// cancelled and must be undone.
|
/// canceled and must be undone.
|
||||||
committed: bool = false,
|
committed: bool = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -101,7 +101,12 @@ on_status: *const fn (ctx: ?*anyopaque) void,
|
|||||||
/// last here" cannot be recovered by looking at the panes afterwards, since
|
/// last here" cannot be recovered by looking at the panes afterwards, since
|
||||||
/// one that finished before your last visit looks identical to one that
|
/// one that finished before your last visit looks identical to one that
|
||||||
/// finished after it.
|
/// finished after it.
|
||||||
on_finished: *const fn (ctx: ?*anyopaque) void,
|
///
|
||||||
|
/// Carries the finished pane's title, which is what a desktop notification says
|
||||||
|
/// under the tab's name. It comes with the edge rather than being read off the
|
||||||
|
/// view afterwards for the same reason the edge does: in a split, the pane that
|
||||||
|
/// finished is not necessarily the one whose title the tab is wearing.
|
||||||
|
on_finished: *const fn (ctx: ?*anyopaque, task: []const u8) void,
|
||||||
|
|
||||||
/// Something in here asked for a review pane.
|
/// Something in here asked for a review pane.
|
||||||
///
|
///
|
||||||
@@ -115,7 +120,7 @@ pub const Callbacks = struct {
|
|||||||
on_empty: *const fn (ctx: ?*anyopaque) void,
|
on_empty: *const fn (ctx: ?*anyopaque) void,
|
||||||
on_title: *const fn (ctx: ?*anyopaque) void,
|
on_title: *const fn (ctx: ?*anyopaque) void,
|
||||||
on_status: *const fn (ctx: ?*anyopaque) void,
|
on_status: *const fn (ctx: ?*anyopaque) void,
|
||||||
on_finished: *const fn (ctx: ?*anyopaque) void,
|
on_finished: *const fn (ctx: ?*anyopaque, task: []const u8) void,
|
||||||
on_review: *const fn (ctx: ?*anyopaque) void,
|
on_review: *const fn (ctx: ?*anyopaque) void,
|
||||||
ctx: ?*anyopaque,
|
ctx: ?*anyopaque,
|
||||||
};
|
};
|
||||||
@@ -167,7 +172,7 @@ pub fn widget(self: *View) *gtk.Widget {
|
|||||||
// arrangement rather than rebuilding an approximation of it.
|
// arrangement rather than rebuilding an approximation of it.
|
||||||
|
|
||||||
/// Put the view on screen. Every structural change goes through here rather
|
/// Put the view on screen. Every structural change goes through here rather
|
||||||
/// than calling the layout directly, so zoom is honoured from one place
|
/// than calling the layout directly, so zoom is honored from one place
|
||||||
/// instead of being re-checked at each call site.
|
/// instead of being re-checked at each call site.
|
||||||
fn render(self: *View) void {
|
fn render(self: *View) void {
|
||||||
if (self.zoomed) |pane| {
|
if (self.zoomed) |pane| {
|
||||||
@@ -418,9 +423,8 @@ pub fn setFocused(self: *View, pane: *Pane) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn paneFinished(self: *View, pane: *Pane) void {
|
pub fn paneFinished(self: *View, pane: *Pane) void {
|
||||||
_ = pane;
|
|
||||||
if (self.closing) return;
|
if (self.closing) return;
|
||||||
self.on_finished(self.ctx);
|
self.on_finished(self.ctx, pane.titleSlice());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn paneTitleChanged(self: *View, pane: *Pane) void {
|
pub fn paneTitleChanged(self: *View, pane: *Pane) void {
|
||||||
@@ -685,7 +689,7 @@ fn reattachAtRoot(self: *View, node: *Layout.Node) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Move the focused pane one step in a direction: the keyboard equivalent of
|
/// Move the focused pane one step in a direction: the keyboard equivalent of
|
||||||
/// dragging it onto the neighbouring pane.
|
/// dragging it onto the neighboring pane.
|
||||||
pub fn moveFocused(self: *View, side: Side) void {
|
pub fn moveFocused(self: *View, side: Side) void {
|
||||||
const pane = self.focusedPane() orelse return;
|
const pane = self.focusedPane() orelse return;
|
||||||
if (self.panes.items.len < 2) return;
|
if (self.panes.items.len < 2) return;
|
||||||
@@ -707,7 +711,7 @@ pub fn moveFocused(self: *View, side: Side) void {
|
|||||||
|
|
||||||
/// Move focus one step in a direction, to whichever pane is sitting there.
|
/// Move focus one step in a direction, to whichever pane is sitting there.
|
||||||
///
|
///
|
||||||
/// The counterpart of `moveFocused`: same neighbour, but the pane stays where
|
/// The counterpart of `moveFocused`: same neighbor, but the pane stays where
|
||||||
/// it is and you go to it instead. Both are answered geometrically rather than
|
/// it is and you go to it instead. Both are answered geometrically rather than
|
||||||
/// by walking the split tree, so "the pane to the left" means the one that
|
/// by walking the split tree, so "the pane to the left" means the one that
|
||||||
/// looks like it — a tree walk would have to pick between siblings and cousins,
|
/// looks like it — a tree walk would have to pick between siblings and cousins,
|
||||||
@@ -723,7 +727,7 @@ pub fn focusNeighbor(self: *View, side: Side) bool {
|
|||||||
// While zoomed there is only one pane on screen and the others are
|
// While zoomed there is only one pane on screen and the others are
|
||||||
// unparented, so there is nothing to probe for and nothing to move to. The
|
// unparented, so there is nothing to probe for and nothing to move to. The
|
||||||
// arrangement is exactly what zoom is hiding; leaving it to hunt for a
|
// arrangement is exactly what zoom is hiding; leaving it to hunt for a
|
||||||
// neighbour would undo what the user asked for.
|
// neighbor would undo what the user asked for.
|
||||||
if (self.zoomed != null) return false;
|
if (self.zoomed != null) return false;
|
||||||
|
|
||||||
const target = self.neighbor(pane, side) orelse return false;
|
const target = self.neighbor(pane, side) orelse return false;
|
||||||
@@ -773,7 +777,7 @@ pub fn beginDrag(self: *View, pane: *Pane) bool {
|
|||||||
|
|
||||||
// Only the zoomed pane is on screen, so there is nothing to drop against
|
// Only the zoomed pane is on screen, so there is nothing to drop against
|
||||||
// and no preview to show. Refusing the drag is better than starting one
|
// and no preview to show. Refusing the drag is better than starting one
|
||||||
// that can only ever be cancelled.
|
// that can only ever be canceled.
|
||||||
if (self.zoomed != null) return false;
|
if (self.zoomed != null) return false;
|
||||||
|
|
||||||
const node = self.layout.find(pane) orelse return false;
|
const node = self.layout.find(pane) orelse return false;
|
||||||
|
|||||||
+127
-21
@@ -28,6 +28,7 @@ const View = @import("View.zig");
|
|||||||
const appearance = @import("appearance.zig");
|
const appearance = @import("appearance.zig");
|
||||||
const emoji = @import("emoji.zig");
|
const emoji = @import("emoji.zig");
|
||||||
const key = @import("key.zig");
|
const key = @import("key.zig");
|
||||||
|
const notify = @import("notify.zig");
|
||||||
const review = @import("review.zig");
|
const review = @import("review.zig");
|
||||||
const shortcuts = @import("shortcuts.zig");
|
const shortcuts = @import("shortcuts.zig");
|
||||||
|
|
||||||
@@ -132,7 +133,7 @@ layout_popover: *gtk.Popover,
|
|||||||
/// they belong to are on screen.
|
/// they belong to are on screen.
|
||||||
layout_rows: std.ArrayListUnmanaged(*LayoutRow) = .empty,
|
layout_rows: std.ArrayListUnmanaged(*LayoutRow) = .empty,
|
||||||
|
|
||||||
/// What a sidebar row is signalling. Defined with the dots themselves, since
|
/// What a sidebar row is signaling. Defined with the dots themselves, since
|
||||||
/// a row and a pane header show the same five states for the same reasons.
|
/// a row and a pane header show the same five states for the same reasons.
|
||||||
const Attention = Pane.Attention;
|
const Attention = Pane.Attention;
|
||||||
|
|
||||||
@@ -155,17 +156,17 @@ const Source = struct {
|
|||||||
///
|
///
|
||||||
/// Shaped like the pane drag in `View`, and for the same reason: the reorder is
|
/// Shaped like the pane drag in `View`, and for the same reason: the reorder is
|
||||||
/// applied as the pointer moves rather than on the drop, so the sidebar under
|
/// applied as the pointer moves rather than on the drop, so the sidebar under
|
||||||
/// the cursor is always the order you will get. That means a cancelled drag has
|
/// the cursor is always the order you will get. That means a canceled drag has
|
||||||
/// something to undo, which is what `origin` is for.
|
/// something to undo, which is what `origin` is for.
|
||||||
const Drag = struct {
|
const Drag = struct {
|
||||||
tab: *Tab,
|
tab: *Tab,
|
||||||
|
|
||||||
/// Where the tab sat in `tabs` when the drag began, so a cancelled drag can
|
/// Where the tab sat in `tabs` when the drag began, so a canceled drag can
|
||||||
/// put it back.
|
/// put it back.
|
||||||
origin: usize,
|
origin: usize,
|
||||||
|
|
||||||
/// Set once a drop has been accepted; a drag that ends without this was
|
/// Set once a drop has been accepted; a drag that ends without this was
|
||||||
/// cancelled, and the preview has to be undone.
|
/// canceled, and the preview has to be undone.
|
||||||
committed: bool = false,
|
committed: bool = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -177,7 +178,7 @@ const Tab = struct {
|
|||||||
row: *gtk.ListBoxRow,
|
row: *gtk.ListBoxRow,
|
||||||
label: *gtk.Label,
|
label: *gtk.Label,
|
||||||
|
|
||||||
/// Shows what the tab's focused pane is, so a web view is recognisable in
|
/// Shows what the tab's focused pane is, so a web view is recognizable in
|
||||||
/// the sidebar without reading the title.
|
/// the sidebar without reading the title.
|
||||||
icon: *gtk.Image,
|
icon: *gtk.Image,
|
||||||
|
|
||||||
@@ -228,6 +229,13 @@ const Tab = struct {
|
|||||||
/// with me?", which only you can answer.
|
/// with me?", which only you can answer.
|
||||||
finished_since_visit: bool = false,
|
finished_since_visit: bool = false,
|
||||||
|
|
||||||
|
/// Whether this tab's finishes are posting notifications, and until when.
|
||||||
|
///
|
||||||
|
/// Session state rather than a setting: a mute is set from the row menu to
|
||||||
|
/// get through the next hour, and a tab does not survive the app anyway, so
|
||||||
|
/// there is nothing here worth writing to a file. See `notify.Mute`.
|
||||||
|
mute: notify.Mute = .off,
|
||||||
|
|
||||||
name: [16]u8,
|
name: [16]u8,
|
||||||
name_len: usize,
|
name_len: usize,
|
||||||
|
|
||||||
@@ -334,7 +342,7 @@ pub fn create(alloc: std.mem.Allocator, app: *adw.Application) !*Window {
|
|||||||
|
|
||||||
// One drop target for the whole list rather than one per row, so that the
|
// One drop target for the whole list rather than one per row, so that the
|
||||||
// gaps between rows and the empty space under the last one are part of it:
|
// gaps between rows and the empty space under the last one are part of it:
|
||||||
// a drag to the bottom of the sidebar should land there, not be cancelled
|
// a drag to the bottom of the sidebar should land there, not be canceled
|
||||||
// for having missed every row by a few pixels.
|
// for having missed every row by a few pixels.
|
||||||
self.installRowDropTarget();
|
self.installRowDropTarget();
|
||||||
|
|
||||||
@@ -605,18 +613,15 @@ fn bindReview(self: *Window, tab: *Tab) void {
|
|||||||
//
|
//
|
||||||
// It does not select the row it belongs to. Renaming a tab, or giving it an
|
// It does not select the row it belongs to. Renaming a tab, or giving it an
|
||||||
// emoji, is not a reason to go and look at it — often it is the opposite, since
|
// emoji, is not a reason to go and look at it — often it is the opposite, since
|
||||||
// the tab you are labelling is the one you are about to leave alone for a while.
|
// the tab you are labeling is the one you are about to leave alone for a while.
|
||||||
|
|
||||||
/// Attach the row's context menu and the right-click that opens it.
|
/// Attach the row's context menu and the right-click that opens it.
|
||||||
|
///
|
||||||
|
/// The menu's contents are not built here — see `fillRowMenu`, which builds them
|
||||||
|
/// as it opens.
|
||||||
fn buildRowMenu(self: *Window, tab: *Tab, anchor: *gtk.Box) void {
|
fn buildRowMenu(self: *Window, tab: *Tab, anchor: *gtk.Box) void {
|
||||||
_ = self;
|
_ = self;
|
||||||
|
|
||||||
const box = gtk.Box.new(.vertical, 2);
|
|
||||||
box.as(gtk.Widget).addCssClass("playpen-row-menu");
|
|
||||||
box.append(menuItem("Rename", &onMenuRename, tab));
|
|
||||||
box.append(menuItem("Settings…", &onMenuSettings, tab));
|
|
||||||
|
|
||||||
tab.menu_popover.setChild(box.as(gtk.Widget));
|
|
||||||
tab.menu_popover.setHasArrow(0);
|
tab.menu_popover.setHasArrow(0);
|
||||||
tab.menu_popover.as(gtk.Widget).setParent(anchor.as(gtk.Widget));
|
tab.menu_popover.as(gtk.Widget).setParent(anchor.as(gtk.Widget));
|
||||||
|
|
||||||
@@ -632,6 +637,53 @@ fn buildRowMenu(self: *Window, tab: *Tab, anchor: *gtk.Box) void {
|
|||||||
anchor.as(gtk.Widget).addController(secondary.as(gtk.EventController));
|
anchor.as(gtk.Widget).addController(secondary.as(gtk.EventController));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build the menu's contents, immediately before it opens.
|
||||||
|
///
|
||||||
|
/// Rebuilt per opening rather than built once and switched about, because half
|
||||||
|
/// of what it says is a deadline that has been quietly passing while the popover
|
||||||
|
/// sat there unopened. There is no moment other than "now" at which "muted for
|
||||||
|
/// another forty minutes" can be made true.
|
||||||
|
fn fillRowMenu(tab: *Tab) void {
|
||||||
|
const box = gtk.Box.new(.vertical, 2);
|
||||||
|
box.as(gtk.Widget).addCssClass("playpen-row-menu");
|
||||||
|
box.append(menuItem("Rename", &onMenuRename, tab));
|
||||||
|
box.append(menuItem("Settings…", &onMenuSettings, tab));
|
||||||
|
|
||||||
|
box.append(gtk.Separator.new(.horizontal).as(gtk.Widget));
|
||||||
|
|
||||||
|
// Cleared as the menu opens as well as when something wants to post, so a
|
||||||
|
// mute that ran out an hour ago isn't still described as one.
|
||||||
|
const now = notify.nowMs();
|
||||||
|
if (tab.mute.expired(now)) tab.mute = .off;
|
||||||
|
|
||||||
|
var buf: [80]u8 = undefined;
|
||||||
|
if (tab.mute.describe(now, &buf)) |state| {
|
||||||
|
// A label rather than an insensitive menu item: this is the answer to
|
||||||
|
// "why has this tab gone quiet", and there is nothing to click.
|
||||||
|
const label = gtk.Label.new(state);
|
||||||
|
label.setXalign(0);
|
||||||
|
label.as(gtk.Widget).addCssClass("playpen-row-menu-state");
|
||||||
|
box.append(label.as(gtk.Widget));
|
||||||
|
|
||||||
|
box.append(menuItem("Unmute", &onMenuUnmute, tab));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offered whether or not the tab is already muted: re-picking is how you
|
||||||
|
// change your mind about how long, and having to unmute first to mute again
|
||||||
|
// for longer would be a menu arguing with you.
|
||||||
|
inline for (std.enums.values(notify.Duration)) |duration| {
|
||||||
|
const Item = struct {
|
||||||
|
fn clicked(_: *gtk.Button, clicked_tab: *Tab) callconv(.c) void {
|
||||||
|
clicked_tab.menu_popover.popdown();
|
||||||
|
clicked_tab.mute = duration.mute(notify.nowMs());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
box.append(menuItem(duration.label(), &Item.clicked, tab));
|
||||||
|
}
|
||||||
|
|
||||||
|
tab.menu_popover.setChild(box.as(gtk.Widget));
|
||||||
|
}
|
||||||
|
|
||||||
/// One line of the row menu, styled like the layout menu's rows so the two
|
/// One line of the row menu, styled like the layout menu's rows so the two
|
||||||
/// popovers read as the same kind of thing.
|
/// popovers read as the same kind of thing.
|
||||||
fn menuItem(
|
fn menuItem(
|
||||||
@@ -647,7 +699,7 @@ fn menuItem(
|
|||||||
return button.as(gtk.Widget);
|
return button.as(gtk.Widget);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open the menu where the pointer is, rather than centred on the row: with one
|
/// Open the menu where the pointer is, rather than centered on the row: with one
|
||||||
/// popover per row anchored to the whole row, a fixed position would put the
|
/// popover per row anchored to the whole row, a fixed position would put the
|
||||||
/// menu somewhere you weren't pointing.
|
/// menu somewhere you weren't pointing.
|
||||||
fn onRowSecondary(
|
fn onRowSecondary(
|
||||||
@@ -663,6 +715,7 @@ fn onRowSecondary(
|
|||||||
.f_width = 1,
|
.f_width = 1,
|
||||||
.f_height = 1,
|
.f_height = 1,
|
||||||
};
|
};
|
||||||
|
fillRowMenu(tab);
|
||||||
tab.menu_popover.setPointingTo(&at);
|
tab.menu_popover.setPointingTo(&at);
|
||||||
tab.menu_popover.popup();
|
tab.menu_popover.popup();
|
||||||
}
|
}
|
||||||
@@ -677,6 +730,11 @@ fn onMenuSettings(_: *gtk.Button, tab: *Tab) callconv(.c) void {
|
|||||||
tab.window.openTabSettings(tab);
|
tab.window.openTabSettings(tab);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn onMenuUnmute(_: *gtk.Button, tab: *Tab) callconv(.c) void {
|
||||||
|
tab.menu_popover.popdown();
|
||||||
|
tab.mute = .off;
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Per-tab settings
|
// Per-tab settings
|
||||||
|
|
||||||
@@ -1290,12 +1348,16 @@ fn select(self: *Window, tab: *Tab) void {
|
|||||||
self.list.selectRow(tab.row);
|
self.list.selectRow(tab.row);
|
||||||
tab.view.focus();
|
tab.view.focus();
|
||||||
|
|
||||||
// Opening the tab is the acknowledgement the row was asking for, and the
|
// Opening the tab is the acknowledgment the row was asking for, and the
|
||||||
// pane you land in counts as answered along with it. Any other pane in a
|
// pane you land in counts as answered along with it. Any other pane in a
|
||||||
// split keeps its own dot until you go to it.
|
// split keeps its own dot until you go to it.
|
||||||
tab.finished_since_visit = false;
|
tab.finished_since_visit = false;
|
||||||
tab.view.answerFocused();
|
tab.view.answerFocused();
|
||||||
self.refreshStatus(tab);
|
self.refreshStatus(tab);
|
||||||
|
|
||||||
|
// And it answers the popup too, which would otherwise sit in the tray
|
||||||
|
// telling you about a tab you are now looking at.
|
||||||
|
if (self.application()) |app| notify.withdraw(app, tab.pageName());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn indexOf(self: *Window, tab: *Tab) ?usize {
|
fn indexOf(self: *Window, tab: *Tab) ?usize {
|
||||||
@@ -1416,7 +1478,7 @@ fn onRowDragPrepare(
|
|||||||
|
|
||||||
// A lone tab has nothing to be reordered against, so there is no preview to
|
// A lone tab has nothing to be reordered against, so there is no preview to
|
||||||
// show and no drop that could change anything. Refusing the drag is better
|
// show and no drop that could change anything. Refusing the drag is better
|
||||||
// than starting one that can only ever be cancelled.
|
// than starting one that can only ever be canceled.
|
||||||
if (self.tabs.items.len < 2) return null;
|
if (self.tabs.items.len < 2) return null;
|
||||||
|
|
||||||
self.drag = .{ .tab = tab, .origin = self.indexOf(tab) orelse return null };
|
self.drag = .{ .tab = tab, .origin = self.indexOf(tab) orelse return null };
|
||||||
@@ -1694,7 +1756,7 @@ fn onSettingsClicked(_: *gtk.Button, self: *Window) callconv(.c) void {
|
|||||||
self.openSettings();
|
self.openSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The colour scheme changed. Everything styled by CSS restyles itself; the
|
/// The color scheme changed. Everything styled by CSS restyles itself; the
|
||||||
/// terminal grids do not, because Cairo draws them from `theme.zig` and GTK
|
/// terminal grids do not, because Cairo draws them from `theme.zig` and GTK
|
||||||
/// has no idea that widget's contents depend on the palette at all. Without
|
/// has no idea that widget's contents depend on the palette at all. Without
|
||||||
/// this, switching to light leaves every terminal a dark rectangle until
|
/// this, switching to light leaves every terminal a dark rectangle until
|
||||||
@@ -1702,7 +1764,7 @@ fn onSettingsClicked(_: *gtk.Button, self: *Window) callconv(.c) void {
|
|||||||
///
|
///
|
||||||
/// Sessions already open are re-palletted rather than left on the one they
|
/// Sessions already open are re-palletted rather than left on the one they
|
||||||
/// started in: a shell you have had running all day is exactly the one you are
|
/// started in: a shell you have had running all day is exactly the one you are
|
||||||
/// looking at when you switch, and leaving it in the old scheme's colours
|
/// looking at when you switch, and leaving it in the old scheme's colors
|
||||||
/// would make the setting look like it only applies to new tabs.
|
/// would make the setting look like it only applies to new tabs.
|
||||||
fn onAppearanceChanged(ctx: ?*anyopaque) void {
|
fn onAppearanceChanged(ctx: ?*anyopaque) void {
|
||||||
const self: *Window = @ptrCast(@alignCast(ctx.?));
|
const self: *Window = @ptrCast(@alignCast(ctx.?));
|
||||||
@@ -1810,10 +1872,54 @@ fn onViewStatus(ctx: ?*anyopaque) void {
|
|||||||
/// Recorded even when the tab is the one on screen: you may well have watched
|
/// Recorded even when the tab is the one on screen: you may well have watched
|
||||||
/// it stop and then gone somewhere else, and the next time you open this tab is
|
/// it stop and then gone somewhere else, and the next time you open this tab is
|
||||||
/// when that stops being news.
|
/// when that stops being news.
|
||||||
fn onViewFinished(ctx: ?*anyopaque) void {
|
fn onViewFinished(ctx: ?*anyopaque, task: []const u8) void {
|
||||||
const tab: *Tab = @ptrCast(@alignCast(ctx.?));
|
const tab: *Tab = @ptrCast(@alignCast(ctx.?));
|
||||||
tab.finished_since_visit = true;
|
tab.finished_since_visit = true;
|
||||||
|
|
||||||
|
// The row first, always, and the popup second, maybe: the dot is the half
|
||||||
|
// that is never wrong and never unwanted.
|
||||||
tab.window.refreshStatus(tab);
|
tab.window.refreshStatus(tab);
|
||||||
|
tab.window.notifyFinished(tab, task);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tell the desktop that a pane finished, unless something says not to.
|
||||||
|
///
|
||||||
|
/// See `notify.wanted` for the three things that can say not to.
|
||||||
|
fn notifyFinished(self: *Window, tab: *Tab, task: []const u8) void {
|
||||||
|
// A timed mute is cleared here rather than on a timer. Nothing needs to
|
||||||
|
// know it has run out until something wants to post, and a GLib timeout per
|
||||||
|
// muted tab would be a great deal of machinery for a deadline nobody is
|
||||||
|
// watching.
|
||||||
|
const now = notify.nowMs();
|
||||||
|
if (tab.mute.expired(now)) tab.mute = .off;
|
||||||
|
|
||||||
|
// "You watched it happen" is the window having the focus and this being the
|
||||||
|
// tab it is showing. Everything the decision rests on is gathered here and
|
||||||
|
// weighed in `notify.wanted`, which is where it can be tested.
|
||||||
|
const watching = self.window.as(gtk.Window).isActive() != 0 and self.activeTab() == tab;
|
||||||
|
|
||||||
|
if (!notify.wanted(.{
|
||||||
|
.enabled = Settings.get().notifications,
|
||||||
|
.mute = tab.mute,
|
||||||
|
.watching = watching,
|
||||||
|
}, now)) return;
|
||||||
|
|
||||||
|
const app = self.application() orelse return;
|
||||||
|
|
||||||
|
var buf: [192]u8 = undefined;
|
||||||
|
notify.post(app, tab.pageName(), self.tabName(tab, &buf), task);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `GApplication` this window belongs to, which is what carries a
|
||||||
|
/// notification to the session.
|
||||||
|
///
|
||||||
|
/// Fetched each time rather than held. It is the same object for the life of the
|
||||||
|
/// process, but a window part-way through teardown has already been unparented
|
||||||
|
/// from it, and a session exiting during teardown is exactly when a finish can
|
||||||
|
/// still arrive.
|
||||||
|
fn application(self: *Window) ?*gio.Application {
|
||||||
|
const app = self.window.as(gtk.Window).getApplication() orelse return null;
|
||||||
|
return app.as(gio.Application);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The view lost its last pane, so the tab goes with it.
|
/// The view lost its last pane, so the tab goes with it.
|
||||||
@@ -2145,7 +2251,7 @@ fn addReviewPane(self: *Window, tab: *Tab) void {
|
|||||||
/// repositories reviews the one you are looking at.
|
/// repositories reviews the one you are looking at.
|
||||||
///
|
///
|
||||||
/// Falls back to playpen's own working directory, which at least gives the
|
/// Falls back to playpen's own working directory, which at least gives the
|
||||||
/// server something to fail on that the user can recognise in the message.
|
/// server something to fail on that the user can recognize in the message.
|
||||||
fn tabDirectory(self: *Window, tab: *Tab, buf: []u8) []const u8 {
|
fn tabDirectory(self: *Window, tab: *Tab, buf: []u8) []const u8 {
|
||||||
_ = self;
|
_ = self;
|
||||||
|
|
||||||
@@ -2160,7 +2266,7 @@ fn tabDirectory(self: *Window, tab: *Tab, buf: []u8) []const u8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Playpen's own directory, which at least gives the server something to
|
// Playpen's own directory, which at least gives the server something to
|
||||||
// fail on that the user can recognise in the message. `std.c` rather than
|
// fail on that the user can recognize in the message. `std.c` rather than
|
||||||
// `std.posix`, matching `Pty.zig`: the latter has been churning across Zig
|
// `std.posix`, matching `Pty.zig`: the latter has been churning across Zig
|
||||||
// releases and this is one call.
|
// releases and this is one call.
|
||||||
if (std.c.getcwd(buf.ptr, buf.len) != null) {
|
if (std.c.getcwd(buf.ptr, buf.len) != null) {
|
||||||
|
|||||||
+10
-10
@@ -1,14 +1,14 @@
|
|||||||
//! Which palette the app is painted with, and the plumbing that keeps three
|
//! Which palette the app is painted with, and the plumbing that keeps three
|
||||||
//! separate colour systems agreeing about it.
|
//! separate color systems agreeing about it.
|
||||||
//!
|
//!
|
||||||
//! * **libadwaita's style manager** colours every stock widget — popovers,
|
//! * **libadwaita's style manager** colors every stock widget — popovers,
|
||||||
//! entries, scrollbars, dialog chrome. It is told to force light or dark, or
|
//! entries, scrollbars, dialog chrome. It is told to force light or dark, or
|
||||||
//! left on `default` so it follows the desktop.
|
//! left on `default` so it follows the desktop.
|
||||||
//! * **`style.css`** colours everything the app draws for itself. It is
|
//! * **`style.css`** colors everything the app draws for itself. It is
|
||||||
//! written entirely against named colours; `palette.zig` supplies their
|
//! written entirely against named colors; `palette.zig` supplies their
|
||||||
//! values for the scheme in force, and the two are loaded as a single
|
//! values for the scheme in force, and the two are loaded as a single
|
||||||
//! provider.
|
//! provider.
|
||||||
//! * **`theme.zig`** holds the colours the terminal renderer reads. Cairo never
|
//! * **`theme.zig`** holds the colors the terminal renderer reads. Cairo never
|
||||||
//! consults the style tree, so a CSS reload on its own would leave every
|
//! consults the style tree, so a CSS reload on its own would leave every
|
||||||
//! terminal grid painted in the palette it started with.
|
//! terminal grid painted in the palette it started with.
|
||||||
//!
|
//!
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
//! that property means a desktop that changes scheme at sunset repaints this
|
//! that property means a desktop that changes scheme at sunset repaints this
|
||||||
//! app too, with no extra machinery and no second source of truth.
|
//! app too, with no extra machinery and no second source of truth.
|
||||||
//!
|
//!
|
||||||
//! Editing a colour comes in through `refresh`, which is the same repaint
|
//! Editing a color comes in through `refresh`, which is the same repaint
|
||||||
//! without the trip through libadwaita: the scheme hasn't changed, only what it
|
//! without the trip through libadwaita: the scheme hasn't changed, only what it
|
||||||
//! is painted in.
|
//! is painted in.
|
||||||
|
|
||||||
@@ -33,14 +33,14 @@ const Settings = @import("Settings.zig");
|
|||||||
const palette = @import("palette.zig");
|
const palette = @import("palette.zig");
|
||||||
const theme = @import("theme.zig");
|
const theme = @import("theme.zig");
|
||||||
|
|
||||||
/// The stylesheet, which is the same in both schemes: every colour in it is a
|
/// The stylesheet, which is the same in both schemes: every color in it is a
|
||||||
/// name, and the names are defined by the block `palette.zig` writes ahead of
|
/// name, and the names are defined by the block `palette.zig` writes ahead of
|
||||||
/// it.
|
/// it.
|
||||||
const style = @embedFile("style.css");
|
const style = @embedFile("style.css");
|
||||||
|
|
||||||
/// The two are handed to GTK as one string, so they live in one buffer. Static
|
/// The two are handed to GTK as one string, so they live in one buffer. Static
|
||||||
/// rather than allocated: this is written on every scheme change and on every
|
/// rather than allocated: this is written on every scheme change and on every
|
||||||
/// colour edited, and a repaint that can fail for want of memory would be a
|
/// color edited, and a repaint that can fail for want of memory would be a
|
||||||
/// stylesheet that half applies.
|
/// stylesheet that half applies.
|
||||||
var css_buf: [palette.css_size + style.len + 1]u8 = undefined;
|
var css_buf: [palette.css_size + style.len + 1]u8 = undefined;
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ fn buildCss(scheme: palette.Scheme) [:0]const u8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Called after the scheme changes, so the owner can repaint the things GTK
|
/// Called after the scheme changes, so the owner can repaint the things GTK
|
||||||
/// doesn't know are colour-dependent — the Cairo-drawn terminal grids.
|
/// doesn't know are color-dependent — the Cairo-drawn terminal grids.
|
||||||
pub const Callback = *const fn (ctx: ?*anyopaque) void;
|
pub const Callback = *const fn (ctx: ?*anyopaque) void;
|
||||||
|
|
||||||
var provider: ?*gtk.CssProvider = null;
|
var provider: ?*gtk.CssProvider = null;
|
||||||
@@ -141,7 +141,7 @@ fn onDarkChanged(
|
|||||||
|
|
||||||
/// Repaint in the palette as it now stands, without touching the preference.
|
/// Repaint in the palette as it now stands, without touching the preference.
|
||||||
///
|
///
|
||||||
/// This is what the colour editor calls after changing a colour: the scheme is
|
/// This is what the color editor calls after changing a color: the scheme is
|
||||||
/// whatever it already was, and everything painted from it — the stylesheet, the
|
/// whatever it already was, and everything painted from it — the stylesheet, the
|
||||||
/// terminal grids — is rebuilt from the new value.
|
/// terminal grids — is rebuilt from the new value.
|
||||||
pub fn refresh() void {
|
pub fn refresh() void {
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
//! Unicode lists nearly twice this many fully-qualified sequences and the
|
//! Unicode lists nearly twice this many fully-qualified sequences and the
|
||||||
//! difference is almost entirely the same gesture in five tones, which makes a
|
//! difference is almost entirely the same gesture in five tones, which makes a
|
||||||
//! grid longer to look through without making it more complete. The version
|
//! grid longer to look through without making it more complete. The version
|
||||||
//! cutoff is there because colour emoji fonts trail Unicode, and a glyph the
|
//! cutoff is there because color emoji fonts trail Unicode, and a glyph the
|
||||||
//! font has never heard of draws as a hex-digit box; the generator's header
|
//! font has never heard of draws as a hex-digit box; the generator's header
|
||||||
//! explains how to raise it.
|
//! explains how to raise it.
|
||||||
//!
|
//!
|
||||||
|
|||||||
+336
@@ -0,0 +1,336 @@
|
|||||||
|
//! Desktop notifications when a pane finishes, and the two ways of not getting
|
||||||
|
//! one.
|
||||||
|
//!
|
||||||
|
//! This exists for the window you are not looking at. A pane that finishes
|
||||||
|
//! already lights its sidebar row and its dot — see `Pane.Attention` — but a row
|
||||||
|
//! is only news if the window is in front of you, and Playpen is a window you
|
||||||
|
//! set an agent going in and then leave. The finish worth telling someone about
|
||||||
|
//! is the one that lands while the window is behind an editor or on another
|
||||||
|
//! workspace, which is precisely the case the dots cannot reach.
|
||||||
|
//!
|
||||||
|
//! Sent as a `GNotification` rather than by calling
|
||||||
|
//! `org.freedesktop.Notifications` ourselves. GIO picks whichever backend the
|
||||||
|
//! session actually has — the portal, GNOME's own interface, or the freedesktop
|
||||||
|
//! one — and sends under the application id, which is what gets the popup
|
||||||
|
//! Playpen's name and icon from the installed desktop entry.
|
||||||
|
//!
|
||||||
|
//! Nothing is attached to a click. The app is deliberately non-unique (see
|
||||||
|
//! `main.zig`), so no process owns `dev.greyson.playpen` on the session bus and
|
||||||
|
//! a daemon trying to call an action back would find nobody home. A popup that
|
||||||
|
//! looks clickable and does nothing is worse than one that plainly just tells
|
||||||
|
//! you something, so no default action is set: clicking closes it. Getting back
|
||||||
|
//! to the window is the compositor's job and it already has a binding for it.
|
||||||
|
//!
|
||||||
|
//! Two mutes, and they compose. The setting (Settings → Notifications) is the
|
||||||
|
//! whole app, for when you want to be left alone for an afternoon; a tab can
|
||||||
|
//! also be muted on its own from its row menu, for a while or until you say
|
||||||
|
//! otherwise, for the one tab whose agent finishes every ninety seconds.
|
||||||
|
//! Neither touches the dots — muting is about the popups, and the row is still
|
||||||
|
//! how you find out what happened while you were away.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const gio = @import("gio");
|
||||||
|
const glib = @import("glib");
|
||||||
|
|
||||||
|
/// The clock a mute's deadline is measured against, in milliseconds.
|
||||||
|
///
|
||||||
|
/// GLib's monotonic clock rather than the wall clock. A mute is a deadline a few
|
||||||
|
/// minutes out, and the wall clock can move under it — a suspend, an NTP step,
|
||||||
|
/// somebody's timezone — in ways that would either free a muted tab hours early
|
||||||
|
/// or leave it silent for the rest of the day. Milliseconds because nothing here
|
||||||
|
/// is finer grained than a minute.
|
||||||
|
pub fn nowMs() i64 {
|
||||||
|
return @divTrunc(glib.getMonotonicTime(), std.time.us_per_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How much of a title and a body we pass on.
|
||||||
|
///
|
||||||
|
/// Both come from a terminal, so both can be any length at all, and every
|
||||||
|
/// notification daemon truncates to what its popup can show anyway. Clamping
|
||||||
|
/// here is about the buffer these are copied into to be NUL-terminated for GIO,
|
||||||
|
/// not about the layout of anyone's popup.
|
||||||
|
const title_max = 96;
|
||||||
|
const body_max = 240;
|
||||||
|
|
||||||
|
/// Whether a tab's notifications are switched off, and until when.
|
||||||
|
///
|
||||||
|
/// A timestamp rather than a countdown because nothing here runs on a timer: a
|
||||||
|
/// mute is checked at the only moment it matters, which is when something is
|
||||||
|
/// about to be posted. A `.until` in the past is a mute that has quietly run
|
||||||
|
/// out, and `expired` is how a caller notices and puts the tab back to `.off`.
|
||||||
|
pub const Mute = union(enum) {
|
||||||
|
/// Notifications reach this tab.
|
||||||
|
off,
|
||||||
|
|
||||||
|
/// Silent until this instant, in milliseconds since the epoch.
|
||||||
|
until: i64,
|
||||||
|
|
||||||
|
/// Silent until someone says otherwise.
|
||||||
|
forever,
|
||||||
|
|
||||||
|
/// Whether a notification posted at `now` should be dropped.
|
||||||
|
pub fn silent(self: Mute, now: i64) bool {
|
||||||
|
return switch (self) {
|
||||||
|
.off => false,
|
||||||
|
.until => |deadline| now < deadline,
|
||||||
|
.forever => true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this is a timed mute whose time is up, so the caller can put the
|
||||||
|
/// tab back to `.off` and stop describing it as muted.
|
||||||
|
pub fn expired(self: Mute, now: i64) bool {
|
||||||
|
return switch (self) {
|
||||||
|
.until => |deadline| now >= deadline,
|
||||||
|
.off, .forever => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the row menu says about the mute above the choices, or null when
|
||||||
|
/// the tab isn't muted and the menu should say nothing at all. Terminated,
|
||||||
|
/// since its one caller hands it straight to a `GtkLabel`.
|
||||||
|
///
|
||||||
|
/// Time remaining rather than a wall-clock deadline: "another 40 minutes" is
|
||||||
|
/// the question someone reopening this menu is asking, and it needs no
|
||||||
|
/// timezone, which the alternative would.
|
||||||
|
pub fn describe(self: Mute, now: i64, buf: []u8) ?[:0]const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.off => null,
|
||||||
|
.forever => "Muted until you unmute it",
|
||||||
|
.until => |deadline| blk: {
|
||||||
|
const left_min = @divTrunc(deadline - now + 59_999, std.time.ms_per_min);
|
||||||
|
if (left_min <= 1) break :blk "Muted for another minute";
|
||||||
|
if (left_min < 60) break :blk std.fmt.bufPrintZ(
|
||||||
|
buf,
|
||||||
|
"Muted for another {d} minutes",
|
||||||
|
.{left_min},
|
||||||
|
) catch "Muted";
|
||||||
|
|
||||||
|
// Rounded down and in hours past the first: someone who muted a
|
||||||
|
// tab for eight hours wants to know it is still most of the
|
||||||
|
// afternoon, not that it is 437 minutes.
|
||||||
|
const left_hours = @divTrunc(left_min, 60);
|
||||||
|
break :blk std.fmt.bufPrintZ(
|
||||||
|
buf,
|
||||||
|
"Muted for another {d} hours",
|
||||||
|
.{left_hours},
|
||||||
|
) catch "Muted";
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The mutes the row menu offers, in the order it lists them.
|
||||||
|
///
|
||||||
|
/// A table rather than four handlers written out, so the menu and the durations
|
||||||
|
/// it can produce cannot drift apart: adding a choice here adds a line to every
|
||||||
|
/// row menu.
|
||||||
|
pub const Duration = enum {
|
||||||
|
fifteen_minutes,
|
||||||
|
one_hour,
|
||||||
|
eight_hours,
|
||||||
|
indefinitely,
|
||||||
|
|
||||||
|
/// Minutes this silences a tab for. Null is `indefinitely`, which has no
|
||||||
|
/// end and so is not a number of minutes.
|
||||||
|
fn minutes(self: Duration) ?i64 {
|
||||||
|
return switch (self) {
|
||||||
|
.fifteen_minutes => 15,
|
||||||
|
.one_hour => 60,
|
||||||
|
.eight_hours => 8 * 60,
|
||||||
|
.indefinitely => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The menu line. Phrased as the thing it does rather than as a bare
|
||||||
|
/// duration, since these sit in a list beside "Rename" and "Settings…".
|
||||||
|
pub fn label(self: Duration) [:0]const u8 {
|
||||||
|
return switch (self) {
|
||||||
|
.fifteen_minutes => "Mute for 15 minutes",
|
||||||
|
.one_hour => "Mute for an hour",
|
||||||
|
.eight_hours => "Mute for 8 hours",
|
||||||
|
.indefinitely => "Mute until I unmute it",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mute(self: Duration, now: i64) Mute {
|
||||||
|
const mins = self.minutes() orelse return .forever;
|
||||||
|
return .{ .until = now + mins * std.time.ms_per_min };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Everything that gets to decide whether a finish reaches the desktop.
|
||||||
|
pub const Decision = struct {
|
||||||
|
/// The app-wide setting: Settings → Notifications.
|
||||||
|
enabled: bool,
|
||||||
|
|
||||||
|
/// The tab's own mute, from its row menu.
|
||||||
|
mute: Mute,
|
||||||
|
|
||||||
|
/// Whether you watched it happen — the window has the focus and this is the
|
||||||
|
/// tab it is showing.
|
||||||
|
watching: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Whether a finish should be told to anyone.
|
||||||
|
///
|
||||||
|
/// Its own function, taking everything as arguments, because the interesting
|
||||||
|
/// half of notifications is the ones that *don't* arrive, and that half is
|
||||||
|
/// otherwise only observable by sitting in front of the app with a stopwatch.
|
||||||
|
/// The window gathers the three answers; this weighs them.
|
||||||
|
pub fn wanted(decision: Decision, now: i64) bool {
|
||||||
|
if (!decision.enabled) return false;
|
||||||
|
if (decision.mute.silent(now)) return false;
|
||||||
|
|
||||||
|
// The dot in the sidebar has already said it, and a popup about the pane you
|
||||||
|
// are sitting in front of is the fastest way to teach someone to switch the
|
||||||
|
// whole feature off.
|
||||||
|
return !decision.watching;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Post a notification, or fail quietly.
|
||||||
|
///
|
||||||
|
/// `id` replaces rather than stacks: a tab whose agent finishes three times
|
||||||
|
/// before you come back should leave one popup behind, not three, and passing
|
||||||
|
/// the same id each time is what the notification protocols mean by that. It is
|
||||||
|
/// also what `withdraw` takes the popup down by.
|
||||||
|
pub fn post(app: *gio.Application, id: [:0]const u8, title: []const u8, body: []const u8) void {
|
||||||
|
var title_buf: [title_max + 1]u8 = undefined;
|
||||||
|
const title_z = terminate(&title_buf, title, "Finished");
|
||||||
|
|
||||||
|
const notification = gio.Notification.new(title_z);
|
||||||
|
defer notification.unref();
|
||||||
|
|
||||||
|
if (body.len > 0) {
|
||||||
|
var body_buf: [body_max + 1]u8 = undefined;
|
||||||
|
notification.setBody(terminate(&body_buf, body, ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not `.high`: a finished task is something you want to know, not something
|
||||||
|
// that should survive a do-not-disturb or push past whatever else is on
|
||||||
|
// screen. The people most likely to run this are the people least likely to
|
||||||
|
// want an interruption.
|
||||||
|
notification.setPriority(.normal);
|
||||||
|
|
||||||
|
app.sendNotification(id, notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Take down whatever `post` left on screen for `id`, if the session's daemon
|
||||||
|
/// supports it. A no-op where it doesn't, and harmless where there is nothing
|
||||||
|
/// showing.
|
||||||
|
pub fn withdraw(app: *gio.Application, id: [:0]const u8) void {
|
||||||
|
app.withdrawNotification(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copy `text` into `buf` NUL-terminated, clamped to fit, falling back to
|
||||||
|
/// `empty` when there is nothing to copy.
|
||||||
|
///
|
||||||
|
/// The clamp walks back off a UTF-8 continuation byte rather than cutting where
|
||||||
|
/// the length happens to land. Tab titles here routinely end in an emoji — the
|
||||||
|
/// app has a picker for putting one on a row — and half a codepoint is a popup
|
||||||
|
/// that either shows a replacement box or gets dropped by the daemon for being
|
||||||
|
/// invalid UTF-8.
|
||||||
|
fn terminate(buf: []u8, text: []const u8, comptime empty: [:0]const u8) [:0]const u8 {
|
||||||
|
if (text.len == 0) return empty;
|
||||||
|
|
||||||
|
var n = @min(text.len, buf.len - 1);
|
||||||
|
while (n < text.len and n > 0 and text[n] & 0xC0 == 0x80) n -= 1;
|
||||||
|
|
||||||
|
@memcpy(buf[0..n], text[0..n]);
|
||||||
|
buf[n] = 0;
|
||||||
|
return buf[0..n :0];
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a mute that has not run out is silent, and then isn't" {
|
||||||
|
const now: i64 = 1_000_000;
|
||||||
|
const mute: Mute = .{ .until = now + 5 * std.time.ms_per_min };
|
||||||
|
|
||||||
|
try std.testing.expect(mute.silent(now));
|
||||||
|
try std.testing.expect(!mute.expired(now));
|
||||||
|
|
||||||
|
const later = now + 6 * std.time.ms_per_min;
|
||||||
|
try std.testing.expect(!mute.silent(later));
|
||||||
|
try std.testing.expect(mute.expired(later));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "off and forever never expire" {
|
||||||
|
const now: i64 = 1_000_000;
|
||||||
|
const off: Mute = .off;
|
||||||
|
const forever: Mute = .forever;
|
||||||
|
|
||||||
|
try std.testing.expect(!off.silent(now));
|
||||||
|
try std.testing.expect(!off.expired(now));
|
||||||
|
|
||||||
|
try std.testing.expect(forever.silent(now));
|
||||||
|
try std.testing.expect(!forever.expired(now));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "every duration produces a mute that is silent now" {
|
||||||
|
const now: i64 = 1_000_000;
|
||||||
|
for (std.enums.values(Duration)) |duration| {
|
||||||
|
try std.testing.expect(duration.mute(now).silent(now));
|
||||||
|
try std.testing.expect(duration.label().len > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a timed mute reads back as time remaining" {
|
||||||
|
const now: i64 = 1_000_000;
|
||||||
|
var buf: [64]u8 = undefined;
|
||||||
|
|
||||||
|
const off: Mute = .off;
|
||||||
|
const forever: Mute = .forever;
|
||||||
|
try std.testing.expect(off.describe(now, &buf) == null);
|
||||||
|
try std.testing.expectEqualStrings("Muted until you unmute it", forever.describe(now, &buf).?);
|
||||||
|
|
||||||
|
const quarter = Duration.fifteen_minutes.mute(now);
|
||||||
|
try std.testing.expectEqualStrings("Muted for another 15 minutes", quarter.describe(now, &buf).?);
|
||||||
|
|
||||||
|
const day = Duration.eight_hours.mute(now);
|
||||||
|
try std.testing.expectEqualStrings("Muted for another 8 hours", day.describe(now, &buf).?);
|
||||||
|
|
||||||
|
// The last minute rounds up to a minute rather than down to none: a mute
|
||||||
|
// with thirty seconds left is still a mute, and "another 0 minutes" reads
|
||||||
|
// like a bug.
|
||||||
|
const nearly = Mute{ .until = now + 30 * std.time.ms_per_s };
|
||||||
|
try std.testing.expectEqualStrings("Muted for another minute", nearly.describe(now, &buf).?);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "nothing is posted for a tab you are looking at" {
|
||||||
|
const now: i64 = 1_000_000;
|
||||||
|
const watched: Decision = .{ .enabled = true, .mute = .off, .watching = true };
|
||||||
|
const elsewhere: Decision = .{ .enabled = true, .mute = .off, .watching = false };
|
||||||
|
|
||||||
|
try std.testing.expect(!wanted(watched, now));
|
||||||
|
try std.testing.expect(wanted(elsewhere, now));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "either mute is enough to stop a post" {
|
||||||
|
const now: i64 = 1_000_000;
|
||||||
|
|
||||||
|
// The setting off, everything else willing.
|
||||||
|
try std.testing.expect(!wanted(.{ .enabled = false, .mute = .off, .watching = false }, now));
|
||||||
|
|
||||||
|
// The setting on, this one tab muted — for a while, and then not.
|
||||||
|
const timed = Duration.one_hour.mute(now);
|
||||||
|
try std.testing.expect(!wanted(.{ .enabled = true, .mute = timed, .watching = false }, now));
|
||||||
|
try std.testing.expect(wanted(
|
||||||
|
.{ .enabled = true, .mute = timed, .watching = false },
|
||||||
|
now + 61 * std.time.ms_per_min,
|
||||||
|
));
|
||||||
|
|
||||||
|
// And indefinitely means indefinitely.
|
||||||
|
try std.testing.expect(!wanted(
|
||||||
|
.{ .enabled = true, .mute = .forever, .watching = false },
|
||||||
|
now + 400 * std.time.ms_per_day,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "a title is clamped without splitting a codepoint" {
|
||||||
|
var buf: [8]u8 = undefined;
|
||||||
|
|
||||||
|
// Seven bytes of room and a three-byte glyph at the boundary: the glyph goes
|
||||||
|
// whole rather than being cut in the middle.
|
||||||
|
try std.testing.expectEqualStrings("abcde", terminate(&buf, "abcde🙂", ""));
|
||||||
|
try std.testing.expectEqualStrings("abc🙂", terminate(&buf, "abc🙂", ""));
|
||||||
|
try std.testing.expectEqualStrings("(none)", terminate(&buf, "", "(none)"));
|
||||||
|
}
|
||||||
+20
-20
@@ -1,6 +1,6 @@
|
|||||||
//! sRGB in, sRGB out, with a perceptual space in the middle.
|
//! sRGB in, sRGB out, with a perceptual space in the middle.
|
||||||
//!
|
//!
|
||||||
//! This exists for `tint.zig`, which builds a whole palette out of one colour
|
//! This exists for `tint.zig`, which builds a whole palette out of one color
|
||||||
//! and therefore has to answer questions like "the same hue, a third as bright"
|
//! and therefore has to answer questions like "the same hue, a third as bright"
|
||||||
//! and "one step lighter than that". Those questions have no good answer in RGB
|
//! and "one step lighter than that". Those questions have no good answer in RGB
|
||||||
//! — halving the channels of a mid blue gives a navy, halving the channels of a
|
//! — halving the channels of a mid blue gives a navy, halving the channels of a
|
||||||
@@ -14,10 +14,10 @@
|
|||||||
//! polar form — lightness, chroma, hue — is what a palette is really made of:
|
//! polar form — lightness, chroma, hue — is what a palette is really made of:
|
||||||
//! hold the hue, walk the lightness, and the surfaces of a theme fall out.
|
//! hold the hue, walk the lightness, and the surfaces of a theme fall out.
|
||||||
//!
|
//!
|
||||||
//! The one thing OKLCh will happily do that sRGB will not is name a colour that
|
//! The one thing OKLCh will happily do that sRGB will not is name a color that
|
||||||
//! does not exist on a monitor — a fully saturated yellow at the lightness of a
|
//! does not exist on a monitor — a fully saturated yellow at the lightness of a
|
||||||
//! midtone, say. `toRgb` deals with that by giving up chroma rather than
|
//! midtone, say. `toRgb` deals with that by giving up chroma rather than
|
||||||
//! lightness or hue: the result is the most colourful version of the colour that
|
//! lightness or hue: the result is the most colorful version of the color that
|
||||||
//! can actually be shown, which is what "as close as the screen gets" should
|
//! can actually be shown, which is what "as close as the screen gets" should
|
||||||
//! mean for a palette. Clipping the channels instead would shift the hue, and a
|
//! mean for a palette. Clipping the channels instead would shift the hue, and a
|
||||||
//! generated theme whose reds drift orange as they darken looks broken in a way
|
//! generated theme whose reds drift orange as they darken looks broken in a way
|
||||||
@@ -31,13 +31,13 @@ const palette = @import("palette.zig");
|
|||||||
|
|
||||||
const Rgb = palette.Rgb;
|
const Rgb = palette.Rgb;
|
||||||
|
|
||||||
/// A colour in OKLCh: how light, how colourful, and which colour.
|
/// A color in OKLCh: how light, how colorful, and which color.
|
||||||
pub const Lch = struct {
|
pub const Lch = struct {
|
||||||
/// Perceptual lightness. 0 is black, 1 is white, and 0.5 looks like a
|
/// Perceptual lightness. 0 is black, 1 is white, and 0.5 looks like a
|
||||||
/// midtone rather than merely being one arithmetically.
|
/// midtone rather than merely being one arithmetically.
|
||||||
l: f32,
|
l: f32,
|
||||||
|
|
||||||
/// Colourfulness. 0 is a grey; how high it can go before leaving sRGB
|
/// Colorfulness. 0 is a gray; how high it can go before leaving sRGB
|
||||||
/// depends on both the lightness and the hue, and is around 0.32 at best.
|
/// depends on both the lightness and the hue, and is around 0.32 at best.
|
||||||
c: f32,
|
c: f32,
|
||||||
|
|
||||||
@@ -59,17 +59,17 @@ pub fn fromRgb(rgb: Rgb) Lch {
|
|||||||
return .{
|
return .{
|
||||||
.l = lab.l,
|
.l = lab.l,
|
||||||
.c = c,
|
.c = c,
|
||||||
// A grey has no hue to report, and `atan2` on two zeroes is entitled to
|
// A gray has no hue to report, and `atan2` on two zeroes is entitled to
|
||||||
// say anything. Zero is as good as any other answer and is at least the
|
// say anything. Zero is as good as any other answer and is at least the
|
||||||
// same one every time, which matters: the palette generator reads a hue
|
// same one every time, which matters: the palette generator reads a hue
|
||||||
// off the base colour and gives it to forty other colours, and a grey
|
// off the base color and gives it to forty other colors, and a gray
|
||||||
// base that produced a different hue on each launch would be a theme
|
// base that produced a different hue on each launch would be a theme
|
||||||
// that changed colour when you restarted the app.
|
// that changed color when you restarted the app.
|
||||||
.h = if (c < 1e-6) 0 else std.math.radiansToDegrees(std.math.atan2(lab.b, lab.a)),
|
.h = if (c < 1e-6) 0 else std.math.radiansToDegrees(std.math.atan2(lab.b, lab.a)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The nearest colour a screen can show, giving up chroma before anything else.
|
/// The nearest color a screen can show, giving up chroma before anything else.
|
||||||
pub fn toRgb(lch: Lch) Rgb {
|
pub fn toRgb(lch: Lch) Rgb {
|
||||||
const l = std.math.clamp(lch.l, 0, 1);
|
const l = std.math.clamp(lch.l, 0, 1);
|
||||||
const rad = std.math.degreesToRadians(lch.h);
|
const rad = std.math.degreesToRadians(lch.h);
|
||||||
@@ -90,7 +90,7 @@ pub fn toRgb(lch: Lch) Rgb {
|
|||||||
/// is not one: it is the image of a cube through a cube root, and the closed
|
/// is not one: it is the image of a cube through a cube root, and the closed
|
||||||
/// forms for it are approximations with their own error. Twelve halvings of a
|
/// forms for it are approximations with their own error. Twelve halvings of a
|
||||||
/// range that is at most 1.0 wide lands well inside a 1/255 step, and this runs
|
/// range that is at most 1.0 wide lands well inside a 1/255 step, and this runs
|
||||||
/// forty times when someone drags a colour picker.
|
/// forty times when someone drags a color picker.
|
||||||
fn fit(l: f32, c: f32, rad: f32) f32 {
|
fn fit(l: f32, c: f32, rad: f32) f32 {
|
||||||
if (Lab.at(l, c, rad).inGamut()) return c;
|
if (Lab.at(l, c, rad).inGamut()) return c;
|
||||||
|
|
||||||
@@ -133,9 +133,9 @@ const Lab = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether this colour is one a screen can show. The tolerance is a hair
|
/// Whether this color is one a screen can show. The tolerance is a hair
|
||||||
/// over half of an 8-bit step in linear light near the top of the range, so
|
/// over half of an 8-bit step in linear light near the top of the range, so
|
||||||
/// a colour that is out of gamut only by the arithmetic isn't hunted down
|
/// a color that is out of gamut only by the arithmetic isn't hunted down
|
||||||
/// by the bisection above for no visible gain.
|
/// by the bisection above for no visible gain.
|
||||||
fn inGamut(self: Lab) bool {
|
fn inGamut(self: Lab) bool {
|
||||||
const lin = self.toLinear();
|
const lin = self.toLinear();
|
||||||
@@ -188,8 +188,8 @@ fn encode(channel: f32) u8 {
|
|||||||
|
|
||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
|
|
||||||
test "a colour survives the round trip" {
|
test "a color survives the round trip" {
|
||||||
// Every hue family, plus the two ends and a grey, since those are the cases
|
// Every hue family, plus the two ends and a gray, since those are the cases
|
||||||
// where the hue is undefined and the transfer function is at its steepest.
|
// where the hue is undefined and the transfer function is at its steepest.
|
||||||
const cases = [_][]const u8{
|
const cases = [_][]const u8{
|
||||||
"#000000", "#ffffff", "#808080", "#3a76f0", "#d2691e",
|
"#000000", "#ffffff", "#808080", "#3a76f0", "#d2691e",
|
||||||
@@ -220,14 +220,14 @@ test "black and white are where they should be" {
|
|||||||
try std.testing.expectApproxEqAbs(@as(f32, 0), fromRgb(.{ .r = 0, .g = 0, .b = 0 }).l, 1e-4);
|
try std.testing.expectApproxEqAbs(@as(f32, 0), fromRgb(.{ .r = 0, .g = 0, .b = 0 }).l, 1e-4);
|
||||||
try std.testing.expectApproxEqAbs(@as(f32, 1), fromRgb(.{ .r = 255, .g = 255, .b = 255 }).l, 1e-4);
|
try std.testing.expectApproxEqAbs(@as(f32, 1), fromRgb(.{ .r = 255, .g = 255, .b = 255 }).l, 1e-4);
|
||||||
|
|
||||||
// A grey has no hue, and says so rather than saying whatever `atan2` makes
|
// A gray has no hue, and says so rather than saying whatever `atan2` makes
|
||||||
// of two zeroes.
|
// of two zeroes.
|
||||||
const grey = fromRgb(.{ .r = 128, .g = 128, .b = 128 });
|
const gray = fromRgb(.{ .r = 128, .g = 128, .b = 128 });
|
||||||
try std.testing.expectApproxEqAbs(@as(f32, 0), grey.c, 1e-3);
|
try std.testing.expectApproxEqAbs(@as(f32, 0), gray.c, 1e-3);
|
||||||
try std.testing.expectEqual(@as(f32, 0), grey.h);
|
try std.testing.expectEqual(@as(f32, 0), gray.h);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "an impossible colour gives up chroma, not hue" {
|
test "an impossible color gives up chroma, not hue" {
|
||||||
// A fully saturated yellow at the lightness of a midtone: nothing like it
|
// A fully saturated yellow at the lightness of a midtone: nothing like it
|
||||||
// exists in sRGB, and asking for it has to produce *something*.
|
// exists in sRGB, and asking for it has to produce *something*.
|
||||||
const asked: Lch = .{ .l = 0.5, .c = 0.3, .h = 100 };
|
const asked: Lch = .{ .l = 0.5, .c = 0.3, .h = 100 };
|
||||||
@@ -237,7 +237,7 @@ test "an impossible colour gives up chroma, not hue" {
|
|||||||
try std.testing.expectApproxEqAbs(asked.h, got.h, 1.5);
|
try std.testing.expectApproxEqAbs(asked.h, got.h, 1.5);
|
||||||
try std.testing.expect(got.c < asked.c);
|
try std.testing.expect(got.c < asked.c);
|
||||||
|
|
||||||
// And it is still as colourful as sRGB allows, rather than having been
|
// And it is still as colorful as sRGB allows, rather than having been
|
||||||
// rounded down to something safe: pushing it back up leaves the gamut.
|
// rounded down to something safe: pushing it back up leaves the gamut.
|
||||||
try std.testing.expect(!Lab.at(asked.l, got.c + 0.01, std.math.degreesToRadians(asked.h)).inGamut());
|
try std.testing.expect(!Lab.at(asked.l, got.c + 0.01, std.math.degreesToRadians(asked.h)).inGamut());
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-53
@@ -1,9 +1,9 @@
|
|||||||
//! The palette: every colour the app paints itself with, what each one defaults
|
//! The palette: every color the app paints itself with, what each one defaults
|
||||||
//! to in either scheme, and the shape an override takes when someone changes it.
|
//! to in either scheme, and the shape an override takes when someone changes it.
|
||||||
//!
|
//!
|
||||||
//! This used to be two stylesheets — `palette-dark.css` and
|
//! This used to be two stylesheets — `palette-dark.css` and
|
||||||
//! `palette-light.css`, one of them prepended to `style.css` at compile time —
|
//! `palette-light.css`, one of them prepended to `style.css` at compile time —
|
||||||
//! and that was the right shape right up until the settings page grew a colour
|
//! and that was the right shape right up until the settings page grew a color
|
||||||
//! editor. An editor needs the *names*, to list them; the *defaults*, to say
|
//! editor. An editor needs the *names*, to list them; the *defaults*, to say
|
||||||
//! which ones have been changed and to put them back; and some *grouping*, so
|
//! which ones have been changed and to put them back; and some *grouping*, so
|
||||||
//! that the list reads as a palette rather than as forty hex fields. None of
|
//! that the list reads as a palette rather than as forty hex fields. None of
|
||||||
@@ -12,8 +12,8 @@
|
|||||||
//!
|
//!
|
||||||
//! So the CSS is generated from here instead: `writeCss` emits one
|
//! So the CSS is generated from here instead: `writeCss` emits one
|
||||||
//! `@define-color` per name, `appearance.zig` prepends the result to
|
//! `@define-color` per name, `appearance.zig` prepends the result to
|
||||||
//! `style.css`, and `theme.zig` resolves the terminal renderer's colours out of
|
//! `style.css`, and `theme.zig` resolves the terminal renderer's colors out of
|
||||||
//! the same table. There is still exactly one place a colour is written down.
|
//! the same table. There is still exactly one place a color is written down.
|
||||||
//!
|
//!
|
||||||
//! Overrides are held per scheme and only where they exist. That is what makes
|
//! Overrides are held per scheme and only where they exist. That is what makes
|
||||||
//! "reset" and "you have changed this one" answerable at all, and it means a
|
//! "reset" and "you have changed this one" answerable at all, and it means a
|
||||||
@@ -21,10 +21,10 @@
|
|||||||
//! touched it, rather than only the people who had never opened the editor.
|
//! touched it, rather than only the people who had never opened the editor.
|
||||||
//!
|
//!
|
||||||
//! Under the overrides there is one more layer: a `Tint`, which is a single
|
//! Under the overrides there is one more layer: a `Tint`, which is a single
|
||||||
//! colour that `tint.zig` builds an entire palette out of. It stands in for the
|
//! color that `tint.zig` builds an entire palette out of. It stands in for the
|
||||||
//! defaults rather than beside them, so the three layers read outward from the
|
//! defaults rather than beside them, so the three layers read outward from the
|
||||||
//! most specific — this colour, then the colour built from your base, then the
|
//! most specific — this color, then the color built from your base, then the
|
||||||
//! colour Playpen ships. That ordering is what lets someone pick a base and then
|
//! color Playpen ships. That ordering is what lets someone pick a base and then
|
||||||
//! fix the two swatches they didn't like without the fix being undone the next
|
//! fix the two swatches they didn't like without the fix being undone the next
|
||||||
//! time they nudge the base, and it is why `Changes` is a struct rather than the
|
//! time they nudge the base, and it is why `Changes` is a struct rather than the
|
||||||
//! bare override array it used to be.
|
//! bare override array it used to be.
|
||||||
@@ -89,7 +89,7 @@ pub const Rgb = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A resolved colour scheme. Not the same thing as the user's preference, which
|
/// A resolved color scheme. Not the same thing as the user's preference, which
|
||||||
/// has a third option — see `Settings.Theme`. By the time a scheme reaches this
|
/// has a third option — see `Settings.Theme`. By the time a scheme reaches this
|
||||||
/// module, "system" has been resolved to one of these.
|
/// module, "system" has been resolved to one of these.
|
||||||
pub const Scheme = enum {
|
pub const Scheme = enum {
|
||||||
@@ -116,7 +116,7 @@ pub const Group = enum {
|
|||||||
terminal,
|
terminal,
|
||||||
ansi,
|
ansi,
|
||||||
|
|
||||||
/// Whether this group's colours reach the UI as named CSS colours. The two
|
/// Whether this group's colors reach the UI as named CSS colors. The two
|
||||||
/// that don't are the terminal's: Cairo draws the grid itself and never
|
/// that don't are the terminal's: Cairo draws the grid itself and never
|
||||||
/// consults the style tree.
|
/// consults the style tree.
|
||||||
pub fn isCss(self: Group) bool {
|
pub fn isCss(self: Group) bool {
|
||||||
@@ -134,7 +134,7 @@ pub const Group = enum {
|
|||||||
.accents => "Accent",
|
.accents => "Accent",
|
||||||
.states => "Status",
|
.states => "Status",
|
||||||
.terminal => "Terminal",
|
.terminal => "Terminal",
|
||||||
.ansi => "Terminal ANSI colours",
|
.ansi => "Terminal ANSI colors",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,17 +143,17 @@ pub const Group = enum {
|
|||||||
return switch (self) {
|
return switch (self) {
|
||||||
.surfaces => "Darkest first: the window backdrop shows through as the gutter between panes.",
|
.surfaces => "Darkest first: the window backdrop shows through as the gutter between panes.",
|
||||||
.terminal => "What a program in the terminal gets before it asks for anything else. " ++
|
.terminal => "What a program in the terminal gets before it asks for anything else. " ++
|
||||||
"Left alone, these follow the surface, text and accent colours above.",
|
"Left alone, these follow the surface, text and accent colors above.",
|
||||||
.ansi => "The 16 colours programs ask for by name. " ++
|
.ansi => "The 16 colors programs ask for by name. " ++
|
||||||
"The 240 above them are fixed by spec and not editable.",
|
"The 240 above them are fixed by spec and not editable.",
|
||||||
.text, .rows, .accents, .states => null,
|
.text, .rows, .accents, .states => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Every colour in the palette.
|
/// Every color in the palette.
|
||||||
///
|
///
|
||||||
/// The tag is the wire format — it is what a colour is called in
|
/// The tag is the wire format — it is what a color is called in
|
||||||
/// `settings.json` — and for everything in a CSS group it is also the name in
|
/// `settings.json` — and for everything in a CSS group it is also the name in
|
||||||
/// the stylesheet, with `pp_` in front. Renaming one is therefore a migration,
|
/// the stylesheet, with `pp_` in front. Renaming one is therefore a migration,
|
||||||
/// which is why the names here are the ones `style.css` already used.
|
/// which is why the names here are the ones `style.css` already used.
|
||||||
@@ -195,7 +195,7 @@ pub const Key = enum {
|
|||||||
term_selection_bg,
|
term_selection_bg,
|
||||||
term_selection_fg,
|
term_selection_fg,
|
||||||
|
|
||||||
// ---- the 16 named ANSI colours, in palette order ----
|
// ---- the 16 named ANSI colors, in palette order ----
|
||||||
ansi_black,
|
ansi_black,
|
||||||
ansi_red,
|
ansi_red,
|
||||||
ansi_green,
|
ansi_green,
|
||||||
@@ -232,7 +232,7 @@ pub const Key = enum {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The name `style.css` knows this colour by, for the groups that reach CSS
|
/// The name `style.css` knows this color by, for the groups that reach CSS
|
||||||
/// at all. Derived from the tag rather than written out again, so the two
|
/// at all. Derived from the tag rather than written out again, so the two
|
||||||
/// cannot drift.
|
/// cannot drift.
|
||||||
pub fn cssName(self: Key) ?[:0]const u8 {
|
pub fn cssName(self: Key) ?[:0]const u8 {
|
||||||
@@ -244,16 +244,16 @@ pub const Key = enum {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This colour's index in the 256-colour palette, for the 16 that have one.
|
/// This color's index in the 256-color palette, for the 16 that have one.
|
||||||
pub fn ansiIndex(self: Key) ?u8 {
|
pub fn ansiIndex(self: Key) ?u8 {
|
||||||
if (self.group() != .ansi) return null;
|
if (self.group() != .ansi) return null;
|
||||||
return @intCast(@intFromEnum(self) - @intFromEnum(Key.ansi_black));
|
return @intCast(@intFromEnum(self) - @intFromEnum(Key.ansi_black));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The colour this one falls back to when it hasn't been set.
|
/// The color this one falls back to when it hasn't been set.
|
||||||
///
|
///
|
||||||
/// This is what keeps the terminal and the frame around it looking like one
|
/// This is what keeps the terminal and the frame around it looking like one
|
||||||
/// surface without asking anyone to set the same colour twice. The
|
/// surface without asking anyone to set the same color twice. The
|
||||||
/// terminal's background *is* the pane surface, its foreground *is* the
|
/// terminal's background *is* the pane surface, its foreground *is* the
|
||||||
/// body text, and a selection *is* the muted accent — until someone says
|
/// body text, and a selection *is* the muted accent — until someone says
|
||||||
/// otherwise, at which point they come apart, which is the whole reason
|
/// otherwise, at which point they come apart, which is the whole reason
|
||||||
@@ -318,7 +318,7 @@ pub const Key = enum {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What this colour is for, for the ones where the name isn't the whole
|
/// What this color is for, for the ones where the name isn't the whole
|
||||||
/// story. Shown as the row's tooltip.
|
/// story. Shown as the row's tooltip.
|
||||||
pub fn hint(self: Key) ?[:0]const u8 {
|
pub fn hint(self: Key) ?[:0]const u8 {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
@@ -326,7 +326,7 @@ pub const Key = enum {
|
|||||||
.surface_raised => "The strip above a pane holding its title",
|
.surface_raised => "The strip above a pane holding its title",
|
||||||
.border_strong => "Edges that have to carry on their own, without a fill behind them",
|
.border_strong => "Edges that have to carry on their own, without a fill behind them",
|
||||||
.text_dim => "Secondary labels — hints, counts, addresses",
|
.text_dim => "Secondary labels — hints, counts, addresses",
|
||||||
.text_faint => "Things that are labelled rather than read, like the title of a pane you aren't in",
|
.text_faint => "Things that are labeled rather than read, like the title of a pane you aren't in",
|
||||||
.accent_strong => "Small marks that have to stay legible on a surface: icons, dots, a row's state bar",
|
.accent_strong => "Small marks that have to stay legible on a surface: icons, dots, a row's state bar",
|
||||||
.accent_muted => "Washes and the border of the focused pane",
|
.accent_muted => "Washes and the border of the focused pane",
|
||||||
.ok => "A pane that finished and hasn't been looked at",
|
.ok => "A pane that finished and hasn't been looked at",
|
||||||
@@ -338,7 +338,7 @@ pub const Key = enum {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The colour before any override: this key's own default, or the default of
|
/// The color before any override: this key's own default, or the default of
|
||||||
/// whatever it inherits from.
|
/// whatever it inherits from.
|
||||||
pub fn default(self: Key, scheme: Scheme) Rgb {
|
pub fn default(self: Key, scheme: Scheme) Rgb {
|
||||||
if (self.ansiIndex()) |i| return switch (scheme) {
|
if (self.ansiIndex()) |i| return switch (scheme) {
|
||||||
@@ -360,17 +360,17 @@ pub const Key = enum {
|
|||||||
|
|
||||||
pub const count = std.enums.values(Key).len;
|
pub const count = std.enums.values(Key).len;
|
||||||
|
|
||||||
/// The colours someone has changed, per key, in one scheme. A fixed array
|
/// The colors someone has changed, per key, in one scheme. A fixed array
|
||||||
/// rather than a list of pairs: it is 120 bytes, it needs no allocator, and it
|
/// rather than a list of pairs: it is 120 bytes, it needs no allocator, and it
|
||||||
/// makes "is this key overridden" a lookup rather than a search.
|
/// makes "is this key overridden" a lookup rather than a search.
|
||||||
pub const Overrides = std.enums.EnumArray(Key, ?Rgb);
|
pub const Overrides = std.enums.EnumArray(Key, ?Rgb);
|
||||||
|
|
||||||
pub const no_overrides: Overrides = .initFill(null);
|
pub const no_overrides: Overrides = .initFill(null);
|
||||||
|
|
||||||
/// One colour, and how far apart to spread everything built around it. See
|
/// One color, and how far apart to spread everything built around it. See
|
||||||
/// `tint.zig` for what "built around it" means.
|
/// `tint.zig` for what "built around it" means.
|
||||||
pub const Tint = struct {
|
pub const Tint = struct {
|
||||||
/// The colour the palette is generated from. It is also, near enough, what
|
/// The color the palette is generated from. It is also, near enough, what
|
||||||
/// `accent` comes out as — see `tint.derive`.
|
/// `accent` comes out as — see `tint.derive`.
|
||||||
base: Rgb,
|
base: Rgb,
|
||||||
|
|
||||||
@@ -384,8 +384,8 @@ pub const Tint = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Everything someone has done to one scheme's palette: the base colour it is
|
/// Everything someone has done to one scheme's palette: the base color it is
|
||||||
/// generated from, if any, and the individual colours they have set by hand.
|
/// generated from, if any, and the individual colors they have set by hand.
|
||||||
///
|
///
|
||||||
/// Both, and in that order, rather than either — see the note at the top of the
|
/// Both, and in that order, rather than either — see the note at the top of the
|
||||||
/// file. A base is a starting point and the overrides are the corrections to it,
|
/// file. A base is a starting point and the overrides are the corrections to it,
|
||||||
@@ -395,7 +395,7 @@ pub const Changes = struct {
|
|||||||
overrides: Overrides = no_overrides,
|
overrides: Overrides = no_overrides,
|
||||||
|
|
||||||
/// Whether anything in this scheme has been changed at all. What the
|
/// Whether anything in this scheme has been changed at all. What the
|
||||||
/// editor's "reset everything" button is greyed out by.
|
/// editor's "reset everything" button is grayed out by.
|
||||||
pub fn anySet(self: *const Changes) bool {
|
pub fn anySet(self: *const Changes) bool {
|
||||||
if (self.tint != null) return true;
|
if (self.tint != null) return true;
|
||||||
for (std.enums.values(Key)) |key| {
|
for (std.enums.values(Key)) |key| {
|
||||||
@@ -405,20 +405,20 @@ pub const Changes = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A scheme nobody has touched. Named so that the places that resolve a colour
|
/// A scheme nobody has touched. Named so that the places that resolve a color
|
||||||
/// without any settings to hand — the terminal renderer's compile-time initial
|
/// without any settings to hand — the terminal renderer's compile-time initial
|
||||||
/// palette, and the tests — have something to point at.
|
/// palette, and the tests — have something to point at.
|
||||||
pub const unchanged: Changes = .{};
|
pub const unchanged: Changes = .{};
|
||||||
|
|
||||||
/// The colour to paint with: the override if there is one, else what the key
|
/// The color to paint with: the override if there is one, else what the key
|
||||||
/// inherits (overrides and all), else the colour a base generates for it, else
|
/// inherits (overrides and all), else the color a base generates for it, else
|
||||||
/// the default.
|
/// the default.
|
||||||
///
|
///
|
||||||
/// The inheritance is resolved through the overrides rather than around them,
|
/// The inheritance is resolved through the overrides rather than around them,
|
||||||
/// which is the point of it — recolour `surface` and the terminal background
|
/// which is the point of it — recolor `surface` and the terminal background
|
||||||
/// follows, without anyone having had to set both. It is resolved *before* the
|
/// follows, without anyone having had to set both. It is resolved *before* the
|
||||||
/// generated palette for the same reason: a terminal background whose pane
|
/// generated palette for the same reason: a terminal background whose pane
|
||||||
/// surface has been overridden should follow the override, not the base colour
|
/// surface has been overridden should follow the override, not the base color
|
||||||
/// the override was correcting.
|
/// the override was correcting.
|
||||||
pub fn resolve(key: Key, scheme: Scheme, changes: *const Changes) Rgb {
|
pub fn resolve(key: Key, scheme: Scheme, changes: *const Changes) Rgb {
|
||||||
if (changes.overrides.get(key)) |color| return color;
|
if (changes.overrides.get(key)) |color| return color;
|
||||||
@@ -427,11 +427,11 @@ pub fn resolve(key: Key, scheme: Scheme, changes: *const Changes) Rgb {
|
|||||||
return key.default(scheme);
|
return key.default(scheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Emit the palette as named CSS colours, for `style.css` to be written
|
/// Emit the palette as named CSS colors, for `style.css` to be written
|
||||||
/// against.
|
/// against.
|
||||||
///
|
///
|
||||||
/// `appearance.zig` prepends this to the stylesheet and loads the pair as a
|
/// `appearance.zig` prepends this to the stylesheet and loads the pair as a
|
||||||
/// single provider. Only the CSS groups appear; the terminal's colours go to
|
/// single provider. Only the CSS groups appear; the terminal's colors go to
|
||||||
/// Cairo instead, through `theme.zig`.
|
/// Cairo instead, through `theme.zig`.
|
||||||
pub fn writeCss(
|
pub fn writeCss(
|
||||||
scheme: Scheme,
|
scheme: Scheme,
|
||||||
@@ -457,17 +457,17 @@ pub const css_size = count * ("@define-color pp_surface_raised_active #rrggbb;\n
|
|||||||
//
|
//
|
||||||
// Two tables, one per scheme, in the same shape and the same order as the two
|
// Two tables, one per scheme, in the same shape and the same order as the two
|
||||||
// CSS files they replace. A key that inherits (see `Key.inherits`) is absent
|
// CSS files they replace. A key that inherits (see `Key.inherits`) is absent
|
||||||
// from both rather than repeating the colour it would have copied.
|
// from both rather than repeating the color it would have copied.
|
||||||
|
|
||||||
const Table = std.enums.EnumArray(Key, ?Rgb);
|
const Table = std.enums.EnumArray(Key, ?Rgb);
|
||||||
|
|
||||||
/// The default for a key the table below doesn't mention: no colour of its own,
|
/// The default for a key the table below doesn't mention: no color of its own,
|
||||||
/// meaning it inherits one or is an ANSI colour taken from libghostty-vt. Named
|
/// meaning it inherits one or is an ANSI color taken from libghostty-vt. Named
|
||||||
/// because it has to be typed as an *inner* null — an outer one would mean "this
|
/// because it has to be typed as an *inner* null — an outer one would mean "this
|
||||||
/// table has no default" and make every key mandatory.
|
/// table has no default" and make every key mandatory.
|
||||||
const unset: ?Rgb = null;
|
const unset: ?Rgb = null;
|
||||||
|
|
||||||
/// Deep navy rather than neutral grey, lifted a little at each step so the
|
/// Deep navy rather than neutral gray, lifted a little at each step so the
|
||||||
/// three surfaces — window, sidebar, pane — separate without any of them
|
/// three surfaces — window, sidebar, pane — separate without any of them
|
||||||
/// reading as light.
|
/// reading as light.
|
||||||
const dark: Table = .initDefault(unset, .{
|
const dark: Table = .initDefault(unset, .{
|
||||||
@@ -495,12 +495,12 @@ const dark: Table = .initDefault(unset, .{
|
|||||||
|
|
||||||
// The three states that are news. Kept away from the accent on purpose: the
|
// The three states that are news. Kept away from the accent on purpose: the
|
||||||
// accent means "here" and these mean "something happened", and a sidebar
|
// accent means "here" and these mean "something happened", and a sidebar
|
||||||
// where those are the same colour answers neither question.
|
// where those are the same color answers neither question.
|
||||||
.ok = h("#3ecf8e"),
|
.ok = h("#3ecf8e"),
|
||||||
.warn = h("#f0b849"),
|
.warn = h("#f0b849"),
|
||||||
.err = h("#f2717b"),
|
.err = h("#f2717b"),
|
||||||
|
|
||||||
// The cursor is the one terminal colour that is nobody else's: it goes to
|
// The cursor is the one terminal color that is nobody else's: it goes to
|
||||||
// the bright accent here and *down* to the mid accent on white, because on
|
// the bright accent here and *down* to the mid accent on white, because on
|
||||||
// a near-white background a bright block swallows the character under it.
|
// a near-white background a bright block swallows the character under it.
|
||||||
.term_cursor = h("#6191f3"),
|
.term_cursor = h("#6191f3"),
|
||||||
@@ -509,7 +509,7 @@ const dark: Table = .initDefault(unset, .{
|
|||||||
/// Not an inversion of the dark palette. `accent_strong` is *darker* than
|
/// Not an inversion of the dark palette. `accent_strong` is *darker* than
|
||||||
/// `accent` here, for the same reason it is brighter there — its job is to
|
/// `accent` here, for the same reason it is brighter there — its job is to
|
||||||
/// carry as a small mark against the surface it sits on. The three state
|
/// carry as a small mark against the surface it sits on. The three state
|
||||||
/// colours are pulled well away from their dark values too: a dot in `#3ecf8e`
|
/// colors are pulled well away from their dark values too: a dot in `#3ecf8e`
|
||||||
/// reads clearly on navy and disappears on white.
|
/// reads clearly on navy and disappears on white.
|
||||||
///
|
///
|
||||||
/// The text stops short of black. Full-contrast black on white is harsher to
|
/// The text stops short of black. Full-contrast black on white is harsher to
|
||||||
@@ -541,7 +541,7 @@ const light: Table = .initDefault(unset, .{
|
|||||||
.term_cursor = h("#2c6bed"),
|
.term_cursor = h("#2c6bed"),
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The 16 named colours, retuned for a light background.
|
/// The 16 named colors, retuned for a light background.
|
||||||
///
|
///
|
||||||
/// This is the part of a light scheme that is easy to skip and shouldn't be:
|
/// This is the part of a light scheme that is easy to skip and shouldn't be:
|
||||||
/// libghostty-vt's default yellow is a mid tan and the standard xterm one is
|
/// libghostty-vt's default yellow is a mid tan and the standard xterm one is
|
||||||
@@ -549,7 +549,7 @@ const light: Table = .initDefault(unset, .{
|
|||||||
///
|
///
|
||||||
/// Two entries change meaning rather than brightness. `white` (7) and `bright
|
/// Two entries change meaning rather than brightness. `white` (7) and `bright
|
||||||
/// white` (15) are the foreground half of "white text", and on a white
|
/// white` (15) are the foreground half of "white text", and on a white
|
||||||
/// background they have to go dark or the text they colour disappears
|
/// background they have to go dark or the text they color disappears
|
||||||
/// altogether. Every light terminal theme makes this trade: a program that
|
/// altogether. Every light terminal theme makes this trade: a program that
|
||||||
/// asked for a white *background* gets a dark block instead, which is jarring
|
/// asked for a white *background* gets a dark block instead, which is jarring
|
||||||
/// but rare, and the alternative is text that cannot be read at all, which is
|
/// but rare, and the alternative is text that cannot be read at all, which is
|
||||||
@@ -574,18 +574,18 @@ const light_ansi = [16]Rgb{
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// A hex literal for the tables above, which is where the readability is: a
|
/// A hex literal for the tables above, which is where the readability is: a
|
||||||
/// palette wants to be read as the colours a designer wrote down, not as sixty
|
/// palette wants to be read as the colors a designer wrote down, not as sixty
|
||||||
/// structs of three numbers.
|
/// structs of three numbers.
|
||||||
inline fn h(comptime text: []const u8) Rgb {
|
inline fn h(comptime text: []const u8) Rgb {
|
||||||
comptime {
|
comptime {
|
||||||
// Sixty of these are parsed at compile time, and the integer parser they
|
// Sixty of these are parsed at compile time, and the integer parser they
|
||||||
// go through is not cheap by the branch counter's reckoning.
|
// go through is not cheap by the branch counter's reckoning.
|
||||||
@setEvalBranchQuota(100_000);
|
@setEvalBranchQuota(100_000);
|
||||||
return Rgb.parse(text) orelse @compileError("not a colour: " ++ text);
|
return Rgb.parse(text) orelse @compileError("not a color: " ++ text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every key has to end up with a colour, or the palette has a hole in it that
|
// Every key has to end up with a color, or the palette has a hole in it that
|
||||||
// nothing will report until something is painted with it.
|
// nothing will report until something is painted with it.
|
||||||
comptime {
|
comptime {
|
||||||
for (std.enums.values(Key)) |key| {
|
for (std.enums.values(Key)) |key| {
|
||||||
@@ -634,7 +634,7 @@ test "ansi keys map to palette indices in order" {
|
|||||||
try std.testing.expectEqual(@as(?u8, null), Key.accent.ansiIndex());
|
try std.testing.expectEqual(@as(?u8, null), Key.accent.ansiIndex());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "an unset colour resolves to its default" {
|
test "an unset color resolves to its default" {
|
||||||
try std.testing.expectEqual(Rgb.parse("#070c15").?, resolve(.bg, .dark, &unchanged));
|
try std.testing.expectEqual(Rgb.parse("#070c15").?, resolve(.bg, .dark, &unchanged));
|
||||||
try std.testing.expectEqual(Rgb.parse("#d9e1ee").?, resolve(.bg, .light, &unchanged));
|
try std.testing.expectEqual(Rgb.parse("#d9e1ee").?, resolve(.bg, .light, &unchanged));
|
||||||
}
|
}
|
||||||
@@ -651,16 +651,16 @@ test "the terminal follows the surface it is drawn on" {
|
|||||||
changes.overrides.set(.surface, Rgb.parse("#123456").?);
|
changes.overrides.set(.surface, Rgb.parse("#123456").?);
|
||||||
try std.testing.expectEqual(Rgb.parse("#123456").?, resolve(.term_bg, .dark, &changes));
|
try std.testing.expectEqual(Rgb.parse("#123456").?, resolve(.term_bg, .dark, &changes));
|
||||||
|
|
||||||
// ...and not once it has been given a colour of its own.
|
// ...and not once it has been given a color of its own.
|
||||||
changes.overrides.set(.term_bg, Rgb.parse("#abcdef").?);
|
changes.overrides.set(.term_bg, Rgb.parse("#abcdef").?);
|
||||||
try std.testing.expectEqual(Rgb.parse("#abcdef").?, resolve(.term_bg, .dark, &changes));
|
try std.testing.expectEqual(Rgb.parse("#abcdef").?, resolve(.term_bg, .dark, &changes));
|
||||||
try std.testing.expectEqual(Rgb.parse("#123456").?, resolve(.surface, .dark, &changes));
|
try std.testing.expectEqual(Rgb.parse("#123456").?, resolve(.surface, .dark, &changes));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "a base colour stands in for the defaults, and an override for the base" {
|
test "a base color stands in for the defaults, and an override for the base" {
|
||||||
var changes: Changes = .{ .tint = .{ .base = Rgb.parse("#d2691e").? } };
|
var changes: Changes = .{ .tint = .{ .base = Rgb.parse("#d2691e").? } };
|
||||||
|
|
||||||
// Nothing has been set by hand, so every colour is the generated one...
|
// Nothing has been set by hand, so every color is the generated one...
|
||||||
try std.testing.expect(!resolve(.bg, .dark, &changes).eql(Key.bg.default(.dark)));
|
try std.testing.expect(!resolve(.bg, .dark, &changes).eql(Key.bg.default(.dark)));
|
||||||
try std.testing.expectEqual(
|
try std.testing.expectEqual(
|
||||||
Rgb.parse("#d2691e").?,
|
Rgb.parse("#d2691e").?,
|
||||||
@@ -673,7 +673,7 @@ test "a base colour stands in for the defaults, and an override for the base" {
|
|||||||
resolve(.term_bg, .dark, &changes),
|
resolve(.term_bg, .dark, &changes),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ...and a colour set by hand outranks the base rather than the other way
|
// ...and a color set by hand outranks the base rather than the other way
|
||||||
// round, which is what lets the two be used together.
|
// round, which is what lets the two be used together.
|
||||||
changes.overrides.set(.accent, Rgb.parse("#00ff00").?);
|
changes.overrides.set(.accent, Rgb.parse("#00ff00").?);
|
||||||
try std.testing.expectEqual(Rgb.parse("#00ff00").?, resolve(.accent, .dark, &changes));
|
try std.testing.expectEqual(Rgb.parse("#00ff00").?, resolve(.accent, .dark, &changes));
|
||||||
@@ -691,7 +691,7 @@ test "anySet notices a base with nothing else changed" {
|
|||||||
try std.testing.expect(edited.anySet());
|
try std.testing.expect(edited.anySet());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "writeCss emits every named colour and no others" {
|
test "writeCss emits every named color and no others" {
|
||||||
var buf: [css_size]u8 = undefined;
|
var buf: [css_size]u8 = undefined;
|
||||||
var writer: std.Io.Writer = .fixed(&buf);
|
var writer: std.Io.Writer = .fixed(&buf);
|
||||||
|
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ fn save(self: *Store) Error!void {
|
|||||||
/// Deliberately not filtered by diff context. A comment is content someone
|
/// Deliberately not filtered by diff context. A comment is content someone
|
||||||
/// typed: it has to survive the base ref moving, the working tree being
|
/// typed: it has to survive the base ref moving, the working tree being
|
||||||
/// committed, or the page being reloaded onto a different selection. Whether a
|
/// committed, or the page being reloaded onto a different selection. Whether a
|
||||||
/// comment still lines up with the diff on screen is the frontend's judgement —
|
/// comment still lines up with the diff on screen is the frontend's judgment —
|
||||||
/// it has the parsed diff, and it marks the ones it cannot place as outdated.
|
/// it has the parsed diff, and it marks the ones it cannot place as outdated.
|
||||||
pub fn list(self: *Store, gpa: std.mem.Allocator) ![]Comment {
|
pub fn list(self: *Store, gpa: std.mem.Allocator) ![]Comment {
|
||||||
self.mutex.lockUncancelable(self.io);
|
self.mutex.lockUncancelable(self.io);
|
||||||
|
|||||||
+50
-33
@@ -1,15 +1,15 @@
|
|||||||
/* Zen-style vertical tabs: a sidebar column with the terminal inset to its
|
/* Zen-style vertical tabs: a sidebar column with the terminal inset to its
|
||||||
right.
|
right.
|
||||||
|
|
||||||
Every colour here is a name from `palette.zig`, which writes the matching
|
Every color here is a name from `palette.zig`, which writes the matching
|
||||||
`@define-color` block for the scheme in force; `appearance.zig` prepends that
|
`@define-color` block for the scheme in force; `appearance.zig` prepends that
|
||||||
and loads the pair as a single provider. Nothing below may hardcode a colour —
|
and loads the pair as a single provider. Nothing below may hardcode a color —
|
||||||
a literal hex would be a rule that looks right in one scheme and wrong in the
|
a literal hex would be a rule that looks right in one scheme and wrong in the
|
||||||
other, it would look right in whichever one you happened to be testing in,
|
other, it would look right in whichever one you happened to be testing in,
|
||||||
and the colour editor in the settings page could not reach it at all.
|
and the color editor in the settings page could not reach it at all.
|
||||||
|
|
||||||
`alpha()` is used for every wash rather than a second named colour, so a
|
`alpha()` is used for every wash rather than a second named color, so a
|
||||||
state's tint is always derived from the state's own colour and the two
|
state's tint is always derived from the state's own color and the two
|
||||||
can't drift apart. */
|
can't drift apart. */
|
||||||
|
|
||||||
.playpen-window {
|
.playpen-window {
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
|
|
||||||
The rule underneath is stated explicitly rather than left to the one Adwaita
|
The rule underneath is stated explicitly rather than left to the one Adwaita
|
||||||
draws for every header bar: that one is a shadow in the desktop theme's
|
draws for every header bar: that one is a shadow in the desktop theme's
|
||||||
colour, which on this palette read as a stray line rather than as the pair
|
color, which on this palette read as a stray line rather than as the pair
|
||||||
it now makes with the footer's. */
|
it now makes with the footer's. */
|
||||||
.playpen-sidebar headerbar {
|
.playpen-sidebar headerbar {
|
||||||
background: none;
|
background: none;
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* The two header actions. Styled explicitly rather than left to Adwaita, whose
|
/* The two header actions. Styled explicitly rather than left to Adwaita, whose
|
||||||
default button is a grey that belongs to a different window than this one.
|
default button is a gray that belongs to a different window than this one.
|
||||||
Scoped by class instead of by `headerbar button` so the window controls
|
Scoped by class instead of by `headerbar button` so the window controls
|
||||||
beside them keep their own appearance — those belong to the desktop, not to
|
beside them keep their own appearance — those belong to the desktop, not to
|
||||||
the app, and making them match would be the wrong kind of consistent.
|
the app, and making them match would be the wrong kind of consistent.
|
||||||
@@ -113,9 +113,9 @@ button.playpen-header-button:hover,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* The kind icon picks up the accent on the current row. The status dot is
|
/* The kind icon picks up the accent on the current row. The status dot is
|
||||||
excluded: it carries a colour that *is* the information, and this rule is
|
excluded: it carries a color that *is* the information, and this rule is
|
||||||
specific enough to beat the state classes, which used to leave the selected
|
specific enough to beat the state classes, which used to leave the selected
|
||||||
row's dot accent-coloured whatever it was trying to say. */
|
row's dot accent-colored whatever it was trying to say. */
|
||||||
.playpen-list > row:selected image:not(.playpen-status-dot) {
|
.playpen-list > row:selected image:not(.playpen-status-dot) {
|
||||||
color: @pp_accent_strong;
|
color: @pp_accent_strong;
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,7 @@ button.playpen-header-button:hover,
|
|||||||
rather than left to the glyph, so a sidebar mixing emoji rows with icon rows
|
rather than left to the glyph, so a sidebar mixing emoji rows with icon rows
|
||||||
still starts every label in the same column — emoji vary in advance width and
|
still starts every label in the same column — emoji vary in advance width and
|
||||||
without this the labels beside them would step in and out by a pixel or two
|
without this the labels beside them would step in and out by a pixel or two
|
||||||
per row. The font size is a little under the icon's 16px because a colour
|
per row. The font size is a little under the icon's 16px because a color
|
||||||
glyph fills its box where a symbolic icon leaves air around itself. */
|
glyph fills its box where a symbolic icon leaves air around itself. */
|
||||||
.playpen-tab-emoji {
|
.playpen-tab-emoji {
|
||||||
min-width: 16px;
|
min-width: 16px;
|
||||||
@@ -133,7 +133,7 @@ button.playpen-header-button:hover,
|
|||||||
|
|
||||||
/* State on the row itself, not just on its dot.
|
/* State on the row itself, not just on its dot.
|
||||||
|
|
||||||
A bar down the leading edge, and behind the row a wash of the same colour for
|
A bar down the leading edge, and behind the row a wash of the same color for
|
||||||
the two states that actually want you. The dot says what a row is doing once
|
the two states that actually want you. The dot says what a row is doing once
|
||||||
you are reading it; these are what let you sweep a sidebar of a dozen tabs
|
you are reading it; these are what let you sweep a sidebar of a dozen tabs
|
||||||
and land on the one that wants you without reading any of them.
|
and land on the one that wants you without reading any of them.
|
||||||
@@ -178,7 +178,7 @@ button.playpen-header-button:hover,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* The current row keeps its own background: the wash and the selection would
|
/* The current row keeps its own background: the wash and the selection would
|
||||||
otherwise blend into a colour that reads as neither. The bar survives, which
|
otherwise blend into a color that reads as neither. The bar survives, which
|
||||||
is the part that carries the state. Done is absent because it no longer has a
|
is the part that carries the state. Done is absent because it no longer has a
|
||||||
wash to suppress. */
|
wash to suppress. */
|
||||||
.playpen-list > row:selected.playpen-attn-input,
|
.playpen-list > row:selected.playpen-attn-input,
|
||||||
@@ -201,7 +201,7 @@ button.playpen-header-button:hover,
|
|||||||
box-shadow: inset 3px 0 0 @pp_accent_strong;
|
box-shadow: inset 3px 0 0 @pp_accent_strong;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The copy of that row travelling with the pointer, which GTK draws into a
|
/* The copy of that row traveling with the pointer, which GTK draws into a
|
||||||
surface of its own. What it paints is the row's *box* — the icon, the label
|
surface of its own. What it paints is the row's *box* — the icon, the label
|
||||||
and the close button — and a row's background belongs to the row above that
|
and the close button — and a row's background belongs to the row above that
|
||||||
box, so without this the label would float on nothing. Given the selected
|
box, so without this the label would float on nothing. Given the selected
|
||||||
@@ -308,12 +308,12 @@ dnd.playpen-tab-drag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Status dots. One shared appearance for the sidebar row and the pane header,
|
/* Status dots. One shared appearance for the sidebar row and the pane header,
|
||||||
coloured entirely by which state class is on the icon. Symbolic icons take
|
colored entirely by which state class is on the icon. Symbolic icons take
|
||||||
their colour from `color`, so nothing here needs a second asset.
|
their color from `color`, so nothing here needs a second asset.
|
||||||
|
|
||||||
These are read at a glance from across a screen full of tabs, so the colours
|
These are read at a glance from across a screen full of tabs, so the colors
|
||||||
are picked to survive that: amber and red carry the two states that actually
|
are picked to survive that: amber and red carry the two states that actually
|
||||||
want you, and they are the only warm colours anywhere in the window. */
|
want you, and they are the only warm colors anywhere in the window. */
|
||||||
.playpen-status-dot {
|
.playpen-status-dot {
|
||||||
-gtk-icon-size: 11px;
|
-gtk-icon-size: 11px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -375,6 +375,23 @@ dnd.playpen-tab-drag {
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The rule between what a row *is* and whether it is allowed to interrupt you.
|
||||||
|
Inset from the popover's edges so it reads as a divider inside a list rather
|
||||||
|
than as the list being cut in two. */
|
||||||
|
.playpen-row-menu > separator {
|
||||||
|
margin: 3px 6px;
|
||||||
|
background-color: @pp_border;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* What a tab's mute is currently doing, above the ways out of it. Sized and
|
||||||
|
dimmed like the sublabels in the settings dialog, because it is the same kind
|
||||||
|
of thing: a caption on the controls under it, not a control. */
|
||||||
|
.playpen-row-menu-state {
|
||||||
|
padding: 2px 8px;
|
||||||
|
color: @pp_text_faint;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
/* A web pane's navigation bar, below the pane header. Kept visually quieter
|
/* A web pane's navigation bar, below the pane header. Kept visually quieter
|
||||||
than the header so the two rows don't compete. */
|
than the header so the two rows don't compete. */
|
||||||
.playpen-nav {
|
.playpen-nav {
|
||||||
@@ -445,7 +462,7 @@ dnd.playpen-tab-drag {
|
|||||||
asking without being read pane by pane.
|
asking without being read pane by pane.
|
||||||
|
|
||||||
Only the three states that are news get a border. Working deliberately does
|
Only the three states that are news get a border. Working deliberately does
|
||||||
not: it is the resting state of every pane you have set going, and colouring
|
not: it is the resting state of every pane you have set going, and coloring
|
||||||
the frame for it would leave the whole view repainting itself all afternoon
|
the frame for it would leave the whole view repainting itself all afternoon
|
||||||
while saying nothing you did not already know. Its pulsing dot is enough.
|
while saying nothing you did not already know. Its pulsing dot is enough.
|
||||||
|
|
||||||
@@ -465,7 +482,7 @@ dnd.playpen-tab-drag {
|
|||||||
border-color: @pp_err;
|
border-color: @pp_err;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The header carries a wash of the same colour. The border alone is a hairline
|
/* The header carries a wash of the same color. The border alone is a hairline
|
||||||
around a large shape; the header is a solid band right next to the dot and
|
around a large shape; the header is a solid band right next to the dot and
|
||||||
the title, which is what makes a pane readable at a glance in a four-way
|
the title, which is what makes a pane readable at a glance in a four-way
|
||||||
split. */
|
split. */
|
||||||
@@ -601,9 +618,9 @@ dnd.playpen-tab-drag {
|
|||||||
choice is visible without opening anything, and so the one you are on is
|
choice is visible without opening anything, and so the one you are on is
|
||||||
readable at a glance rather than by reading a word. */
|
readable at a glance rather than by reading a word. */
|
||||||
/* `background-image: none` on every rule below is load-bearing. Adwaita paints
|
/* `background-image: none` on every rule below is load-bearing. Adwaita paints
|
||||||
button states with a gradient *over* the background colour, so setting the
|
button states with a gradient *over* the background color, so setting the
|
||||||
colour alone leaves a washed-out version of it — most visibly on the checked
|
color alone leaves a washed-out version of it — most visibly on the checked
|
||||||
button, where the accent came out as a pale grey-blue. `:backdrop` is
|
button, where the accent came out as a pale gray-blue. `:backdrop` is
|
||||||
spelled out for the same reason: Adwaita dims unfocused windows, and the
|
spelled out for the same reason: Adwaita dims unfocused windows, and the
|
||||||
selected scheme is exactly the thing that has to stay readable while you are
|
selected scheme is exactly the thing that has to stay readable while you are
|
||||||
looking at the window behind this one to see what it did. */
|
looking at the window behind this one to see what it did. */
|
||||||
@@ -639,11 +656,11 @@ dnd.playpen-tab-drag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------
|
/* -------------------------------------------------------------------------
|
||||||
The colour editor
|
The color editor
|
||||||
|
|
||||||
One row per colour in the palette, in sections. The rows are deliberately
|
One row per color in the palette, in sections. The rows are deliberately
|
||||||
plain: on a page where thirty-nine swatches are the point, the swatch should
|
plain: on a page where thirty-nine swatches are the point, the swatch should
|
||||||
be the only thing on a line carrying any colour, and a bordered card per
|
be the only thing on a line carrying any color, and a bordered card per
|
||||||
section would read as noise. The space between sections does that work
|
section would read as noise. The space between sections does that work
|
||||||
instead. */
|
instead. */
|
||||||
|
|
||||||
@@ -665,10 +682,10 @@ dnd.playpen-tab-drag {
|
|||||||
color: @pp_text_faint;
|
color: @pp_text_faint;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Adwaita gives a colour button the full button treatment: a gradient over the
|
/* Adwaita gives a color button the full button treatment: a gradient over the
|
||||||
colour, and a border in a grey belonging to neither palette. What is wanted
|
color, and a border in a gray belonging to neither palette. What is wanted
|
||||||
is a rectangle of the colour with this app's border around it, so the swatch
|
is a rectangle of the color with this app's border around it, so the swatch
|
||||||
reads as the colour rather than as a button holding one. */
|
reads as the color rather than as a button holding one. */
|
||||||
.playpen-color-group colorbutton > button {
|
.playpen-color-group colorbutton > button {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -684,9 +701,9 @@ dnd.playpen-tab-drag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* The contrast slider, for the same reason as the swatch above it: Adwaita
|
/* The contrast slider, for the same reason as the swatch above it: Adwaita
|
||||||
paints a scale in the *desktop's* accent colour, which on a page whose whole
|
paints a scale in the *desktop's* accent color, which on a page whose whole
|
||||||
subject is the colour this window is painted from would be the one thing on
|
subject is the color this window is painted from would be the one thing on
|
||||||
screen ignoring it. Left stock, dragging it towards a warm theme leaves a
|
screen ignoring it. Left stock, dragging it toward a warm theme leaves a
|
||||||
blue bar sitting in the middle of the result. */
|
blue bar sitting in the middle of the result. */
|
||||||
.playpen-contrast trough {
|
.playpen-contrast trough {
|
||||||
background-color: @pp_surface_raised_active;
|
background-color: @pp_surface_raised_active;
|
||||||
@@ -789,7 +806,7 @@ dnd.playpen-tab-drag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* The glyph in force. `background-image: none` for the same reason as the theme
|
/* The glyph in force. `background-image: none` for the same reason as the theme
|
||||||
buttons above: Adwaita paints selection with a gradient over the colour, and
|
buttons above: Adwaita paints selection with a gradient over the color, and
|
||||||
without this the accent comes out washed. */
|
without this the accent comes out washed. */
|
||||||
.playpen-emoji-grid > flowboxchild:selected,
|
.playpen-emoji-grid > flowboxchild:selected,
|
||||||
.playpen-emoji-grid > flowboxchild:selected:backdrop {
|
.playpen-emoji-grid > flowboxchild:selected:backdrop {
|
||||||
|
|||||||
+9
-9
@@ -1,7 +1,7 @@
|
|||||||
//! The colours the terminal renderer draws with, resolved for the scheme the
|
//! The colors the terminal renderer draws with, resolved for the scheme the
|
||||||
//! app is currently in.
|
//! app is currently in.
|
||||||
//!
|
//!
|
||||||
//! Everything the *widgets* are painted with reaches them as named CSS colours
|
//! Everything the *widgets* are painted with reaches them as named CSS colors
|
||||||
//! (see `palette.zig` and `appearance.zig`). The terminal grid can't work that
|
//! (see `palette.zig` and `appearance.zig`). The terminal grid can't work that
|
||||||
//! way: Cairo draws it directly and never consults the style tree, so a
|
//! way: Cairo draws it directly and never consults the style tree, so a
|
||||||
//! stylesheet reload on its own would leave every open terminal painted in the
|
//! stylesheet reload on its own would leave every open terminal painted in the
|
||||||
@@ -24,7 +24,7 @@ const palette = @import("palette.zig");
|
|||||||
pub const Rgb = palette.Rgb;
|
pub const Rgb = palette.Rgb;
|
||||||
pub const Scheme = palette.Scheme;
|
pub const Scheme = palette.Scheme;
|
||||||
|
|
||||||
/// The colours of one scheme, already resolved through the user's overrides.
|
/// The colors of one scheme, already resolved through the user's overrides.
|
||||||
const Resolved = struct {
|
const Resolved = struct {
|
||||||
/// Terminal default background, used when the program hasn't set one.
|
/// Terminal default background, used when the program hasn't set one.
|
||||||
bg: Rgb,
|
bg: Rgb,
|
||||||
@@ -32,19 +32,19 @@ const Resolved = struct {
|
|||||||
/// Terminal default foreground.
|
/// Terminal default foreground.
|
||||||
fg: Rgb,
|
fg: Rgb,
|
||||||
|
|
||||||
/// Cursor block colour.
|
/// Cursor block color.
|
||||||
cursor: Rgb,
|
cursor: Rgb,
|
||||||
|
|
||||||
/// Background and foreground of selected text. Both are fixed rather than
|
/// Background and foreground of selected text. Both are fixed rather than
|
||||||
/// derived from the cell underneath, because a selection has to read as one
|
/// derived from the cell underneath, because a selection has to read as one
|
||||||
/// continuous block across text the program has coloured every which way —
|
/// continuous block across text the program has colored every which way —
|
||||||
/// a translucent wash over the existing colours leaves the highlight
|
/// a translucent wash over the existing colors leaves the highlight
|
||||||
/// looking like a rendering artefact wherever the text is already bright.
|
/// looking like a rendering artifact wherever the text is already bright.
|
||||||
selection_bg: Rgb,
|
selection_bg: Rgb,
|
||||||
selection_fg: Rgb,
|
selection_fg: Rgb,
|
||||||
|
|
||||||
/// The full 256-colour palette. The 240 above index 16 are fixed by the
|
/// The full 256-color palette. The 240 above index 16 are fixed by the
|
||||||
/// xterm spec — a 6×6×6 cube and a grey ramp — and mean the same thing
|
/// xterm spec — a 6×6×6 cube and a gray ramp — and mean the same thing
|
||||||
/// whatever the background is, so they are left exactly as libghostty-vt
|
/// whatever the background is, so they are left exactly as libghostty-vt
|
||||||
/// built them; only the 16 named ones are ours to set.
|
/// built them; only the 16 named ones are ours to set.
|
||||||
ansi: vt.color.Palette,
|
ansi: vt.color.Palette,
|
||||||
|
|||||||
+52
-52
@@ -1,29 +1,29 @@
|
|||||||
//! One colour in, a whole palette out.
|
//! One color in, a whole palette out.
|
||||||
//!
|
//!
|
||||||
//! Forty swatches is a fair editor and a poor starting point. Almost nobody
|
//! Forty swatches is a fair editor and a poor starting point. Almost nobody
|
||||||
//! wants to choose a window backdrop, a sidebar, a pane surface, a pane header
|
//! wants to choose a window backdrop, a sidebar, a pane surface, a pane header
|
||||||
//! and two border weights — they want the app to be *green*, and for the six
|
//! and two border weights — they want the app to be *green*, and for the six
|
||||||
//! greys behind the green to be the six greys that go with it. That is what this
|
//! grays behind the green to be the six grays that go with it. That is what this
|
||||||
//! module is: a base colour, and a rule for everything else.
|
//! module is: a base color, and a rule for everything else.
|
||||||
//!
|
//!
|
||||||
//! It sits underneath the per-colour overrides rather than replacing them (see
|
//! It sits underneath the per-color overrides rather than replacing them (see
|
||||||
//! `palette.resolve`). Picking a base changes what every unedited colour
|
//! `palette.resolve`). Picking a base changes what every unedited color
|
||||||
//! resolves to; a colour that has been set by hand still wins. So the two ways
|
//! resolves to; a color that has been set by hand still wins. So the two ways
|
||||||
//! of theming compose in the order you would want them to — build the thing out
|
//! of theming compose in the order you would want them to — build the thing out
|
||||||
//! of one colour, then go and fix the one swatch you don't like — and neither
|
//! of one color, then go and fix the one swatch you don't like — and neither
|
||||||
//! destroys the other's work.
|
//! destroys the other's work.
|
||||||
//!
|
//!
|
||||||
//! **How the rule works.** Every colour is one of three kinds:
|
//! **How the rule works.** Every color is one of three kinds:
|
||||||
//!
|
//!
|
||||||
//! * Most of them are *tinted*: they take the base's hue, a fraction of its
|
//! * Most of them are *tinted*: they take the base's hue, a fraction of its
|
||||||
//! colourfulness, and a fixed lightness. This is the structure of the theme —
|
//! colorfulness, and a fixed lightness. This is the structure of the theme —
|
||||||
//! the surfaces stacked from the backdrop up to the pane header, the three
|
//! the surfaces stacked from the backdrop up to the pane header, the three
|
||||||
//! weights of text on them. A green base makes them faintly green, a grey
|
//! weights of text on them. A green base makes them faintly green, a gray
|
||||||
//! base makes them grey, and their spacing never changes, because that
|
//! base makes them gray, and their spacing never changes, because that
|
||||||
//! spacing is what tells a pane header from a pane.
|
//! spacing is what tells a pane header from a pane.
|
||||||
//! * Three of them are the *accent*: the base colour itself, and a lighter and a
|
//! * Three of them are the *accent*: the base color itself, and a lighter and a
|
||||||
//! more muted version of it. Picking `#3a76f0` gets you `#3a76f0` as the
|
//! more muted version of it. Picking `#3a76f0` gets you `#3a76f0` as the
|
||||||
//! accent, which is the only behaviour that makes the colour picker feel like
|
//! accent, which is the only behavior that makes the color picker feel like
|
||||||
//! it did what you asked.
|
//! it did what you asked.
|
||||||
//! * The rest are *named* — red, green, "failed", "waiting on you". A red that
|
//! * The rest are *named* — red, green, "failed", "waiting on you". A red that
|
||||||
//! followed the base hue would not be red. These keep their own hue and take
|
//! followed the base hue would not be red. These keep their own hue and take
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
//! scaling it about a midpoint. The difference matters in the light scheme,
|
//! scaling it about a midpoint. The difference matters in the light scheme,
|
||||||
//! where the surfaces sit hard against white: scaling about a midpoint pushes
|
//! where the surfaces sit hard against white: scaling about a midpoint pushes
|
||||||
//! them all through the ceiling and the pane, the pane header and the sidebar
|
//! them all through the ceiling and the pane, the pane header and the sidebar
|
||||||
//! come out the same colour. Anchoring at the ends instead means the ramp can
|
//! come out the same color. Anchoring at the ends instead means the ramp can
|
||||||
//! only ever spread apart or draw together, which is the thing the setting
|
//! only ever spread apart or draw together, which is the thing the setting
|
||||||
//! claims to do.
|
//! claims to do.
|
||||||
//!
|
//!
|
||||||
@@ -56,7 +56,7 @@ const Rgb = palette.Rgb;
|
|||||||
const Scheme = palette.Scheme;
|
const Scheme = palette.Scheme;
|
||||||
const Tint = palette.Tint;
|
const Tint = palette.Tint;
|
||||||
|
|
||||||
/// The colour every unedited swatch in `scheme` resolves to under `tint`.
|
/// The color every unedited swatch in `scheme` resolves to under `tint`.
|
||||||
///
|
///
|
||||||
/// Total for every key that doesn't inherit one — which is all of them that
|
/// Total for every key that doesn't inherit one — which is all of them that
|
||||||
/// reach here, since `palette.resolve` follows inheritance before it gets this
|
/// reach here, since `palette.resolve` follows inheritance before it gets this
|
||||||
@@ -70,10 +70,10 @@ pub fn derive(key: Key, scheme: Scheme, tint: Tint) Rgb {
|
|||||||
const ramp = lightness_ramp.get(scheme);
|
const ramp = lightness_ramp.get(scheme);
|
||||||
|
|
||||||
const shade: oklab.Lch = switch (stop.role) {
|
const shade: oklab.Lch = switch (stop.role) {
|
||||||
// Structure: the base's hue, a share of its colourfulness, and a fixed
|
// Structure: the base's hue, a share of its colorfulness, and a fixed
|
||||||
// rung on the ladder. The share is a fraction rather than an absolute
|
// rung on the ladder. The share is a fraction rather than an absolute
|
||||||
// so that a grey base yields a grey window — there is no floor here
|
// so that a gray base yields a gray window — there is no floor here
|
||||||
// that would make a deliberately colourless theme come out faintly
|
// that would make a deliberately colorless theme come out faintly
|
||||||
// blue.
|
// blue.
|
||||||
.tinted => .{
|
.tinted => .{
|
||||||
.l = stop.l,
|
.l = stop.l,
|
||||||
@@ -81,9 +81,9 @@ pub fn derive(key: Key, scheme: Scheme, tint: Tint) Rgb {
|
|||||||
.h = base.h,
|
.h = base.h,
|
||||||
},
|
},
|
||||||
|
|
||||||
// The accent is the colour that was picked, moved only as far as it has
|
// The accent is the color that was picked, moved only as far as it has
|
||||||
// to be. The lightness clamp is the whole of that: someone who picks a
|
// to be. The lightness clamp is the whole of that: someone who picks a
|
||||||
// near-black as their base means "make it this colour", not "make the
|
// near-black as their base means "make it this color", not "make the
|
||||||
// one thing that has to stand out invisible", and the two lighter and
|
// one thing that has to stand out invisible", and the two lighter and
|
||||||
// darker accents are then offsets from wherever it landed.
|
// darker accents are then offsets from wherever it landed.
|
||||||
.accent => .{
|
.accent => .{
|
||||||
@@ -92,10 +92,10 @@ pub fn derive(key: Key, scheme: Scheme, tint: Tint) Rgb {
|
|||||||
.h = base.h,
|
.h = base.h,
|
||||||
},
|
},
|
||||||
|
|
||||||
// A colour that has to stay the colour it is named after keeps its hue
|
// A color that has to stay the color it is named after keeps its hue
|
||||||
// outright, and takes from the base only how loud to be — measured
|
// outright, and takes from the base only how loud to be — measured
|
||||||
// against the accent the shipped palette was tuned around. The floor
|
// against the accent the shipped palette was tuned around. The floor
|
||||||
// keeps a grey theme's "failed" marker distinguishable from its
|
// keeps a gray theme's "failed" marker distinguishable from its
|
||||||
// "finished" one; the ceiling keeps a neon base from producing sixteen
|
// "finished" one; the ceiling keeps a neon base from producing sixteen
|
||||||
// more neons.
|
// more neons.
|
||||||
.named => .{
|
.named => .{
|
||||||
@@ -116,23 +116,23 @@ pub fn derive(key: Key, scheme: Scheme, tint: Tint) Rgb {
|
|||||||
// -------------------------------------------------------------------------
|
// -------------------------------------------------------------------------
|
||||||
// Contrast
|
// Contrast
|
||||||
|
|
||||||
/// Move a lightness away from — or towards — the ends of its scheme's ramp.
|
/// Move a lightness away from — or toward — the ends of its scheme's ramp.
|
||||||
///
|
///
|
||||||
/// The ramp's own ends are pushed out first, each towards the nearer end of the
|
/// The ramp's own ends are pushed out first, each toward the nearer end of the
|
||||||
/// scale and by the same *share of the room it has left*, and everything in
|
/// scale and by the same *share of the room it has left*, and everything in
|
||||||
/// between is carried along by the affine map that takes the old ends to the new
|
/// between is carried along by the affine map that takes the old ends to the new
|
||||||
/// ones. Two things fall out of that shape and both are the point:
|
/// ones. Two things fall out of that shape and both are the point:
|
||||||
///
|
///
|
||||||
/// * Nothing can be pushed past black or white, so nothing clips. A ramp that
|
/// * Nothing can be pushed past black or white, so nothing clips. A ramp that
|
||||||
/// clipped would not merely stop responding at the top — it would collapse the
|
/// clipped would not merely stop responding at the top — it would collapse the
|
||||||
/// colours nearest the ceiling into each other, which in the light scheme is
|
/// colors nearest the ceiling into each other, which in the light scheme is
|
||||||
/// the pane, the pane header and the sidebar becoming one flat white.
|
/// the pane, the pane header and the sidebar becoming one flat white.
|
||||||
/// * Every gap in the ramp scales by the same factor. Turning contrast up
|
/// * Every gap in the ramp scales by the same factor. Turning contrast up
|
||||||
/// separates the pane from the backdrop and the text from the pane by the same
|
/// separates the pane from the backdrop and the text from the pane by the same
|
||||||
/// proportion, rather than doing something dramatic at one end of the ladder
|
/// proportion, rather than doing something dramatic at one end of the ladder
|
||||||
/// and nothing at the other.
|
/// and nothing at the other.
|
||||||
///
|
///
|
||||||
/// `spread` is how much of that a given colour takes. The structural colours
|
/// `spread` is how much of that a given color takes. The structural colors
|
||||||
/// take all of it; an accent or a red takes about half, because past a point
|
/// take all of it; an accent or a red takes about half, because past a point
|
||||||
/// contrast stops making them starker and starts making them not red.
|
/// contrast stops making them starker and starts making them not red.
|
||||||
fn stretch(l: f32, contrast: f32, ramp: Ramp, spread: f32) f32 {
|
fn stretch(l: f32, contrast: f32, ramp: Ramp, spread: f32) f32 {
|
||||||
@@ -149,21 +149,21 @@ fn stretch(l: f32, contrast: f32, ramp: Ramp, spread: f32) f32 {
|
|||||||
/// becomes bare white.
|
/// becomes bare white.
|
||||||
const contrast_reach = 0.5;
|
const contrast_reach = 0.5;
|
||||||
|
|
||||||
/// Contrast drains a little colour out of the structural surfaces as it goes up
|
/// Contrast drains a little color out of the structural surfaces as it goes up
|
||||||
/// — a starker theme is a cleaner one — and puts a little back into the colours
|
/// — a starker theme is a cleaner one — and puts a little back into the colors
|
||||||
/// whose job is to be seen.
|
/// whose job is to be seen.
|
||||||
const chroma_calm = 0.20;
|
const chroma_calm = 0.20;
|
||||||
const chroma_lift = 0.12;
|
const chroma_lift = 0.12;
|
||||||
|
|
||||||
/// The chroma of the accent both shipped palettes are built around, which is
|
/// The chroma of the accent both shipped palettes are built around, which is
|
||||||
/// what a `named` colour's intensity is measured against. A base this colourful
|
/// what a `named` color's intensity is measured against. A base this colorful
|
||||||
/// leaves the status and terminal colours exactly as the tables below name them.
|
/// leaves the status and terminal colors exactly as the tables below name them.
|
||||||
const reference_chroma = 0.1944;
|
const reference_chroma = 0.1944;
|
||||||
|
|
||||||
/// The two ends of a scheme's lightness ladder, and the band an accent has to
|
/// The two ends of a scheme's lightness ladder, and the band an accent has to
|
||||||
/// land in to be legible against it.
|
/// land in to be legible against it.
|
||||||
const Ramp = struct {
|
const Ramp = struct {
|
||||||
/// The lightness of the colour furthest into the background — the window
|
/// The lightness of the color furthest into the background — the window
|
||||||
/// backdrop in the dark scheme, the body text in the light one.
|
/// backdrop in the dark scheme, the body text in the light one.
|
||||||
lo: f32,
|
lo: f32,
|
||||||
|
|
||||||
@@ -188,7 +188,7 @@ const Role = enum {
|
|||||||
accent,
|
accent,
|
||||||
named,
|
named,
|
||||||
|
|
||||||
/// How much of the contrast stretch this kind of colour takes. See
|
/// How much of the contrast stretch this kind of color takes. See
|
||||||
/// `stretch`.
|
/// `stretch`.
|
||||||
fn spread(self: Role) f32 {
|
fn spread(self: Role) f32 {
|
||||||
return switch (self) {
|
return switch (self) {
|
||||||
@@ -199,20 +199,20 @@ const Role = enum {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// One colour's recipe in one scheme. What the three numbers mean depends on the
|
/// One color's recipe in one scheme. What the three numbers mean depends on the
|
||||||
/// role, which is why they are named for their role rather than for OKLCh.
|
/// role, which is why they are named for their role rather than for OKLCh.
|
||||||
const Stop = struct {
|
const Stop = struct {
|
||||||
role: Role,
|
role: Role,
|
||||||
|
|
||||||
/// `tinted` and `named`: the lightness outright. `accent`: an offset from
|
/// `tinted` and `named`: the lightness outright. `accent`: an offset from
|
||||||
/// the base colour's own.
|
/// the base color's own.
|
||||||
l: f32,
|
l: f32,
|
||||||
|
|
||||||
/// `tinted` and `accent`: a multiple of the base colour's chroma. `named`:
|
/// `tinted` and `accent`: a multiple of the base color's chroma. `named`:
|
||||||
/// a chroma outright, scaled by how colourful the base is.
|
/// a chroma outright, scaled by how colorful the base is.
|
||||||
c: f32,
|
c: f32,
|
||||||
|
|
||||||
/// `named` only: the hue that makes it the colour it is named after.
|
/// `named` only: the hue that makes it the color it is named after.
|
||||||
h: f32 = 0,
|
h: f32 = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -220,7 +220,7 @@ fn tinted(l: f32, c: f32) Stop {
|
|||||||
return .{ .role = .tinted, .l = l, .c = c };
|
return .{ .role = .tinted, .l = l, .c = c };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `dl` is measured from the base colour's lightness, so that the three accents
|
/// `dl` is measured from the base color's lightness, so that the three accents
|
||||||
/// stay the same distance apart wherever the base sits.
|
/// stay the same distance apart wherever the base sits.
|
||||||
fn accent(dl: f32, c: f32) Stop {
|
fn accent(dl: f32, c: f32) Stop {
|
||||||
return .{ .role = .accent, .l = dl, .c = c };
|
return .{ .role = .accent, .l = dl, .c = c };
|
||||||
@@ -234,7 +234,7 @@ const Stops = std.enums.EnumArray(Key, ?Stop);
|
|||||||
|
|
||||||
/// Named because it has to be typed as an *inner* null: an outer one would mean
|
/// Named because it has to be typed as an *inner* null: an outer one would mean
|
||||||
/// "this table has no stop for anything". A key absent from both tables is one
|
/// "this table has no stop for anything". A key absent from both tables is one
|
||||||
/// that inherits its colour from another and never reaches this module.
|
/// that inherits its color from another and never reaches this module.
|
||||||
const inherited: ?Stop = null;
|
const inherited: ?Stop = null;
|
||||||
|
|
||||||
fn stops(scheme: Scheme) *const Stops {
|
fn stops(scheme: Scheme) *const Stops {
|
||||||
@@ -278,9 +278,9 @@ const dark_stops: Stops = .initDefault(inherited, .{
|
|||||||
// the mid one, for the reason `palette.Key.hint` gives.
|
// the mid one, for the reason `palette.Key.hint` gives.
|
||||||
.term_cursor = accent(0.0742, 0.797),
|
.term_cursor = accent(0.0742, 0.797),
|
||||||
|
|
||||||
// The four neutral ANSI colours are tinted rather than named: "black" and
|
// The four neutral ANSI colors are tinted rather than named: "black" and
|
||||||
// "white" here are the ends of the terminal's own greyscale, and a terminal
|
// "white" here are the ends of the terminal's own grayscale, and a terminal
|
||||||
// whose greys are a different grey from the window around it looks like a
|
// whose grays are a different gray from the window around it looks like a
|
||||||
// pane that failed to load.
|
// pane that failed to load.
|
||||||
.ansi_black = tinted(0.2380, 0.190),
|
.ansi_black = tinted(0.2380, 0.190),
|
||||||
.ansi_red = named(0.6308, 0.150, 21.4),
|
.ansi_red = named(0.6308, 0.150, 21.4),
|
||||||
@@ -353,7 +353,7 @@ const light_stops: Stops = .initDefault(inherited, .{
|
|||||||
|
|
||||||
// A key has a stop in both tables or in neither, and which it is has to be the
|
// A key has a stop in both tables or in neither, and which it is has to be the
|
||||||
// same answer `palette.Key.inherits` gives — otherwise a generated palette
|
// same answer `palette.Key.inherits` gives — otherwise a generated palette
|
||||||
// either has a hole in it that `derive` walks into, or quietly stops honouring
|
// either has a hole in it that `derive` walks into, or quietly stops honoring
|
||||||
// an inheritance the rest of the app is written against.
|
// an inheritance the rest of the app is written against.
|
||||||
comptime {
|
comptime {
|
||||||
for (std.enums.values(Key)) |key| {
|
for (std.enums.values(Key)) |key| {
|
||||||
@@ -370,7 +370,7 @@ comptime {
|
|||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
|
|
||||||
/// The largest difference in any channel, which is the useful measure here: a
|
/// The largest difference in any channel, which is the useful measure here: a
|
||||||
/// generated colour is right if you cannot tell it from the one it is standing
|
/// generated color is right if you cannot tell it from the one it is standing
|
||||||
/// in for, and a couple of steps in one channel is well inside that.
|
/// in for, and a couple of steps in one channel is well inside that.
|
||||||
fn drift(a: Rgb, b: Rgb) u16 {
|
fn drift(a: Rgb, b: Rgb) u16 {
|
||||||
return @max(
|
return @max(
|
||||||
@@ -395,7 +395,7 @@ test "the accent regenerates the palette it was taken from" {
|
|||||||
const generated = derive(key, scheme, tint);
|
const generated = derive(key, scheme, tint);
|
||||||
const shipped = key.default(scheme);
|
const shipped = key.default(scheme);
|
||||||
|
|
||||||
// Wider for the ANSI colours: those stops are a deliberate retune
|
// Wider for the ANSI colors: those stops are a deliberate retune
|
||||||
// rather than a transcription — libghostty-vt's dark sixteen are
|
// rather than a transcription — libghostty-vt's dark sixteen are
|
||||||
// washed out beside a full-strength accent, and the shipped light
|
// washed out beside a full-strength accent, and the shipped light
|
||||||
// cyan is duller than the rest of its row.
|
// cyan is duller than the rest of its row.
|
||||||
@@ -412,8 +412,8 @@ test "the accent regenerates the palette it was taken from" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test "the accent is the colour that was picked" {
|
test "the accent is the color that was picked" {
|
||||||
// Anything else makes the picker feel broken. The only licence taken is the
|
// Anything else makes the picker feel broken. The only license taken is the
|
||||||
// lightness clamp, and these two sit well inside it.
|
// lightness clamp, and these two sit well inside it.
|
||||||
for ([_][]const u8{ "#3a76f0", "#d2691e" }) |text| {
|
for ([_][]const u8{ "#3a76f0", "#d2691e" }) |text| {
|
||||||
const base = Rgb.parse(text).?;
|
const base = Rgb.parse(text).?;
|
||||||
@@ -449,13 +449,13 @@ test "the surfaces stay in order, whatever the base and the contrast" {
|
|||||||
const l = oklab.fromRgb(derive(key, scheme, tint)).l;
|
const l = oklab.fromRgb(derive(key, scheme, tint)).l;
|
||||||
|
|
||||||
// Light steps *down* from its pane surface for the two
|
// Light steps *down* from its pane surface for the two
|
||||||
// raised colours, so it is the size of each step that has to
|
// raised colors, so it is the size of each step that has to
|
||||||
// hold rather than the direction of all of them.
|
// hold rather than the direction of all of them.
|
||||||
//
|
//
|
||||||
// The floor is low because the tightest step in the shipped
|
// The floor is low because the tightest step in the shipped
|
||||||
// dark palette — the sidebar against the pane beside it — is
|
// dark palette — the sidebar against the pane beside it — is
|
||||||
// itself only 0.014, those two being separated as much by
|
// itself only 0.014, those two being separated as much by
|
||||||
// the gutter between them as by their colours. Softening the
|
// the gutter between them as by their colors. Softening the
|
||||||
// contrast draws that to about 0.012, which is the number
|
// contrast draws that to about 0.012, which is the number
|
||||||
// this has to sit under. What it is really watching for is a
|
// this has to sit under. What it is really watching for is a
|
||||||
// step going to nothing.
|
// step going to nothing.
|
||||||
@@ -503,7 +503,7 @@ test "contrast spreads the ramp without pushing anything off the end" {
|
|||||||
try testing.expect(soft < level);
|
try testing.expect(soft < level);
|
||||||
try testing.expect(level < stark);
|
try testing.expect(level < stark);
|
||||||
|
|
||||||
// And the colours nearest the ceiling stay distinct rather than piling
|
// And the colors nearest the ceiling stay distinct rather than piling
|
||||||
// up against it, which is what a ramp that scaled about its middle would
|
// up against it, which is what a ramp that scaled about its middle would
|
||||||
// do to the light scheme.
|
// do to the light scheme.
|
||||||
const tint: Tint = .{ .base = base, .contrast = 1 };
|
const tint: Tint = .{ .base = base, .contrast = 1 };
|
||||||
@@ -518,11 +518,11 @@ fn span(scheme: Scheme, tint: Tint) f32 {
|
|||||||
oklab.fromRgb(derive(.bg, scheme, tint)).l);
|
oklab.fromRgb(derive(.bg, scheme, tint)).l);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "a grey base makes a grey theme, and keeps the status colours apart" {
|
test "a gray base makes a gray theme, and keeps the status colors apart" {
|
||||||
const tint: Tint = .{ .base = Rgb.parse("#808080").? };
|
const tint: Tint = .{ .base = Rgb.parse("#808080").? };
|
||||||
|
|
||||||
for (std.enums.values(Scheme)) |scheme| {
|
for (std.enums.values(Scheme)) |scheme| {
|
||||||
// No floor sneaking colour back into a theme that asked for none.
|
// No floor sneaking color back into a theme that asked for none.
|
||||||
for ([_]Key{ .bg, .surface, .text, .accent }) |key| {
|
for ([_]Key{ .bg, .surface, .text, .accent }) |key| {
|
||||||
try testing.expect(oklab.fromRgb(derive(key, scheme, tint)).c < 0.01);
|
try testing.expect(oklab.fromRgb(derive(key, scheme, tint)).c < 0.01);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -26,7 +26,7 @@ just over half of those are the same gesture five more times; a grid of them is
|
|||||||
harder to look through, not more complete. Every base glyph is present, which
|
harder to look through, not more complete. Every base glyph is present, which
|
||||||
is the same choice GTK's own emoji chooser, iOS and Slack all make.
|
is the same choice GTK's own emoji chooser, iOS and Slack all make.
|
||||||
|
|
||||||
So is anything newer than MAX_VERSION. Colour emoji fonts trail Unicode by a
|
So is anything newer than MAX_VERSION. Color emoji fonts trail Unicode by a
|
||||||
year or two, and a glyph the font has never heard of draws as a hex-digit box —
|
year or two, and a glyph the font has never heard of draws as a hex-digit box —
|
||||||
which in a picker reads as a bug rather than as a font that needs updating. At
|
which in a picker reads as a bug rather than as a font that needs updating. At
|
||||||
the time of writing, Noto Color Emoji could draw all but 7 of the E17.0
|
the time of writing, Noto Color Emoji could draw all but 7 of the E17.0
|
||||||
@@ -50,7 +50,7 @@ EMOJI_TEST = "https://unicode.org/Public/emoji/latest/emoji-test.txt"
|
|||||||
ANNOTATIONS = "https://raw.githubusercontent.com/unicode-org/cldr/main/common/annotations/en.xml"
|
ANNOTATIONS = "https://raw.githubusercontent.com/unicode-org/cldr/main/common/annotations/en.xml"
|
||||||
DERIVED = "https://raw.githubusercontent.com/unicode-org/cldr/main/common/annotationsDerived/en.xml"
|
DERIVED = "https://raw.githubusercontent.com/unicode-org/cldr/main/common/annotationsDerived/en.xml"
|
||||||
|
|
||||||
# Terms no data file will ever give you: what a glyph means to someone labelling
|
# Terms no data file will ever give you: what a glyph means to someone labeling
|
||||||
# a terminal tab. CLDR knows a rocket is a "space ship"; it does not know it is
|
# a terminal tab. CLDR knows a rocket is a "space ship"; it does not know it is
|
||||||
# what you reach for when the tab is a deploy.
|
# what you reach for when the tab is a deploy.
|
||||||
SYNONYMS = {
|
SYNONYMS = {
|
||||||
@@ -305,7 +305,7 @@ HEADER = '''//! Every emoji a tab can wear in place of its icon, and the search
|
|||||||
//! Unicode lists nearly twice this many fully-qualified sequences and the
|
//! Unicode lists nearly twice this many fully-qualified sequences and the
|
||||||
//! difference is almost entirely the same gesture in five tones, which makes a
|
//! difference is almost entirely the same gesture in five tones, which makes a
|
||||||
//! grid longer to look through without making it more complete. The version
|
//! grid longer to look through without making it more complete. The version
|
||||||
//! cutoff is there because colour emoji fonts trail Unicode, and a glyph the
|
//! cutoff is there because color emoji fonts trail Unicode, and a glyph the
|
||||||
//! font has never heard of draws as a hex-digit box; the generator's header
|
//! font has never heard of draws as a hex-digit box; the generator's header
|
||||||
//! explains how to raise it.
|
//! explains how to raise it.
|
||||||
//!
|
//!
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<!-- The topbar brand mark as a favicon: the Octicon `file-diff` glyph knocked
|
<!-- The topbar brand mark as a favicon: the Octicon `file-diff` glyph knocked
|
||||||
out of a Primer-blue tile (bgColor.accent.emphasis #316dca, shaded a shade
|
out of a Primer-blue tile (bgColor.accent.emphasis #316dca, shaded a shade
|
||||||
either side for depth). Same shape and colour as <Icon name="file-diff" />
|
either side for depth). Same shape and color as <Icon name="file-diff" />
|
||||||
in the header, so the tab matches the app. Vite copies public/ to dist/,
|
in the header, so the tab matches the app. Vite copies public/ to dist/,
|
||||||
which web/embed.go ships inside the binary. -->
|
which web/embed.go ships inside the binary. -->
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
+5
-5
@@ -213,7 +213,7 @@ export default function App() {
|
|||||||
|
|
||||||
// The parsed diff lives here rather than in DiffView because the comments rail
|
// The parsed diff lives here rather than in DiffView because the comments rail
|
||||||
// needs it too: deciding which comments the diff can still place is one
|
// needs it too: deciding which comments the diff can still place is one
|
||||||
// judgement, made once, so the rail and the diff can't disagree about it.
|
// judgment, made once, so the rail and the diff can't disagree about it.
|
||||||
const parsedFiles = useMemo(() => (payload ? parseDiff(payload.patch) : []), [payload]);
|
const parsedFiles = useMemo(() => (payload ? parseDiff(payload.patch) : []), [payload]);
|
||||||
|
|
||||||
// What each file's diff currently says, digested. Viewed marks are stored
|
// What each file's diff currently says, digested. Viewed marks are stored
|
||||||
@@ -395,20 +395,20 @@ export default function App() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!path || !revision || stale) return;
|
if (!path || !revision || stale) return;
|
||||||
|
|
||||||
let cancelled = false;
|
let canceled = false;
|
||||||
let busy = false;
|
let busy = false;
|
||||||
|
|
||||||
const check = async () => {
|
const check = async () => {
|
||||||
// A hidden pane is a pane nobody is reading. It gets checked the moment it
|
// A hidden pane is a pane nobody is reading. It gets checked the moment it
|
||||||
// comes back instead, which is when the answer matters.
|
// comes back instead, which is when the answer matters.
|
||||||
if (cancelled || busy || document.hidden) return;
|
if (canceled || busy || document.hidden) return;
|
||||||
busy = true;
|
busy = true;
|
||||||
const seq = reqRef.current;
|
const seq = reqRef.current;
|
||||||
try {
|
try {
|
||||||
const { revision: now } = await api.revision(ctx, { ignoreWhitespace: ignoreWs });
|
const { revision: now } = await api.revision(ctx, { ignoreWhitespace: ignoreWs });
|
||||||
// A load that started while this was in flight has already answered the
|
// A load that started while this was in flight has already answered the
|
||||||
// question, with a revision this closure doesn't know about.
|
// question, with a revision this closure doesn't know about.
|
||||||
if (cancelled || seq !== reqRef.current) return;
|
if (canceled || seq !== reqRef.current) return;
|
||||||
if (now && now !== revision) setStale(now);
|
if (now && now !== revision) setStale(now);
|
||||||
} catch {
|
} catch {
|
||||||
// A failed poll says nothing about the diff — the next one will.
|
// A failed poll says nothing about the diff — the next one will.
|
||||||
@@ -424,7 +424,7 @@ export default function App() {
|
|||||||
document.addEventListener('visibilitychange', onVisible);
|
document.addEventListener('visibilitychange', onVisible);
|
||||||
window.addEventListener('focus', onVisible);
|
window.addEventListener('focus', onVisible);
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
canceled = true;
|
||||||
window.clearInterval(timer);
|
window.clearInterval(timer);
|
||||||
document.removeEventListener('visibilitychange', onVisible);
|
document.removeEventListener('visibilitychange', onVisible);
|
||||||
window.removeEventListener('focus', onVisible);
|
window.removeEventListener('focus', onVisible);
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ function SingleThread({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BodyEditor edits a comment's text in place of its rendered body. It starts
|
// BodyEditor edits a comment's text in place of its rendered body. It starts
|
||||||
// from the saved text and only reports a change on save, so cancelling always
|
// from the saved text and only reports a change on save, so canceling always
|
||||||
// leaves the stored comment untouched.
|
// leaves the stored comment untouched.
|
||||||
function BodyEditor({
|
function BodyEditor({
|
||||||
initial,
|
initial,
|
||||||
|
|||||||
@@ -206,12 +206,12 @@ function FileView({
|
|||||||
setOldSource(null);
|
setOldSource(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let cancelled = false;
|
let canceled = false;
|
||||||
api.fileContent(base, file.oldPath).then((s) => {
|
api.fileContent(base, file.oldPath).then((s) => {
|
||||||
if (!cancelled) setOldSource(s);
|
if (!canceled) setOldSource(s);
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
canceled = true;
|
||||||
};
|
};
|
||||||
}, [base, file.oldPath, file.type]);
|
}, [base, file.oldPath, file.type]);
|
||||||
|
|
||||||
@@ -227,14 +227,14 @@ function FileView({
|
|||||||
// Highlighting is done over the *whole* file, never over the visible hunks
|
// Highlighting is done over the *whole* file, never over the visible hunks
|
||||||
// alone. Prism is a stateful tokenizer: a construct that opens above the first
|
// alone. Prism is a stateful tokenizer: a construct that opens above the first
|
||||||
// visible line — a block comment, a template literal, a heredoc — leaves it in
|
// visible line — a block comment, a template literal, a heredoc — leaves it in
|
||||||
// the wrong state and mis-colours everything after it, so what got highlighted
|
// the wrong state and mis-colors everything after it, so what got highlighted
|
||||||
// would depend on which context happened to be collapsed. Handing it the base
|
// would depend on which context happened to be collapsed. Handing it the base
|
||||||
// source (react-diff-view derives the head side by applying `hunks`) makes the
|
// source (react-diff-view derives the head side by applying `hunks`) makes the
|
||||||
// result identical no matter what is expanded.
|
// result identical no matter what is expanded.
|
||||||
//
|
//
|
||||||
// A wholly added or deleted file needs no base source: its hunks already carry
|
// A wholly added or deleted file needs no base source: its hunks already carry
|
||||||
// every line, so tokenizing them is exact. Otherwise we wait for the fetch
|
// every line, so tokenizing them is exact. Otherwise we wait for the fetch
|
||||||
// rather than highlight a fragment — a beat of plain text beats wrong colours.
|
// rather than highlight a fragment — a beat of plain text beats wrong colors.
|
||||||
const tokens: HunkTokens | undefined = useMemo(() => {
|
const tokens: HunkTokens | undefined = useMemo(() => {
|
||||||
const lang = languageForFile(path);
|
const lang = languageForFile(path);
|
||||||
if (!lang) return undefined;
|
if (!lang) return undefined;
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
// pulled in as a dependency because we need a dozen of ~600, and a local table
|
// pulled in as a dependency because we need a dozen of ~600, and a local table
|
||||||
// keeps the icon set visible in one place instead of hidden behind imports.
|
// keeps the icon set visible in one place instead of hidden behind imports.
|
||||||
//
|
//
|
||||||
// Every glyph is authored on a 16×16 grid with `fill: currentColor`, so colour
|
// Every glyph is authored on a 16×16 grid with `fill: currentColor`, so color
|
||||||
// comes from the surrounding text colour and size from the `size` prop.
|
// comes from the surrounding text color and size from the `size` prop.
|
||||||
|
|
||||||
const PATHS = {
|
const PATHS = {
|
||||||
'chevron-down':
|
'chevron-down':
|
||||||
|
|||||||
+9
-9
@@ -1,7 +1,7 @@
|
|||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
review — local code review UI
|
review — local code review UI
|
||||||
|
|
||||||
The styling is GitHub's, not an interpretation of it. Every colour below is a
|
The styling is GitHub's, not an interpretation of it. Every color below is a
|
||||||
Primer token: the dark block is Primer's `dark dimmed` theme and the light
|
Primer token: the dark block is Primer's `dark dimmed` theme and the light
|
||||||
block is `light`, both at the values GitHub currently ships. The diff
|
block is `light`, both at the values GitHub currently ships. The diff
|
||||||
geometry — 12px/24px mono rows, 45px line-number columns, a 22px marker
|
geometry — 12px/24px mono rows, 45px line-number columns, a 22px marker
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
--attention-soft: rgba(174, 124, 20, 0.15);
|
--attention-soft: rgba(174, 124, 20, 0.15);
|
||||||
--attention-muted: rgba(174, 124, 20, 0.4);
|
--attention-muted: rgba(174, 124, 20, 0.4);
|
||||||
|
|
||||||
/* Diff blob colours, sampled from a rendered diff. */
|
/* Diff blob colors, sampled from a rendered diff. */
|
||||||
--add-bg: #263834; /* addition, code cell */
|
--add-bg: #263834; /* addition, code cell */
|
||||||
--add-gutter: #31503d; /* addition, line number cell */
|
--add-gutter: #31503d; /* addition, line number cell */
|
||||||
--add-word: #31583c; /* addition, word-level mark */
|
--add-word: #31583c; /* addition, word-level mark */
|
||||||
@@ -1153,7 +1153,7 @@ body.is-resizing {
|
|||||||
border-left-color: var(--claude);
|
border-left-color: var(--claude);
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
/* An outdated card keeps its status colour on the pill but takes the attention
|
/* An outdated card keeps its status color on the pill but takes the attention
|
||||||
edge, since "can't find the code any more" is what you need to notice first. */
|
edge, since "can't find the code any more" is what you need to notice first. */
|
||||||
.comment-card.is-outdated {
|
.comment-card.is-outdated {
|
||||||
border-left-color: var(--attention);
|
border-left-color: var(--attention);
|
||||||
@@ -1251,7 +1251,7 @@ body.is-resizing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* What stands in for a diff too large to render, once its warning has been
|
/* What stands in for a diff too large to render, once its warning has been
|
||||||
dismissed — the same centred treatment as .loading, plus the way back in. */
|
dismissed — the same centered treatment as .loading, plus the way back in. */
|
||||||
.oversize-notice {
|
.oversize-notice {
|
||||||
max-width: 560px;
|
max-width: 560px;
|
||||||
margin: 64px auto;
|
margin: 64px auto;
|
||||||
@@ -1370,7 +1370,7 @@ body.is-resizing {
|
|||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
/* Primer Label: 999px pill, coloured border, 12px text. */
|
/* Primer Label: 999px pill, colored border, 12px text. */
|
||||||
.file-status {
|
.file-status {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
@@ -1468,7 +1468,7 @@ body.is-resizing {
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* A viewed file steps back: its header greys out so the ones still to read are
|
/* A viewed file steps back: its header grays out so the ones still to read are
|
||||||
what the eye lands on. */
|
what the eye lands on. */
|
||||||
.file.is-viewed {
|
.file.is-viewed {
|
||||||
border-color: var(--border-muted);
|
border-color: var(--border-muted);
|
||||||
@@ -1529,7 +1529,7 @@ body.is-resizing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Outdated comments -------------------------------------------------- */
|
/* ---- Outdated comments -------------------------------------------------- */
|
||||||
/* Attention yellow, the same colour the gutter uses for a commented line: the
|
/* Attention yellow, the same color the gutter uses for a commented line: the
|
||||||
comment is intact, only its anchor is gone. Nothing here reads as an error. */
|
comment is intact, only its anchor is gone. Nothing here reads as an error. */
|
||||||
.outdated-note {
|
.outdated-note {
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
@@ -1719,7 +1719,7 @@ body.is-resizing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* A range being dragged, or one a pending comment is attached to. GitHub tints
|
/* A range being dragged, or one a pending comment is attached to. GitHub tints
|
||||||
commented lines with attention yellow rather than the accent colour. */
|
commented lines with attention yellow rather than the accent color. */
|
||||||
.diff-gutter-selected {
|
.diff-gutter-selected {
|
||||||
background: var(--attention-muted);
|
background: var(--attention-muted);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
@@ -1812,7 +1812,7 @@ body.is-resizing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ---- Primer prettylights syntax theme ----------------------------------- */
|
/* ---- Primer prettylights syntax theme ----------------------------------- */
|
||||||
/* Prism's token names mapped onto GitHub's syntax colours. Both places code is
|
/* Prism's token names mapped onto GitHub's syntax colors. Both places code is
|
||||||
highlighted share them: the diff, and a fenced block in a comment. */
|
highlighted share them: the diff, and a fenced block in a comment. */
|
||||||
:is(.diff, .md) .token.comment,
|
:is(.diff, .md) .token.comment,
|
||||||
:is(.diff, .md) .token.prolog,
|
:is(.diff, .md) .token.prolog,
|
||||||
|
|||||||
Reference in New Issue
Block a user