From 57719355bd627ce823681b096c7752818e1b5e29 Mon Sep 17 00:00:00 2001 From: Jiacai Liu Date: Tue, 4 Jul 2023 22:52:13 +0800 Subject: [PATCH] add night shift (#12) * add night shift * tidy * add args for night-shift * update CI * fix macos build * bump to macos-13 * debug * fix header * disable night-shift in CI --- .github/workflows/binary.yml | 41 ++++++- README.org | 27 ++++- build.zig | 34 +++++- src/night-shift.zig | 212 +++++++++++++++++++++++++++++++++++ src/util.zig | 10 +- 5 files changed, 313 insertions(+), 11 deletions(-) create mode 100644 src/night-shift.zig diff --git a/.github/workflows/binary.yml b/.github/workflows/binary.yml index 57fa5d9..2a4cfe3 100644 --- a/.github/workflows/binary.yml +++ b/.github/workflows/binary.yml @@ -4,7 +4,8 @@ on: workflow_dispatch: pull_request: paths: - - '.github/workflows/binary.yml' + - '**.zig' + - '.github/workflows/CI.yml' push: branches: - main @@ -12,9 +13,6 @@ on: - '**.zig' - '.github/workflows/binary.yml' -env: - BUILD_DIR: "build" - jobs: build: timeout-minutes: 10 @@ -31,6 +29,37 @@ jobs: - "x86_64-linux" - "arm-linux" - "aarch64-linux" + steps: + - uses: actions/checkout@v3 + with: + submodules: true + - uses: goto-bus-stop/setup-zig@v2 + with: + version: ${{ matrix.zig-version }} + - name: Set Environment Variables + run: | + echo "BUILD_DATE=$(date +'%Y-%m-%dT%H:%M:%S%z')" >> $GITHUB_ENV + - name: Build + run: | + zig build -Dtarget=${{ matrix.targets }} -Doptimize=ReleaseSafe \ + -Dgit_commit=${{ github.head_ref }}-${{ github.sha }} \ + -Dbuild_date=${{ env.BUILD_DATE }} + # https://github.com/actions/upload-artifact#maintaining-file-permissions-and-case-sensitive-files + tar -cvf zigcli.tar zig-out/bin/ + - name: Upload + uses: actions/upload-artifact@v3 + with: + name: zigcli-${{ matrix.targets }} + path: zigcli.tar + + build-macos: + timeout-minutes: 10 + runs-on: macos-latest + strategy: + fail-fast: false + matrix: + zig-version: [master] + targets: - "x86_64-macos" - "aarch64-macos" steps: @@ -43,11 +72,13 @@ jobs: - name: Set Environment Variables run: | echo "BUILD_DATE=$(date +'%Y-%m-%dT%H:%M:%S%z')" >> $GITHUB_ENV + # xcrun --show-sdk-path + # find $(xcrun --show-sdk-path) -name objc.h - name: Build run: | zig build -Dtarget=${{ matrix.targets }} -Doptimize=ReleaseSafe \ -Dgit_commit=${{ github.head_ref }}-${{ github.sha }} \ - -Dbuild_date=${{ env.BUILD_DATE }} + -Dbuild_date=${{ env.BUILD_DATE }} -Dis_ci=true # https://github.com/actions/upload-artifact#maintaining-file-permissions-and-case-sensitive-files tar -cvf zigcli.tar zig-out/bin/ - name: Upload diff --git a/README.org b/README.org index 901c4bd..98df3f1 100644 --- a/README.org +++ b/README.org @@ -1,6 +1,6 @@ #+TITLE: Zigcli #+DATE: 2022-09-20T22:55:17+0800 -#+LASTMOD: 2023-06-29T20:22:28+0800 +#+LASTMOD: 2023-07-04T22:26:07+0800 #+AUTHOR: Jiacai Liu #+EMAIL: dev@liujiacai.net #+OPTIONS: toc:nil num:nil @@ -14,6 +14,7 @@ Command line programs written in Zig. Currently there are: - =tree=, list contents of directories in a tree-like format. - =yes=, output a string repeatedly until killed. - =pidof=, like [[https://man7.org/linux/man-pages/man1/pidof.1.html][pidof]], but for macOS. +- =night-shift=, control [[https://support.apple.com/guide/mac-help/use-night-shift-mchl97bc676d/mac][Night Shift]] in cli, build for macOS. Prebuilt binaries can be found in [[https://github.com/jiacai2050/loc/actions/workflows/binary.yml][CI's artifacts]], or you can build from source: #+begin_src bash @@ -63,6 +64,30 @@ Ruby 1 8 5 2 1 201.00B -------- ---- ---- ---- ------- ----- ------- Total 17 1260 1072 42 146 34.46K #+end_src +* Night shift +#+begin_src bash :results verbatim code :exports both +./zig-out/bin/night-shift -h +#+end_src + +#+RESULTS: +#+begin_src bash + USAGE: + ./zig-out/bin/night-shift [OPTIONS] [--] + + Available commands by category: + Manual on/off control: + on Turn Night Shift on + off Turn Night Shift off + toggle Toggle Night Shift + + Color temperature: + temp View temperature preference + temp <0-100> Set temperature preference + + OPTIONS: + -v, --version Print version + -h, --help Print help information +#+end_src * Roadmap ** Loc diff --git a/build.zig b/build.zig index fcebf33..6d9ae3d 100644 --- a/build.zig +++ b/build.zig @@ -24,6 +24,7 @@ pub fn build(b: *Build) void { b.modules.put("build_info", opt.createModule()) catch @panic("OOM"); b.modules.put("simargs", simargs_dep.module("simargs")) catch @panic("OOM"); b.modules.put("table-helper", table_dep.module("table-helper")) catch @panic("OOM"); + const is_ci = b.option(bool, "is_ci", "Build in CI") orelse false; var all_tests = std.ArrayList(*Build.Step).init(b.allocator); inline for (.{ @@ -31,8 +32,9 @@ pub fn build(b: *Build) void { "loc", "pidof", "yes", + "night-shift", }) |prog_name| { - if (buildCli(b, prog_name, optimize, target)) |exe| { + if (buildCli(b, prog_name, optimize, target, is_ci)) |exe| { var deps = b.modules.iterator(); while (deps.next()) |dep| { exe.addModule(dep.key_ptr.*, dep.value_ptr.*); @@ -67,17 +69,41 @@ fn buildTestStep(b: *std.Build, comptime name: []const u8, target: std.zig.Cross return test_step; } -fn buildCli(b: *std.Build, comptime name: []const u8, optimize: std.builtin.Mode, target: std.zig.CrossTarget) ?*Build.CompileStep { - if (std.mem.eql(u8, name, "pidof")) { +fn buildCli( + b: *std.Build, + comptime name: []const u8, + optimize: std.builtin.Mode, + target: std.zig.CrossTarget, + is_ci: bool, +) ?*Build.CompileStep { + if (std.mem.eql(u8, name, "night-shift") or std.mem.eql(u8, name, "pidof")) { if (target.getOsTag() != .macos) { return null; } } - return b.addExecutable(.{ + if (is_ci and std.mem.eql(u8, name, "night-shift")) { + // zig build -Dtarget=aarch64-macos will throw error + // error: warning(link): library not found for '-lobjc' + // warning(link): Library search paths: + // warning(link): framework not found for '-framework CoreBrightness' + // warning(link): Framework search paths: + // warning(link): /System/Library/PrivateFrameworks + // so disable this in CI environment. + return null; + } + + const exe = b.addExecutable(.{ .name = name, .root_source_file = FileSource.relative("src/" ++ name ++ ".zig"), .target = target, .optimize = optimize, }); + + if (std.mem.eql(u8, name, "night-shift")) { + exe.linkSystemLibrary("objc"); + exe.addFrameworkPath("/System/Library/PrivateFrameworks"); + exe.linkFramework("CoreBrightness"); + } + return exe; } diff --git a/src/night-shift.zig b/src/night-shift.zig new file mode 100644 index 0000000..7370234 --- /dev/null +++ b/src/night-shift.zig @@ -0,0 +1,212 @@ +//! Control Night shift in cli, build for macOS. +//! +const std = @import("std"); +const simargs = @import("simargs"); +const util = @import("util.zig"); +const c = @cImport({ + @cInclude("objc/objc.h"); + @cInclude("objc/message.h"); +}); + +const Time = extern struct { + hour: c_int, + minute: c_int, +}; +const Schedule = extern struct { + from_time: Time, + to_time: Time, +}; + +// Refer https://github.com/smudge/nightlight/blob/03595a642f0876388db11b9f5a3bd8261ab178d5/src/macos/status.rs#L21 +const Status = extern struct { + active: bool, + enabled: bool, + sun_schedule_permitted: bool, + mode: c_int, + schedule: Schedule, + disable_flags: c_ulonglong, + available: bool, +}; + +const Client = struct { + inner: c.id, + allocator: std.mem.Allocator, + + const Self = @This(); + + fn init(allocator: std.mem.Allocator) Self { + // https://developer.limneos.net/?ios=14.4&framework=CoreBrightness.framework&header=CBBlueLightClient.h + const clazz = c.objc_getClass("CBBlueLightClient"); + const call: *fn (c.id, c.SEL) callconv(.C) c.id = @constCast(@ptrCast(&c.objc_msgSend)); + + return Self{ + .inner = call( + call(@alignCast(@ptrCast(clazz.?)), c.sel_registerName("alloc")), + c.sel_registerName("init"), + ), + .allocator = allocator, + }; + } + + fn getStatus(self: Self) !*Status { + var status = try self.allocator.create(Status); + const call: *fn (c.id, c.SEL, *Status) callconv(.C) bool = + @constCast(@ptrCast(&c.objc_msgSend)); + const ret = call(self.inner, c.sel_registerName("getBlueLightStatus:"), status); + if (!ret) { + return error.getBlueLightStatus; + } + + return status; + } + + fn setEnabled(self: Self, enabled: bool) !void { + const call: *fn (c.id, c.SEL, bool) callconv(.C) bool = @constCast(@ptrCast(&c.objc_msgSend)); + const ret = call(self.inner, c.sel_registerName("setEnabled:"), enabled); + if (!ret) { + return error.getStrength; + } + } + + fn turnOn(self: Self) !void { + return self.setEnabled(true); + } + + fn turnOff(self: Self) !void { + return self.setEnabled(false); + } + + fn getStrength(self: Self) !f32 { + var strength: f32 = 0; + const call: *fn (c.id, c.SEL, *f32) callconv(.C) bool = @constCast(@ptrCast(&c.objc_msgSend)); + const ret = call(self.inner, c.sel_registerName("getStrength:"), &strength); + if (!ret) { + return error.getStrength; + } + + return strength; + } + + fn setStrength(self: Self, strength: f32) !void { + const call: *fn (c.id, c.SEL, f32, bool) callconv(.C) bool = @constCast(@ptrCast(&c.objc_msgSend)); + const ret = call(self.inner, c.sel_registerName("setStrength:commit:"), strength, true); + if (!ret) { + return error.setStrength; + } + } + + fn destroyStatus(self: Self, status: *Status) void { + self.allocator.destroy(status); + } +}; + +const Action = enum { + Status, + On, + Off, + Toggle, + Temp, + + const FromString = std.ComptimeStringMap(@This(), .{ + .{ "status", .Status }, + .{ "on", .On }, + .{ "off", .Off }, + .{ "toggle", .Toggle }, + .{ "temp", .Temp }, + }); +}; + +pub fn main() !void { + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + const opt = try simargs.parse(allocator, struct { + version: bool = false, + help: bool = false, + + pub const __shorts__ = .{ + .version = .v, + .help = .h, + }; + + pub const __messages__ = .{ + .help = "Print help information", + .version = "Print version", + }; + }, + \\ + \\ + \\ Available commands by category: + \\ Manual on/off control: + \\ on Turn Night Shift on + \\ off Turn Night Shift off + \\ toggle Toggle Night Shift + \\ + \\ Color temperature: + \\ temp View temperature preference + \\ temp <0-100> Set temperature preference + , util.get_build_info()); + defer opt.deinit(); + + const action: Action = if (opt.positional_args.items.len == 0) + .Status + else + Action.FromString.get(opt.positional_args.items[0]) orelse .Status; + + const client = Client.init(allocator); + var wtr = std.io.getStdOut().writer(); + + switch (action) { + .Status => { + var status = try client.getStatus(); + defer client.destroyStatus(status); + + if (!status.enabled) { + try wtr.writeAll("enabled: off"); + return; + } + + const schedule = switch (status.mode) { + 0 => "Off", + 1 => "SunsetToSunrise", + 2 => try std.fmt.allocPrint(allocator, "Custom({d}:{d}-{d}:{d})", .{ + status.schedule.from_time.hour, + status.schedule.from_time.minute, + status.schedule.to_time.hour, + status.schedule.to_time.minute, + }), + else => "Unknown", + }; + try wtr.print( + \\Enabled: on + \\Schedule: {s} + \\Temperature: {d:.0} + , .{ schedule, try client.getStrength() * 100 }); + }, + .Temp => { + if (opt.positional_args.items.len == 2) { + const strength = try std.fmt.parseFloat(f32, opt.positional_args.items[1]); + try client.setStrength(strength / 100.0); + return; + } + + const strength = try client.getStrength(); + try wtr.print("{d:.0}\n", .{strength * 100}); + }, + .Toggle => { + var status = try client.getStatus(); + if (status.enabled) { + try client.turnOff(); + } else { + try client.turnOn(); + } + }, + .On => { + try client.turnOn(); + }, + .Off => { + try client.turnOff(); + }, + } +} diff --git a/src/util.zig b/src/util.zig index 9adf118..4d4992c 100644 --- a/src/util.zig +++ b/src/util.zig @@ -1,5 +1,6 @@ const std = @import("std"); const info = @import("build_info"); +const builtin = @import("builtin"); const mem = std.mem; pub const StringUtil = struct { @@ -17,8 +18,15 @@ pub const StringUtil = struct { }; pub fn get_build_info() []const u8 { - return std.fmt.comptimePrint("Build date: {s}\nGit commit: {s}", .{ + return std.fmt.comptimePrint( + \\Git commit: {s} + \\Build date: {s} + \\Zig version: {s} + \\Zig backend: {s} + , .{ info.build_date, info.git_commit, + builtin.zig_version_string, + builtin.zig_backend, }); }