Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add night shift #12

Merged
merged 9 commits into from
Jul 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 36 additions & 5 deletions .github/workflows/binary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,15 @@ on:
workflow_dispatch:
pull_request:
paths:
- '.github/workflows/binary.yml'
- '**.zig'
- '.github/workflows/CI.yml'
push:
branches:
- main
paths:
- '**.zig'
- '.github/workflows/binary.yml'

env:
BUILD_DIR: "build"

jobs:
build:
timeout-minutes: 10
Expand All @@ -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:
Expand All @@ -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
Expand Down
27 changes: 26 additions & 1 deletion README.org
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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] [--] <command>

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
Expand Down
34 changes: 30 additions & 4 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,17 @@ 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 (.{
"tree",
"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.*);
Expand Down Expand Up @@ -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;
}
212 changes: 212 additions & 0 deletions src/night-shift.zig
Original file line number Diff line number Diff line change
@@ -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",
};
},
\\<command>
\\
\\ 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();
},
}
}
Loading