summaryrefslogtreecommitdiff
path: root/bin/statusline.ha
diff options
context:
space:
mode:
Diffstat (limited to 'bin/statusline.ha')
-rw-r--r--bin/statusline.ha319
1 files changed, 319 insertions, 0 deletions
diff --git a/bin/statusline.ha b/bin/statusline.ha
new file mode 100644
index 0000000..4eed68d
--- /dev/null
+++ b/bin/statusline.ha
@@ -0,0 +1,319 @@
+// SPDX-FileCopyrightText: 2024 Sam Nystrom <sam@samnystrom.dev>
+// SPDX-License-Identifier: ISC
+
+use encoding::utf8;
+use fmt;
+use fs;
+use io;
+use memio;
+use os;
+use os::exec;
+use strconv;
+use strings;
+use time;
+use time::date;
+
+def ROSEWATER: str = "#f5e0dc";
+def FLAMINGO: str = "#f2cdcd";
+def PINK: str = "#f5c2e7";
+def MAUVE: str = "#cba6f7";
+def RED: str = "#f38ba8";
+def MAROON: str = "#eba0ac";
+def PEACH: str = "#fab387";
+def YELLOW: str = "#f9e2af";
+def GREEN: str = "#a6e3a1";
+def TEAL: str = "#94e2d5";
+def SKY: str = "#89dceb";
+def SAPPHIRE: str = "#74c7ec";
+def BLUE: str = "#89b4fa";
+def LAVENDER: str = "#b4befe";
+def TEXT: str = "#cdd6f4";
+def SUBTEXT1: str = "#bac2de";
+def SUBTEXT0: str = "#a6adc8";
+def OVERLAY2: str = "#9399b2";
+def OVERLAY1: str = "#7f849c";
+def OVERLAY0: str = "#6c7086";
+def SURFACE2: str = "#585b70";
+def SURFACE1: str = "#45475a";
+def SURFACE0: str = "#313244";
+def BASE: str = "#1e1e2e";
+def MANTLE: str = "#181825";
+def CRUST: str = "#11111b";
+
+def FG: [_]str = [MAUVE, RED, YELLOW, GREEN, SAPPHIRE];
+def BG: str = BASE;
+def TXT: str = BASE;
+
+type error = !(exec::error | !exec::exit_status | utf8::invalid | strconv::invalid | strconv::overflow | io::error | fs::error);
+
+fn strerror(err: error) const str = {
+ match (err) {
+ case let err: io::error =>
+ return io::strerror(err);
+ case let err: fs::error =>
+ return fs::strerror(err);
+ case let err: exec::error =>
+ return exec::strerror(err);
+ case let err: !exec::exit_status =>
+ return "command exited nonzero";
+ case utf8::invalid =>
+ return "invalid UTF-8";
+ case strconv::invalid =>
+ return "invalid number";
+ case strconv::overflow =>
+ return "invalid number";
+ };
+};
+
+type json = ([](str, json) | []json | str | f64 | int | bool | void);
+
+fn print_json_str(out: io::handle, s: str) (void | io::error) = {
+ fmt::fprint(out, "\"")?;
+ let it = strings::iter(s);
+ for (let r => strings::next(&it)) {
+ switch (r) {
+ case '\b' =>
+ fmt::fprint(out, "\b")?;
+ case '\f' =>
+ fmt::fprint(out, "\f")?;
+ case '\n' =>
+ fmt::fprint(out, "\n")?;
+ case '\r' =>
+ fmt::fprint(out, "\r")?;
+ case '\t' =>
+ fmt::fprint(out, "\t")?;
+ case '\"' =>
+ fmt::fprint(out, "\\\"")?;
+ case '\\' =>
+ fmt::fprint(out, "\\\\")?;
+ case =>
+ if (r: u32 < 0x20) {
+ fmt::fprintf(out, "\\u{:.4x}", r: u32)?;
+ } else {
+ fmt::fprint(out, r)?;
+ };
+ };
+ };
+ fmt::fprint(out, "\"")?;
+};
+
+fn dump(out: io::handle, value: json) (void | io::error) = {
+ match (value) {
+ case let obj: [](str, json) =>
+ fmt::fprint(out, "{")?;
+ for (let i = 0z; i < len(obj); i += 1) {
+ if (i > 0) fmt::fprint(out, ",")?;
+ print_json_str(out, obj[i].0)?;
+ fmt::fprint(out, ":")?;
+ dump(out, obj[i].1)?;
+ };
+ fmt::fprint(out, "}")?;
+ case let arr: []json =>
+ fmt::fprint(out, "[")?;
+ for (let i = 0z; i < len(arr); i += 1) {
+ if (i > 0) fmt::fprint(out, ",")?;
+ dump(out, arr[i])?;
+ };
+ fmt::fprint(out, "]")?;
+ case let s: str =>
+ print_json_str(out, s)?;
+ case let x: f64 =>
+ fmt::fprint(out, x)?;
+ case let n: int =>
+ fmt::fprint(out, n)?;
+ case let b: bool =>
+ fmt::fprint(out, b)?;
+ case void =>
+ fmt::fprint(out, "null")?;
+ };
+};
+
+type span = (str, (str | void), (str | void));
+
+fn spans(spans: (str | span)...) str = {
+ let buf = memio::dynamic();
+ defer io::close(&buf)!;
+ for (let sp .. spans) {
+ match (sp) {
+ case let s: str =>
+ fmt::fprint(&buf, s)!;
+ case let span: span =>
+ let (s, fg, bg) = span;
+ fmt::fprint(&buf, "<span")!;
+ match (fg) {
+ case let fg: str =>
+ fmt::fprintf(&buf, " foreground=\"{}\"", fg)!;
+ case void => void;
+ };
+ match (bg) {
+ case let bg: str =>
+ fmt::fprintf(&buf, " background=\"{}\"", bg)!;
+ case void => void;
+ };
+ fmt::fprintf(&buf, ">{}</span>", s)!;
+ };
+ };
+ return strings::dup(memio::string(&buf)!);
+};
+
+fn run(name: str, args: str...) (str | error) = {
+ let cmd = exec::cmd(name, args...)?;
+ let pipe = exec::pipe();
+ exec::addfile(&cmd, os::stdout_file, pipe.1);
+ let proc = exec::start(&cmd)?;
+ io::close(pipe.1)?;
+ let output = io::drain(pipe.0)?;
+ io::close(pipe.0)?;
+ let status = exec::wait(&proc)?;
+ exec::check(&status)?;
+ return strings::fromutf8(output)?;
+};
+
+fn get_volume() ((uint, bool) | error) = {
+ let output = run("wpctl", "get-volume", "@DEFAULT_AUDIO_SINK@")?;
+ defer free(output);
+ let muted = strings::contains(output, "MUTED");
+ let volume = strconv::stof64(strings::cut(strings::cut(strings::rtrim(output), " ").1, " ").0)?;
+ return ((volume * 100.0): uint, muted);
+};
+
+fn volume() (void | error) = {
+ let (volume, muted) = get_volume()?;
+ let symbol = if (muted) " " else if (volume == 0) "" else if (volume <= 50) "" else " ";
+ let text = fmt::asprintf(" {} {}%", symbol, volume);
+ defer free(text);
+ let text = spans((" ", FG[0], BG), (text, TXT, FG[0]), (" ", BG, FG[0]));
+ defer free(text);
+ dump(os::stdout, [("full_text", text), ("markup", "pango"), ("separator_block_width", 0)]: [](str, json))!;
+};
+
+fn network() (void | error) = {
+ let output = run("iwctl", "station", "wlan0", "get-networks")?;
+ defer free(output);
+ let line = "";
+ let iter = strings::tokenize(output, "\n");
+ for (let s => strings::next_token(&iter)) {
+ if (strings::contains(s, '>')) {
+ line = s;
+ break;
+ };
+ };
+ let ssid = strings::cut(strings::cut(strings::cut(line, " ").1, " ").1, " ").0;
+ let text = if (ssid != "")
+ fmt::asprintf(" 󰖩 {}", ssid)
+ else
+ fmt::asprint(" 󰖪 ");
+ defer free(text);
+ let text = spans(("", FG[1], BG), (text, TXT, FG[1]), (" ", BG, FG[1]));
+ defer free(text);
+ dump(os::stdout, [("full_text", text), ("markup", "pango"), ("separator_block_width", 0)]: [](str, json))!;
+};
+
+fn get_brightness() (uint | error) = {
+ let output = run("brightctl")?;
+ defer free(output);
+ return strconv::stou(strings::rtrim(output))?;
+};
+
+fn brightness() (void | error) = {
+ let text = fmt::asprintf("  {}%", get_brightness()?);
+ defer free(text);
+ let text = spans(("", FG[2], BG), (text, TXT, FG[2]), (" ", BG, FG[2]));
+ defer free(text);
+ dump(os::stdout, [("full_text", text), ("markup", "pango"), ("separator_block_width", 0)]: [](str, json))!;
+};
+
+fn battery() (void | error) = {
+ let file = os::open("/sys/class/power_supply/BAT0/capacity")?;
+ let content = io::drain(file)?;
+ io::close(file)?;
+ defer free(content);
+ let capacity = strconv::stou(strings::rtrim(strings::fromutf8(content)?))?;
+
+ let file = os::open("/sys/class/power_supply/BAT0/status")?;
+ let content = io::drain(file)?;
+ io::close(file)?;
+ defer free(content);
+ let status = strings::rtrim(strings::fromutf8(content)?);
+
+ let symbol = "ERROR";
+ let i = if (capacity == 0) 0u else (capacity - 1) / 10;
+ switch (status) {
+ case "Discharging" =>
+ symbol = ["󰁺", "󰁻", "󰁼", "󰁽", "󰁾", "󰁿", "󰂀", "󰂁", "󰂂", "󰁹"][i]; // 󰂃
+ case "Charging" =>
+ symbol = ["󰢜", "󰂆", "󰂇", "󰂈", "󰢝", "󰂉", "󰢞", "󰂊", "󰂋", "󰂅"][i];
+ case "Full" =>
+ symbol = "󰂄";
+ case => void;
+ };
+ let text = fmt::asprintf(" {} {}%", symbol, capacity);
+ defer free(text);
+ let text = spans(("", FG[3], BG), (text, TXT, FG[3]), (" ", BG, FG[3]));
+ defer free(text);
+ dump(os::stdout, [("full_text", text), ("markup", "pango"), ("separator_block_width", 0)]: [](str, json))!;
+};
+
+fn clock() (void | error) = {
+ // this used to be %-d which disabled padding, but hare doesn't support it
+ let buf = memio::dynamic();
+ defer io::close(&buf)!;
+ date::format(&buf, "  %b %d %H:%M:%S ", &date::now())?;
+ let buf_text = memio::string(&buf)!;
+ let text = spans(("", FG[4], BG), (buf_text, TXT, FG[4]), (" ", FG[4], BG));
+ defer free(text);
+ dump(os::stdout, [("full_text", text), ("markup", "pango"), ("separator_block_width", 0)]: [](str, json))!;
+};
+
+export fn main() void = {
+ dump(os::stdout, [("version", 1), ("click_events", false)]: [](str, json))!;
+ fmt::println()!;
+ fmt::println("[")!;
+
+ let last_vol = 0u;
+ let last_brt = 0u;
+ let last_mute = false;
+ let sleep_dur = time::SECOND;
+
+ for (true) {
+ let (vol, mute) = match (get_volume()) {
+ case let x: (uint, bool) =>
+ yield x;
+ case let err: error =>
+ fmt::errorln("statusbar: error:", strerror(err))!;
+ yield (last_vol, last_mute);
+ };
+ let brt = match (get_brightness()) {
+ case let x: uint =>
+ yield x;
+ case let err: error =>
+ fmt::errorln("statusbar: error:", strerror(err))!;
+ yield last_brt;
+ };
+
+ if (last_vol != vol || last_mute != mute || last_brt != brt) {
+ sleep_dur = time::MILLISECOND * 100;
+ } else if (sleep_dur < time::SECOND) {
+ sleep_dur += time::MILLISECOND * 100;
+ } else {
+ sleep_dur = time::SECOND;
+ };
+ last_vol = vol;
+ last_mute = mute;
+ last_brt = brt;
+
+ let segments: []*fn() (void | error) = [&volume, &network, &brightness, &battery, &clock];
+ fmt::print("[")!;
+ for (let i = 0z; i < len(segments); i += 1) {
+ if (i > 0) fmt::print(",")!;
+ match (segments[i]()) {
+ case void => void;
+ case let err: error =>
+ fmt::errorln("statusbar: error:", strerror(err))!;
+ };
+ };
+ fmt::println("],")!;
+
+ time::sleep(sleep_dur);
+ };
+};