diff options
Diffstat (limited to 'bin/statusline.ha')
| -rw-r--r-- | bin/statusline.ha | 319 |
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); + }; +}; |
