From bf09d4d99c7968b36af7ab37d4303d7ddaf6fa2e Mon Sep 17 00:00:00 2001 From: MeghdadFadaee Date: Mon, 27 Jul 2026 18:03:18 +0330 Subject: [PATCH] feat(cli): add height-aware compact outage schedule --- README.md | 9 ++- main.sh | 155 ++++++++++++++++++++++++++++++++++++++------- tests/test_main.sh | 15 +++++ 3 files changed, 153 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index cdba422..2cff1ef 100644 --- a/README.md +++ b/README.md @@ -232,11 +232,16 @@ The terminal output uses English-only labels and omits the stored address and reason text. When there are no qualifying events for any bill it prints an `ALL CLEAR` screen and exits successfully. +Schedule entries use one compact row each. When every outage cannot fit below +the countdown, the dashboard keeps the nearest entries visible and reports how +many additional outages were omitted. Resizing the terminal reveals more rows. + Colors are enabled only when output is attached to a terminal. Set `NO_COLOR` to -disable them explicitly, or set `COLUMNS` to control the rendered width: +disable them explicitly, or set `COLUMNS` and `LINES` to control the rendered +size: ```sh -NO_COLOR=1 COLUMNS=80 ./main.sh +NO_COLOR=1 COLUMNS=80 LINES=24 ./main.sh ``` When `pyfiglet` or `figlet` provides the `univers` font, the countdown uses it diff --git a/main.sh b/main.sh index 25e85ef..6ab4f19 100755 --- a/main.sh +++ b/main.sh @@ -74,6 +74,7 @@ Options: Environment: NO_COLOR Disable ANSI colors when set COLUMNS Override the detected output width + LINES Override the detected output height Required commands: Bash 4.3+, curl, jq, and GNU date EOF @@ -415,6 +416,38 @@ terminal_columns() { printf '%s\n' "$columns" } +terminal_lines() { + local lines="${LINES:-}" + local terminal_size="" detected_lines="" + + if [[ -z "$lines" ]] && command -v stty >/dev/null 2>&1; then + if [[ -r /dev/tty ]]; then + terminal_size="$(stty size 2>/dev/null /dev/null 2>&1; then + lines="$(tput lines 2>/dev/null || true)" + fi + + if [[ ! "$lines" =~ ^[0-9]+$ ]]; then + lines=24 + elif ((lines < 10)); then + lines=10 + fi + printf '%s\n' "$lines" +} + color_setup() { RESET="" BOLD="" @@ -714,10 +747,29 @@ human_duration() { printf '%s\n' "$output" } +compact_reference() { + local bill_id="$1" + local outage_number="$2" + local width="$3" + local reference="$bill_id/$outage_number" + local bill_width + + if ((${#reference} <= width)); then + printf '%s' "$reference" + elif ((width > ${#outage_number} + 2)); then + bill_width=$((width - ${#outage_number} - 2)) + printf '~%s/%s' "${bill_id: -bill_width}" "$outage_number" + elif ((width > 1)); then + printf '%s~' "${reference:0:width-1}" + else + printf '~' + fi +} + render_event() { local line="$1" - local now_epoch="$2" - local ignored phase start_epoch stop_epoch record type_label timing + local columns="$2" + local ignored phase start_epoch stop_epoch record type_label reference reference_width local -a fields IFS=$'\t' read -r ignored phase start_epoch stop_epoch record <<<"$line" @@ -725,18 +777,24 @@ render_event() { type_label="EMERGENCY" [[ "${fields[8]}" == "true" ]] && type_label="PLANNED" - if [[ "$phase" == "ONGOING" ]]; then - timing="ends in $(human_duration "$((stop_epoch - now_epoch))")" + if ((columns >= 60)); then + reference_width=$((columns - 47)) + reference="$(compact_reference "${fields[0]}" "${fields[1]}" "$reference_width")" + printf '%s%-9s%s %s%-9s%s %-10s %-11s %s\n' \ + "$([[ "$phase" == "ONGOING" ]] && printf '%s' "$RED" || printf '%s' "$CYAN")" \ + "$phase" "$RESET" \ + "$([[ "$type_label" == "PLANNED" ]] && printf '%s' "$GREEN" || printf '%s' "$YELLOW")" \ + "$type_label" "$RESET" "${fields[3]}" "${fields[4]}-${fields[5]}" "$reference" else - timing="starts in $(human_duration "$((start_epoch - now_epoch))")" + reference_width=$((columns - 34)) + ((reference_width < 1)) && reference_width=1 + reference="$(compact_reference "${fields[0]}" "${fields[1]}" "$reference_width")" + printf '%s%-4s%s %-5s %-10s %-11s %s\n' \ + "$([[ "$phase" == "ONGOING" ]] && printf '%s' "$RED" || printf '%s' "$CYAN")" \ + "$([[ "$phase" == "ONGOING" ]] && printf 'ON' || printf 'UP')" "$RESET" \ + "$([[ "$type_label" == "PLANNED" ]] && printf 'PLAN' || printf 'EMERG')" \ + "${fields[3]}" "${fields[4]}-${fields[5]}" "$reference" fi - - printf '%s%s%-10s%s %s%-9s%s %s\n' \ - "$BOLD" "$([[ "$phase" == "ONGOING" ]] && printf '%s' "$RED" || printf '%s' "$CYAN")" \ - "$phase" "$RESET" "$([[ "$type_label" == "PLANNED" ]] && printf '%s' "$GREEN" || printf '%s' "$YELLOW")" \ - "$type_label" "$RESET" "$timing" - printf ' Date %s %s - %s\n' "${fields[3]}" "${fields[4]}" "${fields[5]}" - printf ' Reference bill %s / outage %s\n' "${fields[0]}" "${fields[1]}" } render_empty() { @@ -780,7 +838,7 @@ draw_frame() { if ((TERMINAL_ACTIVE)); then cleared_frame="${frame//$'\n'/$'\033[K\n'}" - printf '\033[H%s\033[K\n\033[J' "$cleared_frame" + printf '\033[H%s\033[K\033[J' "$cleared_frame" else printf '%s\n' "$frame" fi @@ -806,13 +864,17 @@ render_dashboard() { local now_epoch="$1" local columns="${2:-}" local countdown_bill_id="${3:-}" - local rule phase start_epoch stop_epoch record remaining banner - local schedule_line ignored + local lines="${4:-}" + local rule phase start_epoch stop_epoch record remaining banner banner_art + local banner_rows fixed_rows available_rows visible_count hidden_count index show_bill_heading=0 color_setup if [[ -z "$columns" ]]; then columns="$(terminal_columns)" fi + if [[ -z "$lines" ]]; then + lines="$(terminal_lines)" + fi if ((${#SCHEDULE[@]} == 0)); then render_empty "$columns" @@ -834,29 +896,69 @@ render_dashboard() { remaining=$((start_epoch - now_epoch)) fi banner="$(banner_duration "$remaining")" - if [[ -n "$countdown_bill_id" ]]; then + banner_art="$(print_large_countdown "$banner" "$columns")" + banner_rows="$(awk 'END { print NR }' <<<"$banner_art")" + [[ -n "$countdown_bill_id" ]] && show_bill_heading=1 + fixed_rows=$((8 + banner_rows)) + ((show_bill_heading)) && fixed_rows=$((fixed_rows + 2)) + if ((fixed_rows + 2 > lines)); then + banner_art="$banner" + banner_rows=1 + fixed_rows=$((8 + banner_rows)) + ((show_bill_heading)) && fixed_rows=$((fixed_rows + 2)) + if ((fixed_rows + 1 > lines)); then + show_bill_heading=0 + fi + fi + if ((show_bill_heading)); then center_line "COUNTDOWN FOR BILL $countdown_bill_id" "$columns" printf '\n' fi printf '%s%s' "$BOLD" "$YELLOW" - print_large_countdown "$banner" "$columns" + printf '%s\n' "$banner_art" printf '%s' "$RESET" fi printf '\n%s%s%s\n' "$CYAN" "$rule" "$RESET" - printf '\n%sOUTAGE SCHEDULE (%d)%s\n\n' "$BOLD" "${#SCHEDULE[@]}" "$RESET" + printf '\n%sOUTAGE SCHEDULE (%d)%s\n' "$BOLD" "${#SCHEDULE[@]}" "$RESET" + if ((columns >= 60)); then + printf '%s%-9s %-9s %-10s %-11s %s%s\n' \ + "$BOLD" "STATUS" "TYPE" "DATE" "TIME" "BILL/OUTAGE" "$RESET" + else + printf '%s%-4s %-5s %-10s %-11s %s%s\n' \ + "$BOLD" "ST" "TYPE" "DATE" "TIME" "REF" "$RESET" + fi + printf '%s%s%s\n' "$DIM" "$(repeat_character '-' "$columns")" "$RESET" - for schedule_line in "${SCHEDULE[@]}"; do - IFS=$'\t' read -r ignored phase start_epoch stop_epoch record <<<"$schedule_line" - render_event "$schedule_line" "$now_epoch" - printf '%s\n' "$DIM$(repeat_character '-' "$columns")$RESET" + fixed_rows=$((8 + ${banner_rows:-1})) + ((show_bill_heading)) && fixed_rows=$((fixed_rows + 2)) + available_rows=$((lines - fixed_rows - 1)) + ((available_rows < 0)) && available_rows=0 + visible_count="${#SCHEDULE[@]}" + hidden_count=0 + if ((visible_count > available_rows)); then + if ((available_rows > 0)); then + visible_count=$((available_rows - 1)) + else + visible_count=0 + fi + hidden_count=$((${#SCHEDULE[@]} - visible_count)) + fi + + for ((index = 0; index < visible_count; index++)); do + render_event "${SCHEDULE[index]}" "$columns" done + if ((hidden_count && available_rows > 0)); then + printf '%s... %d more outage%s%s\n' \ + "$DIM" "$hidden_count" "$([[ "$hidden_count" == 1 ]] || printf 's')" "$RESET" + fi + printf '%s%s%s\n' "$CYAN" "$rule" "$RESET" } main() { local now_epoch next_refresh=0 refresh_minutes=10 refresh_seconds state_ready=0 local last_rendered=-1 next_transition=0 rebuild_schedule=0 frame key input_fd=0 - local display_columns countdown_bill_id="" + local display_columns display_lines render_lines countdown_bill_id="" while (($#)); do case "$1" in @@ -962,9 +1064,14 @@ main() { fi display_columns="$(terminal_columns)" + display_lines="$(terminal_lines)" + render_lines="$display_lines" + if [[ -n "$REFRESH_ERROR" ]] && ((render_lines > 12)); then + render_lines=$((render_lines - 2)) + fi frame="$( if ((state_ready)); then - render_dashboard "$now_epoch" "$display_columns" "$countdown_bill_id" + render_dashboard "$now_epoch" "$display_columns" "$countdown_bill_id" "$render_lines" else render_unavailable "$display_columns" fi diff --git a/tests/test_main.sh b/tests/test_main.sh index 28f1f66..70c8aac 100755 --- a/tests/test_main.sh +++ b/tests/test_main.sh @@ -120,6 +120,8 @@ assert_equal "01 : 01 : 01" "$(banner_duration 3661)" \ "countdowns should include hours, minutes, and seconds" assert_equal "49 : 30 : 00" "$(banner_duration 178200)" \ "countdowns should use total hours beyond one day" +assert_equal "17" "$(LINES=17 terminal_lines)" \ + "the terminal height override should be respected" FONT_OUTPUT="$(print_large_countdown "00 : 00 : 00" 150)" assert_contains "$FONT_OUTPUT" ',a8888a,' \ "the countdown should consistently use the univers-style font" @@ -189,6 +191,17 @@ if [[ "$OUTPUT" == *$'\033['* ]]; then printf 'FAIL: NO_COLOR output contains ANSI escapes\n' >&2 exit 1 fi +SHORT_OUTPUT="$(NO_COLOR=1 COLUMNS=80 LINES=12 render_dashboard "$NOW_EPOCH" 80 "" 12)" +SHORT_LINE_COUNT="$(awk 'END { print NR }' <<<"$SHORT_OUTPUT")" +assert_equal "12" "$SHORT_LINE_COUNT" \ + "a dashboard with extra outages should fit the terminal height" +assert_contains "$SHORT_OUTPUT" '01 : 00 : 00' \ + "the countdown should remain visible when schedule rows are limited" +assert_contains "$SHORT_OUTPUT" "... 2 more outages" \ + "the dashboard should report schedule rows that do not fit" +SHORT_BOTTOM_LINE="${SHORT_OUTPUT##*$'\n'}" +assert_equal "$(repeat_character '=' 80)" "$SHORT_BOTTOM_LINE" \ + "the compact schedule should end with a full-width rule" WIDE_OUTPUT="$(NO_COLOR=1 COLUMNS=180 render_dashboard "$NOW_EPOCH")" WIDE_TOP_LINE="${WIDE_OUTPUT%%$'\n'*}" assert_equal "180" "${#WIDE_TOP_LINE}" \ @@ -196,6 +209,8 @@ assert_equal "180" "${#WIDE_TOP_LINE}" \ WIDE_BOTTOM_LINE="${WIDE_OUTPUT##*$'\n'}" assert_equal "180" "${#WIDE_BOTTOM_LINE}" \ "the dashboard bottom divider should span the full terminal width" +assert_equal "$(repeat_character '=' 180)" "$WIDE_BOTTOM_LINE" \ + "the dashboard should end with the same rule as the countdown section" FUTURE_NOW="$(TZ=Asia/Tehran date -d '2026-07-18 15:00:00' +%s)" build_schedule "$FIXTURE" "$FUTURE_NOW"