Changes
6 changed files (+314/-33)
-
-
@@ -19,6 +19,12 @@ config = lib.mkIf config.features.wayland-de.enable {home.packages = [ # /programs/theme pkgs.my-theme # Sunwait calculates sunrise or sunset times with civil, nautical, # astronomical and custom twilights, for use with Windows Task Scheduler # or 'cron' on Linux. # https://github.com/risacher/sunwait pkgs.sunwait ]; systemd.user.services.my-theme = {
-
@@ -27,23 +33,14 @@ Description = "Apply appearance theme based on time";}; Service = { Type = "oneshot"; ExecStart = "${pkgs.my-theme}/bin/,theme auto"; }; }; systemd.user.timers.my-theme = { Unit = { Description = "Apply appearance theme periodically and at start up"; Type = "simple"; Restart = "always"; RestartSec = 1; ExecStart = "${pkgs.my-theme}/bin/,theme auto --config ${config.xdg.configHome}/my-theme/config.json --daemon --verbose"; }; Install = { WantedBy = [ "timers.target" ]; }; Timer = { OnCalendar = "hourly"; Persistent = true; WantedBy = [ "default.target" ]; }; }; };
-
-
-
@@ -223,6 +223,7 @@ name = system;value = pkgs.mkShell { packages = with pkgs; [ sunwait tzdata glib zig
-
-
-
@@ -14,7 +14,7 @@ # limitations under the License.# # SPDX-License-Identifier: Apache-2.0 { glib, tzdata, pkg-config, stdenvNoCC, installShellFiles, zig }: { glib, sunwait, tzdata, pkg-config, stdenvNoCC, installShellFiles, zig }: stdenvNoCC.mkDerivation rec { pname = "my-theme"; version = "1.0.0";
-
-
-
@@ -0,0 +1,19 @@// Copyright 2025 Shota FUJI <pockawoooh@gmail.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 const sunwait = @import("./sunwait.zig"); location: sunwait.Location = .{},
-
-
-
@@ -18,9 +18,29 @@ //! Over-powered theme switcher.const std = @import("std"); const Config = @import("./Config.zig"); const gnome = @import("./gnome.zig"); const sunwait = @import("./sunwait.zig"); const Variant = @import("./variant.zig").Variant; pub const std_options = std.Options{ .log_level = .debug, .logFn = log, }; var log_level: std.log.Level = .info; pub fn log( comptime level: std.log.Level, comptime scope: @Type(.enum_literal), comptime format: []const u8, args: anytype, ) void { if (@intFromEnum(level) <= @intFromEnum(log_level)) { std.log.defaultLog(level, scope, format, args); } } const ExitCode = enum(u8) { ok = 0, generic_error = 1,
-
@@ -50,6 +70,11 @@ };} }; fn apply(allocator: std.mem.Allocator, variant: Variant) ExitCode { gnome.apply(allocator, variant) catch {}; return ExitCode.ok; } pub fn main() !u8 { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit();
-
@@ -62,32 +87,111 @@// Skip program name. _ = iter.next(); const initial_arg = iter.next() orelse { std.log.err("Argument is required.", .{}); return ExitCode.incorrect_usage.to_u8(); }; var arg_config_path: ?[]const u8 = null; defer if (arg_config_path) |p| allocator.free(p); const variant_unresolved = UnresolvedVariant.fromString(initial_arg) catch { std.log.err("Unknown variant \"{s}\".", .{initial_arg}); return ExitCode.incorrect_usage.to_u8(); }; var arg_variant_unresolved: ?UnresolvedVariant = null; const variant: Variant = switch (variant_unresolved) { .auto => Variant.fromTime() catch |err| { std.log.err("Unable to resolve variant: {s}", .{@errorName(err)}); return ExitCode.generic_error.to_u8(); }, .manual => |v| v, var is_daemon: bool = false; while (iter.next()) |arg| { if (std.mem.eql(u8, arg, "--config")) { if (arg_config_path) |_| { std.log.err("--config is already set", .{}); return ExitCode.incorrect_usage.to_u8(); } const value = iter.next() orelse { std.log.err("--config option requires a value", .{}); return ExitCode.incorrect_usage.to_u8(); }; arg_config_path = try allocator.dupe(u8, value); continue; } if (std.mem.eql(u8, arg, "--verbose")) { log_level = .debug; continue; } if (std.mem.eql(u8, arg, "--daemon")) { is_daemon = true; continue; } if (arg_variant_unresolved) |_| { std.log.err("Variant is already set (reading \"{s}\")", .{arg}); return ExitCode.incorrect_usage.to_u8(); } arg_variant_unresolved = UnresolvedVariant.fromString(arg) catch { std.log.err("Unknown variant \"{s}\".", .{arg}); return ExitCode.incorrect_usage.to_u8(); }; } const variant_unresolved = arg_variant_unresolved orelse { std.log.err("Variant is required", .{}); return ExitCode.incorrect_usage.to_u8(); }; if (iter.next()) |_| { std.log.err("Too many arguments.", .{}); if (is_daemon and variant_unresolved != .auto) { std.log.err("--daemon option is only available for \"auto\" variant", .{}); return ExitCode.incorrect_usage.to_u8(); } gnome.apply(allocator, variant) catch {}; switch (variant_unresolved) { .auto => { if (arg_config_path) |config_path| { const file = std.fs.cwd().openFile(config_path, .{}) catch |err| { std.log.err("Unable to open config file at {s}: {s}", .{ config_path, @errorName(err) }); return ExitCode.generic_error.to_u8(); }; defer file.close(); return ExitCode.ok.to_u8(); var config_reader = std.json.reader(allocator, file.reader()); defer config_reader.deinit(); const config = std.json.parseFromTokenSource(Config, allocator, &config_reader, .{}) catch |err| { std.log.err("Unable to parse config file at {s}: {s}", .{ config_path, @errorName(err) }); return ExitCode.generic_error.to_u8(); }; defer config.deinit(); if (is_daemon) { while (true) { const current = sunwait.poll(allocator, config.value.location) catch |err| { std.log.err("Failed to get current suntime: {s}", .{@errorName(err)}); return ExitCode.generic_error.to_u8(); }; _ = apply(allocator, current.toVariant()); sunwait.wait(allocator, config.value.location) catch |err| { std.log.err("Failed to wait for suntime event: {s}", .{@errorName(err)}); return ExitCode.generic_error.to_u8(); }; } } const current = sunwait.poll(allocator, config.value.location) catch |err| { std.log.err("Failed to get current suntime: {s}", .{@errorName(err)}); return ExitCode.generic_error.to_u8(); }; return apply(allocator, current.toVariant()).to_u8(); } const variant = Variant.fromTime() catch |err| { std.log.err("Unable to resolve variant: {s}", .{@errorName(err)}); return ExitCode.generic_error.to_u8(); }; return apply(allocator, variant).to_u8(); }, .manual => |variant| { return apply(allocator, variant).to_u8(); }, } } test {
-
-
-
@@ -0,0 +1,160 @@// Copyright 2025 Shota FUJI <pockawoooh@gmail.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // SPDX-License-Identifier: Apache-2.0 //! Helper functions for running "sunwait" command. //! That program is written in plain C but contains "main" in its core file //! so we can't use that as a library. For simplicity, this module spawns //! "sunwait" command instead. const std = @import("std"); const Variant = @import("./variant.zig").Variant; pub const Error = error{ UnexpectedExitCode, CommandNotFound, CommandInterrupted, CommandUnexpectedlyTerminated, } || std.mem.Allocator.Error || std.process.Child.RunError; pub const Location = struct { latitude: f64 = 0.0, longitude: f64 = 0.0, /// Caller is responsible to release returned buffer using `allocator.free`. fn getLatitudeArg(self: Location, allocator: std.mem.Allocator) std.mem.Allocator.Error![]const u8 { const dir: u8 = if (self.latitude < 0) 'S' else 'N'; return std.fmt.allocPrint( allocator, "{d:.4}{u}", .{ self.latitude, dir }, ); } /// Caller is responsible to release returned buffer using `allocator.free`. fn getLongitudeArg(self: Location, allocator: std.mem.Allocator) std.mem.Allocator.Error![]const u8 { const dir: u8 = if (self.longitude < 0) 'W' else 'E'; return std.fmt.allocPrint( allocator, "{d:.4}{u}", .{ self.longitude, dir }, ); } }; pub const PollResult = enum { day, night, pub fn toVariant(self: PollResult) Variant { return switch (self) { .day => Variant.light, .night => Variant.dark, }; } }; pub fn poll(allocator: std.mem.Allocator, location: Location) Error!PollResult { std.log.debug("Getting suntime state...", .{}); const lat_arg = try location.getLatitudeArg(allocator); defer allocator.free(lat_arg); const lon_arg = try location.getLongitudeArg(allocator); defer allocator.free(lon_arg); const run_result = std.process.Child.run(.{ .allocator = allocator, .argv = &.{ "sunwait", "poll", lat_arg, lon_arg, }, }) catch |err| return switch (err) { error.FileNotFound => Error.CommandNotFound, else => err, }; allocator.free(run_result.stderr); allocator.free(run_result.stdout); switch (run_result.term) { .Exited => |code| switch (code) { 2 => return .day, 3 => return .night, else => { std.log.warn("sunwait exited unexpectedly: exit code={d}", .{code}); return Error.UnexpectedExitCode; }, }, .Signal => |sig| { std.log.warn("sunwait(signal): {d}", .{sig}); return Error.CommandInterrupted; }, else => { std.log.err("sunwait terminated abnormally: {s}", .{@tagName(run_result.term)}); return Error.CommandUnexpectedlyTerminated; }, } } /// Block the running thread until day/night changes. pub fn wait(allocator: std.mem.Allocator, location: Location) Error!void { std.log.debug("Waiting suntime events...", .{}); const lat_arg = try location.getLatitudeArg(allocator); defer allocator.free(lat_arg); const lon_arg = try location.getLongitudeArg(allocator); defer allocator.free(lon_arg); const run_result = std.process.Child.run(.{ .allocator = allocator, .argv = &.{ "sunwait", "wait", lat_arg, lon_arg, }, }) catch |err| return switch (err) { error.FileNotFound => Error.CommandNotFound, else => err, }; allocator.free(run_result.stderr); allocator.free(run_result.stdout); switch (run_result.term) { .Exited => |code| switch (code) { 0 => { // sunwait returns incorrect result when invoked exactly at the sunrise/sunset time. std.log.debug("Waiting 5 seconds for accurate result...", .{}); std.Thread.sleep(std.time.ns_per_s * 5); return; }, else => { std.log.warn("sunwait exited unexpectedly: exit code={d}", .{code}); return Error.UnexpectedExitCode; }, }, .Signal => |sig| { std.log.warn("sunwait(signal): {d}", .{sig}); return Error.CommandInterrupted; }, else => { std.log.err("sunwait terminated abnormally: {s}", .{@tagName(run_result.term)}); return Error.CommandUnexpectedlyTerminated; }, } }
-