Add notifications.
This commit is contained in:
1 parent
afad6a2636
commit
7414c89fe6
33 files changed
+1026
-381
No files matched your search
+69
-27
@@ -4,8 +4,8 @@
|
||||
//! something changes. It carries a version from the start, so adding the next
|
||||
//! setting is not also the day we invent a format.
|
||||
//!
|
||||
//! The settings are three different shapes. The colour scheme is one word. The
|
||||
//! **palette changes** are a base colour per scheme, plus a colour per name for
|
||||
//! The settings are three different shapes. The color scheme is one word. The
|
||||
//! **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
|
||||
//! list: which
|
||||
//! saved layout to open, what to fill its parameters in with, and what to call
|
||||
@@ -15,9 +15,9 @@
|
||||
//! field.
|
||||
//!
|
||||
//! 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
|
||||
//! 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
|
||||
//! **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
|
||||
@@ -73,8 +73,8 @@ pub const Theme = enum {
|
||||
}
|
||||
};
|
||||
|
||||
/// What someone has done to the palette, per scheme: the colour it is generated
|
||||
/// from, and the individual colours they have set by hand.
|
||||
/// What someone has done to the palette, per scheme: the color it is generated
|
||||
/// from, and the individual colors they have set by hand.
|
||||
///
|
||||
/// 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
|
||||
@@ -137,8 +137,17 @@ theme: Theme = .system,
|
||||
/// a mistyped chord and every shell in every tab exiting.
|
||||
confirm_quit: bool = true,
|
||||
|
||||
/// Changes to the palette. Empty means every colour is at its default, which is
|
||||
/// the state anyone who never opens the colour editor stays in — and it is the
|
||||
/// Whether a pane finishing posts a desktop notification.
|
||||
///
|
||||
/// 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.
|
||||
colors: Colors = .{},
|
||||
|
||||
@@ -183,7 +192,7 @@ pub fn deinit() void {
|
||||
|
||||
/// 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
|
||||
/// 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
|
||||
@@ -259,6 +268,10 @@ fn parse(self: *Settings, text: []const u8) ParseError!void {
|
||||
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("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": {…}}`.
|
||||
///
|
||||
/// Nothing here can fail the file. A colour 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 —
|
||||
/// Nothing here can fail the file. A color is one word of a palette, and the
|
||||
/// 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
|
||||
/// newer version and a typo in a hand-edited file cost the same nothing.
|
||||
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();
|
||||
while (it.next()) |kv| {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -313,7 +326,7 @@ fn parseColors(self: *Settings, raw: std.json.Value) void {
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -325,7 +338,7 @@ fn parseColors(self: *Settings, raw: std.json.Value) void {
|
||||
/// Read the generated palettes: `{"dark": {"base": "#3a76f0", "contrast": 0.5}}`.
|
||||
///
|
||||
/// 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
|
||||
/// contrast costs only the contrast: the base is the setting, and dropping the
|
||||
/// 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 {
|
||||
std.log.warn("settings: \"{s}\" is not a #rrggbb colour", .{text});
|
||||
std.log.warn("settings: \"{s}\" is not a #rrggbb color", .{text});
|
||||
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:
|
||||
/// 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
|
||||
/// binding and is logged; what it falls back to is the shipped default, which is
|
||||
/// 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.objectField("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
|
||||
// the startup list below is: a file you have opened to see what you can put
|
||||
// in it should name what it accepts.
|
||||
@@ -636,7 +651,7 @@ fn serialize(self: *Settings) SaveError![]u8 {
|
||||
}
|
||||
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
|
||||
// because that is the order they apply in, and a settings file is read by
|
||||
// people as well as by this.
|
||||
@@ -657,7 +672,7 @@ fn serialize(self: *Settings) SaveError![]u8 {
|
||||
try json.endObject();
|
||||
|
||||
// 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
|
||||
// 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
|
||||
@@ -738,7 +753,7 @@ fn configDir(buf: []u8) ?[:0]const u8 {
|
||||
//
|
||||
// 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
|
||||
// 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
|
||||
// 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);
|
||||
}
|
||||
|
||||
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" {
|
||||
var settings = forTesting();
|
||||
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("#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.
|
||||
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));
|
||||
}
|
||||
|
||||
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();
|
||||
defer settings.arena.deinit();
|
||||
|
||||
try settings.parse(
|
||||
\\{"theme": "dark", "colors": {"dark": {
|
||||
\\ "not_a_colour": "#123456",
|
||||
\\ "not_a_color": "#123456",
|
||||
\\ "border": "rebeccapurple",
|
||||
\\ "sidebar": 12,
|
||||
\\ "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(.sidebar));
|
||||
|
||||
// The whole file survives, including the theme above the colours and the
|
||||
// one colour in the list that was well formed.
|
||||
// The whole file survives, including the theme above the colors and the
|
||||
// one color in the list that was well formed.
|
||||
try std.testing.expectEqual(Theme.dark, settings.theme);
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
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();
|
||||
defer settings.arena.deinit();
|
||||
|
||||
|
||||
Reference in new issue
Block a user