// SPDX-FileCopyrightText: 2024 Sam Nystrom // 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, " 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, ">{}", 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); }; };