~rabbits/orca

02f72af8103b85375e7de408049b6d49ee0b2fd6 — neauoire a month ago d95ee58 + 5ba56ca
Merge branch 'master' of git.sr.ht:~rabbits/orca
5 files changed, 266 insertions(+), 249 deletions(-)

M README.md
M term_util.c
M term_util.h
M tool
M tui_main.c
M README.md => README.md +1 -1
@@ 52,7 52,7 @@ Livecoding terminal UI: The above, plus ncurses (or compatible curses library), 

## Build

The build script, called simply `tool`, is written in `bash`. It should work with `gcc` (including the `musl-gcc` wrapper), `tcc`, and `clang`, and will automatically detect your compiler. You can manually specify a compiler with the `-c` option.
The build script, called simply `tool`, is written in POSIX `sh`. It should work with `gcc` (including the `musl-gcc` wrapper), `tcc`, and `clang`, and will automatically detect your compiler. You can manually specify a compiler with the `-c` option.

Currently known to build on macOS (`gcc`, `clang`, `tcc`) and Linux (`gcc`, `musl-gcc`, `tcc`, and `clang`, optionally with `LLD`), and Windows via cygwin or WSL (`gcc` or `clang`, `tcc` untested).


M term_util.c => term_util.c +55 -36
@@ 70,55 70,66 @@ void qnav_deinit() {
  while (qnav_stack.top)
    qnav_stack_pop();
}
// Set new y and x coordinates for the top and left of a Qblock based on the
// position of the Qblock "below" it in the stack. (Below meaning its order in
// the stack, not vertical position on a Y axis.) The target Qblock should
// already be inserted into the stack somewhere, so don't call this before
// you've finished doing the rest of the setup on the Qblock. The y and x
// fields can be junk, though, since this function writes to them without
// reading them.
static ORCA_NOINLINE void qnav_reposition_block(Qblock *qb) {
  int top = 0, left = 0;
  Qblock *prev = qb->down;
  if (!prev)
    goto done;
  int total_h, total_w;
  getmaxyx(qb->outer_window, total_h, total_w);
  WINDOW *w = prev->outer_window;
  int prev_y = prev->y, prev_x = prev->x, prev_h, prev_w;
  getmaxyx(w, prev_h, prev_w);
  // Start by trying to position the item to the right of the previous item.
  left = prev_x + prev_w + 0;
  int term_h, term_w;
  getmaxyx(stdscr, term_h, term_w);
  // Check if we'll run out of room if we position the new item to the right
  // of the existing item (with the same Y position.)
  if (left + total_w > term_w) {
    // If we have enough room if we position just below the previous item in
    // the stack, do that instead of positioning to the right of it.
    if (prev_x + total_w <= term_w && total_h < term_h - (prev_y + prev_h)) {
      top = prev_y + prev_h;
      left = prev_x;
    }
    // If the item doesn't fit there, but it's less wide than the terminal,
    // right-align it to the edge of the terminal.
    else if (total_w < term_w) {
      left = term_w - total_w;
    }
    // Otherwise, just start the layout over at Y=0,X=0
    else {
      left = 0;
    }
  }
done:
  qb->y = top;
  qb->x = left;
}
static ORCA_NOINLINE void qnav_stack_push(Qblock *qb, int height, int width) {
#ifndef NDEBUG
  for (Qblock *i = qnav_stack.top; i; i = i->down) {
    assert(i != qb);
  }
#endif
  int top = 0, left = 0;
  int total_h = height + 2, total_w = width + 2;
  if (qnav_stack.top) {
    WINDOW *w = qnav_stack.top->outer_window;
    int prev_y, prev_x, prev_h, prev_w;
    getbegyx(w, prev_y, prev_x);
    getmaxyx(w, prev_h, prev_w);
    // Start by trying to position the item to the right of the previous item.
    left = prev_x + prev_w + 0;
    int term_h, term_w;
    getmaxyx(stdscr, term_h, term_w);
    // Check if we'll run out of room if we position the new item to the right
    // of the existing item (with the same Y position.)
    if (left + total_w > term_w) {
      // If we have enough room if we position just below the previous item in
      // the stack, do that instead of positioning to the right of it.
      if (prev_x + total_w <= term_w && total_h < term_h - (prev_y + prev_h)) {
        top = prev_y + prev_h;
        left = prev_x;
      }
      // If the item doesn't fit there, but it's less wide than the terminal,
      // right-align it to the edge of the terminal.
      else if (total_w < term_w) {
        left = term_w - total_w;
      }
      // Otherwise, just start the layout over at Y=0,X=0
      else {
        left = 0;
      }
    }
  if (qnav_stack.top)
    qnav_stack.top->up = qb;
  } else {
  else
    qnav_stack.bottom = qb;
  }
  qb->down = qnav_stack.top;
  qnav_stack.top = qb;
  qb->outer_window = newpad(total_h, total_w);
  // This used to be derwin when when used newwin instead of newpad -- not sure
  // if we should use derwin or subpad now. subpad is probably more compatible.
  // ncurses docs state that it handles it correctly, unlike some others?
  qb->content_window = subpad(qb->outer_window, height, width, 1, 1);
  qb->y = top;
  qb->x = left;
  qnav_reposition_block(qb);
  qnav_stack.occlusion_dirty = true;
}



@@ 216,6 227,14 @@ done:
  return drew_any;
}

void qnav_adjust_term_size(void) {
  if (!qnav_stack.bottom)
    return;
  for (Qblock *qb = qnav_stack.bottom; qb; qb = qb->up)
    qnav_reposition_block(qb);
  qnav_stack.occlusion_dirty = true;
}

void qblock_print_border(Qblock *qb, unsigned int attr) {
  wborder(qb->outer_window, ACS_VLINE | attr, ACS_VLINE | attr,
          ACS_HLINE | attr, ACS_HLINE | attr, ACS_ULCORNER | attr,

M term_util.h => term_util.h +1 -0
@@ 120,6 120,7 @@ void qnav_deinit(void);
Qblock *qnav_top_block(void);
void qnav_stack_pop(void);
bool qnav_draw(void); // also clear qnav_stack.occlusion_dirty
void qnav_adjust_term_size(void);

void qblock_print_frame(Qblock *qb, bool active);
void qblock_set_title(Qblock *qb, char const *title);

M tool => tool +208 -212
@@ 1,5 1,5 @@
#!/usr/bin/env bash
set -eu -o pipefail
#!/bin/sh
set -euf

print_usage() {
cat <<EOF


@@ 40,8 40,12 @@ Optional Features:
EOF
}

if [[ -z "${1:-}" ]]; then
  echo "Error: Command required" >&2
warn() { printf 'Warning: %s\n' "$*" >&2; }
fatal() { printf 'Error: %s\n' "$*" >&2; exit 1; }
script_error() { printf 'Script error: %s\n' "$*" >&2; exit 1; }

if [ -z "${1:-}" ]; then
  printf 'Error: Command required\n' >&2
  print_usage >&2
  exit 1
fi


@@ 49,18 53,17 @@ fi
cmd=$1
shift

os=
case $(uname -s | awk '{print tolower($0)}') in
  linux*) os=linux;;
  darwin*) os=mac;;
  cygwin*) os=cygwin;;
  *bsd*) os=bsd;;
  *) os=unknown;;
  *) os=unknown; warn "Build script not tested on this platform";;
esac

cc_exe="${CC:-cc}"

if [[ $os = cygwin ]]; then
if [ $os = cygwin ]; then
  # Under cygwin, specifically ignore the mingw compilers if they're set as the
  # CC environment variable. This may be the default from the cygwin installer.
  # But we want to use 'gcc' from the cygwin gcc-core package (probably aliased


@@ 74,8 77,7 @@ if [[ $os = cygwin ]]; then
  # happens. So we'll just explicitly set it to gcc. This might mess up people
  # who have clang installed but not gcc, I guess? Is that even possible?
  case $cc_exe in
  i686-w64-mingw32-gcc.exe|\
  x86_64-w64-mingw32-gcc.exe)
  i686-w64-mingw32-gcc.exe|x86_64-w64-mingw32-gcc.exe)
    cc_exe=gcc;;
  esac
fi


@@ 90,151 92,138 @@ mouse_disabled=0
config_mode=release

while getopts c:dhsv-: opt_val; do
  case "$opt_val" in
    -)
      case "$OPTARG" in
        harden) protections_enabled=1;;
        help) print_usage; exit 0;;
        static) static_enabled=1;;
        pie) pie_enabled=1;;
        portmidi) portmidi_enabled=1;;
        no-portmidi|noportmidi) portmidi_enabled=0;;
        mouse) mouse_disabled=0;;
        no-mouse|nomouse) mouse_disabled=1;;
        *)
          echo "Unknown long option --$OPTARG" >&2
          print_usage >&2
          exit 1
          ;;
      esac
      ;;
    c) cc_exe="$OPTARG";;
  case $opt_val in
    -) case $OPTARG in
         harden) protections_enabled=1;;
         help) print_usage; exit 0;;
         static) static_enabled=1;;
         pie) pie_enabled=1;;
         portmidi) portmidi_enabled=1;;
         no-portmidi|noportmidi) portmidi_enabled=0;;
         mouse) mouse_disabled=0;;
         no-mouse|nomouse) mouse_disabled=1;;
         *) printf 'Unknown option --%s\n' "$OPTARG" >&2; exit 1;;
       esac;;
    c) cc_exe=$OPTARG;;
    d) config_mode=debug;;
    h) print_usage; exit 0;;
    s) stats_enabled=1;;
    v) verbose=1;;
    \?) print_usage >&2; exit 1;;
    *) break;;
  esac
done

arch=
case $(uname -m) in
  x86_64) arch=x86_64;;
  *) arch=unknown;;
esac

warn() {
  echo "Warning: $*" >&2
}
fatal() {
  echo "Error: $*" >&2
  exit 1
}
script_error() {
  echo "Script error: $*" >&2
  exit 1
}

verbose_echo() {
  if [[ $verbose = 1 ]]; then
    echo "$@"
  # Don't print 'timed_stats' if it's the first part of the command
  if [ $verbose = 1 ] && [ $# -gt 1 ]; then
    printf '%s ' "$@" | sed -E -e 's/^timed_stats[[:space:]]+//' -e 's/ $//' \
      | tr -d '\n'
    printf '\n'
  fi
  "$@"
}

TIMEFORMAT='%3R'

last_time=

file_size() {
  wc -c < "$1" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//'
}

timed_stats_result=
timed_stats() {
  if [[ $stats_enabled = 1 ]]; then
    { last_time=$( { time "$@" 1>&3- 2>&4-; } 2>&1 ); } 3>&1 4>&2
  if [ $stats_enabled = 1 ] && command -v time >/dev/null 2>&1; then
    TIMEFORMAT='%3R'
    { timed_stats_result=$( { time "$@" 1>&3- 2>&4-; } 2>&1 ); } 3>&1 4>&2
  else
    "$@"
  fi
}

version_string_normalized() {
  echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }';
normalized_version() {
  printf '%s\n' "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }';
}

if [[ ($os == unknown) ]]; then
  warn "Build script not tested on this platform"
fi

# This is not perfect by any means
cc_id=
cc_vers=
lld_detected=0
if cc_vers=$(echo -e '#ifndef __clang__\n#error Not found\n#endif\n__clang_major__.__clang_minor__.__clang_patchlevel__' | "$cc_exe" -E -xc - 2>/dev/null | tail -n 1 | tr -d '\040'); then
  cc_id=clang
  # Mac clang/llvm doesn't say the real version of clang. Just assume it's 3.9.0
  if [[ $os == mac ]]; then
    cc_vers=3.9.0
  else
    if command -v "lld" >/dev/null 2>&1; then
      lld_detected=1
    fi
  fi
elif cc_vers=$(echo -e '#ifndef __GNUC__\n#error Not found\n#endif\n__GNUC__.__GNUC_MINOR__.__GNUC_PATCHLEVEL__' | "$cc_exe" -E -xc - 2>/dev/null | tail -n 1 | tr -d '\040'); then
  cc_id=gcc
elif cc_vers=$(echo -e '#ifndef __TINYC__\n#error Not found\n#endif\n__TINYC__' | "$cc_exe" -E -xc - 2>/dev/null | tail -n 1 | tr -d '\040'); then
  cc_id=tcc
lld_name=lld
if preproc_result=$( \
  ("$cc_exe" -E -xc - 2>/dev/null | tail -n 2 | tr -d '\040') <<EOF
#if defined(__clang__)
clang
__clang_major__.__clang_minor__.__clang_patchlevel__
#elif defined(__GNUC__)
gcc
__GNUC__.__GNUC_MINOR__.__GNUC_PATCHLEVEL__
#elif defined(__TINYC__)
tcc
__TINYC__
#else
#error Unknown compiler
#endif
EOF
); then
  cc_id=$(printf %s "$preproc_result" | head -n 1)
  cc_vers=$(printf %s "$preproc_result" | tail -n 1)
fi

if [[ -z $cc_id ]]; then
  warn "Failed to detect compiler type"
fi
if [[ -z $cc_vers ]]; then
  warn "Failed to detect compiler version"
if [ "$cc_id" = clang ]; then
  case $os in
    # Mac clang/llvm doesn't say the real version of clang. Assume it's 3.9.0.
    mac) cc_vers=3.9.0;;
    *)
      # Debian names versions clang like "clang-9" and also LLD like "lld-9".
      # To tell clang to use LLD, we have to pass an argument like
      # '-fuse-ld=lld'. You would expect that the Debian versions of clang,
      # like clang-9, would want '-fuse-ld=lld-9', but it seems to work both as
      # '-fuse-ld=lld-' and also as '-fuse-ld=lld'. I'm not sure if this holds
      # true if multiple versions of clang are installed.
      if output=$(printf %s "$cc_exe" | awk -F- '
          /^clang\+?\+?-/ && $NF ~ /^[0-9]+$/ { a=$NF }
          END { if (a == "") exit -1; printf("lld-%s", a) }'); then
        lld_name=$output
      fi
      if command -v "$lld_name" >/dev/null 2>&1; then lld_detected=1; fi
    ;;
  esac
fi

test -z "$cc_id" && warn "Failed to detect compiler type"
test -z "$cc_vers" && warn "Failed to detect compiler version"

cc_vers_normalized=$(normalized_version "$cc_vers")

cc_vers_is_gte() {
  if [[ $(version_string_normalized "$cc_vers") -ge $(version_string_normalized "$1") ]]; then
    return 0
  else
    return 1
  fi
  test "$cc_vers_normalized" -ge "$(normalized_version "$1")"
}

cc_id_and_vers_gte() {
  if [[ $cc_id == "$1" ]] && cc_vers_is_gte "$2"; then
    return 0
  else
    return 1
  fi
  test "$cc_id" = "$1" && cc_vers_is_gte "$2"
}

# Append arguments to a string, separated by newlines. Like a bad array.
add() {
  if [[ -z "${1:-}" ]]; then
    script_error "At least one argument required for array add"
  if [ -z "${1:-}" ]; then
    script_error "At least one argument required for add"
  fi
  local array_name
  array_name=${1}
  _add_name=${1}
  shift
  eval "$array_name+=($(printf "'%s' " "$@"))"
}

concat() {
  if [[ -z "${1:-}" || -z "${2:-}" ]]; then
    script_error "Two arguments required for array concat"
  fi
  local lhs_name
  local rhs_name
  lhs_name=${1}
  rhs_name=${2}
  eval "$lhs_name+=(\"\${${rhs_name}[@]}\")"
  while [ -n "${1+x}" ]; do
    # shellcheck disable=SC2034
    _add_hidden=$1
    eval "$_add_name"'=$(printf '"'"'%s\n%s.'"' "'"$'"$_add_name"'" "$_add_hidden")'
    eval "$_add_name"'=${'"$_add_name"'%.}'
    shift
  done
}

try_make_dir() {
  if ! [[ -e "$1" ]]; then
  if ! [ -e "$1" ]; then
    verbose_echo mkdir "$1"
  elif ! [[ -d "$1" ]]; then
  elif ! [ -d "$1" ]; then
    fatal "File $1 already exists but is not a directory"
  fi
}


@@ 242,10 231,10 @@ try_make_dir() {
build_dir=build

build_target() {
  local cc_flags=()
  local libraries=()
  local source_files=()
  local out_exe
  cc_flags=
  libraries=
  source_files=
  out_exe=
  add cc_flags -std=c99 -pipe -finput-charset=UTF-8 -Wall -Wpedantic -Wextra \
    -Wwrite-strings
  if cc_id_and_vers_gte gcc 6.0.0 || cc_id_and_vers_gte clang 3.9.0; then


@@ 253,10 242,10 @@ build_target() {
      -Werror=implicit-function-declaration -Werror=implicit-int \
      -Werror=incompatible-pointer-types -Werror=int-conversion
  fi
  if [[ $cc_id = tcc ]]; then
  if [ "$cc_id" = tcc ]; then
    add cc_flags -Wunsupported
  fi
  if [[ $os = mac && $cc_id = clang ]]; then
  if [ $os = mac ] && [ "$cc_id" = clang ]; then
    # The clang that's shipped with Mac 10.12 has bad behavior for issuing
    # warnings for structs initialed with {0} in C99. We have to disable this
    # warning, or it will issue a bunch of useless warnings. It might be fixed


@@ 264,26 253,26 @@ build_target() {
    # indecipherable, so we'll just always turn it off.
    add cc_flags -Wno-missing-field-initializers
  fi
  if [[ $lld_detected = 1 ]]; then
    add cc_flags -fuse-ld=lld
  if [ $lld_detected = 1 ]; then
    add cc_flags "-fuse-ld=$lld_name"
  fi
  if [[ $protections_enabled = 1 ]]; then
  if [ $protections_enabled = 1 ]; then
    add cc_flags -D_FORTIFY_SOURCE=2 -fstack-protector-strong
  fi
  if [[ $pie_enabled = 1 ]]; then
  if [ $pie_enabled = 1 ]; then
    add cc_flags -pie -fpie -Wl,-pie
  # Only explicitly specify no-pie if cc version is new enough
  elif cc_id_and_vers_gte gcc 6.0.0 || cc_id_and_vers_gte clang 6.0.0; then
    add cc_flags -no-pie -fno-pie
  fi
  if [[ $static_enabled = 1 ]]; then
  if [ $static_enabled = 1 ]; then
    add cc_flags -static
  fi
  case $config_mode in
    debug)
      add cc_flags -DDEBUG -ggdb
      # cygwin gcc doesn't seem to have this stuff, just elide for now
      if [[ $os != cygwin ]]; then
      # cygwin gcc doesn't seem to have this stuff, so just elide for now
      if [ $os != cygwin ]; then
        if cc_id_and_vers_gte gcc 6.0.0 || cc_id_and_vers_gte clang 3.9.0; then
          add cc_flags -fsanitize=address -fsanitize=undefined \
            -fsanitize=float-divide-by-zero


@@ 293,63 282,50 @@ build_target() {
            -fsanitize=unsigned-integer-overflow
        fi
      fi
      if [[ $os = mac ]]; then
        # Our mac clang does not have -Og
        add cc_flags -O1
      else
        add cc_flags -Og
        # needed if address is already specified? doesn't work on mac clang, at
        # least
        # add cc_flags -fsanitize=leak
      fi
      case $os in
        mac) add cc_flags -O1;; # Our Mac clang does not have -Og
        *) add cc_flags -Og;;
      esac
      case $cc_id in
        tcc) add cc_flags -g -bt10;;
      esac
      ;;
    ;;
    release)
      add cc_flags -DNDEBUG -O2 -g0
      if [[ $protections_enabled != 1 ]]; then
      if [ $protections_enabled != 1 ]; then
        add cc_flags -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0
        case $cc_id in
          gcc|clang) add cc_flags -fno-stack-protector;;
          gcc|clang) add cc_flags -fno-stack-protector
        esac
      fi
      if [[ $os = mac ]]; then
        # todo some stripping option
        true
      else
        # -flto is good on both clang and gcc on Linux
        case $cc_id in
          gcc|clang)
            if [[ $os != bsd ]]; then
              add cc_flags -flto
            fi
        esac
        add cc_flags -s
      fi
      ;;
      # -flto is good on both clang and gcc on Linux and Cygwin. Not supported
      # on BSD, and no improvement on Mac. -s gives an obsolescence warning on
      # Mac. For tcc, -flto gives and unsupported warning, and -s is ignored.
      case $cc_id in gcc|clang) case $os in
        linux|cygwin) add cc_flags -flto -s;;
        bsd) add cc_flags -s;;
      esac esac
    ;;
    *) fatal "Unknown build config \"$config_mode\"";;
  esac

  case $arch in
    x86_64)
      # 'nehalem' tuning actually produces faster code for orca than later
      # archs, for both gcc and clang, even if it's running on a later arch
      # CPU. This is likely due to smaller emitted code size. gcc earlier than
      # 4.9 does not recognize the arch flag for it it, though, and I haven't
      # tested a compiler that old, so I don't know what optimization behavior
      # we get with it is. Just leave it at default, in that case.
      case $cc_id in
        # 'nehalem' tuning actually produces faster code for orca than later
        # archs, for both gcc and clang, even if it's running on a later arch
        # CPU. This is likely due to smaller emitted code size. gcc earlier
        # than 4.9 does not recognize the arch flag for it it, though, and I
        # haven't tested a compiler that old, so I don't know what optimization
        # behavior we get with it is. Just leave it at default, in that case.
        gcc)
          if cc_vers_is_gte 4.9; then
            add cc_flags -march=nehalem
          fi
          ;;
        clang)
          add cc_flags -march=nehalem
          ;;
        ;;
        clang) add cc_flags -march=nehalem;;
      esac
      ;;
    ;;
  esac

  add source_files gbuffer.c field.c vmio.c sim.c


@@ 357,7 333,7 @@ build_target() {
    cli)
      add source_files cli_main.c
      out_exe=cli
      ;;
    ;;
    orca|tui)
      add source_files osc_out.c term_util.c sysmisc.c thirdparty/oso.c tui_main.c
      add cc_flags -D_XOPEN_SOURCE_EXTENDED=1


@@ 369,14 345,14 @@ build_target() {
      out_exe=orca
      case $os in
        mac)
          local brew_prefix=
          if ! brew_prefix=$(printenv HOMEBREW_PREFIX); then
             brew_prefix=/usr/local/
             brew_prefix=/usr/local
          fi
          local ncurses_dir="$brew_prefix/opt/ncurses"
          if ! [[ -d "$ncurses_dir" ]]; then
            echo "Error: ncurses directory not found at $ncurses_dir" >&2
            echo "Install with: brew install ncurses" >&2
          ncurses_dir="$brew_prefix/opt/ncurses"
          if ! [ -d "$ncurses_dir" ]; then
            printf 'Error: ncurses directory not found at %s\n' \
              "$ncurses_dir" >&2
            printf 'Install with: brew install ncurses\n' >&2
            exit 1
          fi
          # prefer homebrew version of ncurses if installed. Will give us


@@ 384,11 360,12 @@ build_target() {
          add libraries "-L$ncurses_dir/lib"
          add cc_flags "-I$ncurses_dir/include"
          # todo mach time stuff for mac?
          if [[ $portmidi_enabled = 1 ]]; then
            local portmidi_dir="$brew_prefix/opt/portmidi"
            if ! [[ -d "$portmidi_dir" ]]; then
              echo "Error: PortMidi directory not found at $portmidi_dir" >&2
              echo "Install with: brew install portmidi" >&2
          if [ $portmidi_enabled = 1 ]; then
            portmidi_dir="$brew_prefix/opt/portmidi"
            if ! [ -d "$portmidi_dir" ]; then
              printf 'Error: PortMidi directory not found at %s\n' \
                "$portmidi_dir" >&2
              printf 'Install with: brew install portmidi\n' >&2
              exit 1
            fi
            add libraries "-L$portmidi_dir/lib"


@@ 398,7 375,7 @@ build_target() {
          add cc_flags -DORCA_OS_MAC
        ;;
        bsd)
          if [[ $portmidi_enabled = 1 ]]; then
          if [ $portmidi_enabled = 1 ]; then
            add libraries "-L/usr/local/lib"
            add cc_flags "-I/usr/local/include"
          fi


@@ 413,11 390,12 @@ build_target() {
      # as a separate library that explicitly needs to be linked, or it might
      # not. And if it does, it might need to be either -ltinfo or -ltinfow.
      # Yikes. If this is Linux, let's try asking pkg-config what it thinks.
      local curses_flags=0
      if [[ $os == linux ]]; then
      curses_flags=0
      if [ $os = linux ]; then
        if curses_flags=$(pkg-config --libs ncursesw formw 2>/dev/null); then
          # split by spaces into separate args, then append to array
          IFS=" " read -r -a libraries <<< "$curses_flags"
          # Split by spaces intentionall
          # shellcheck disable=SC2086
          IFS=' ' add libraries $curses_flags
          curses_flags=1
        else
          curses_flags=0


@@ 425,44 403,60 @@ build_target() {
      fi
      # If we didn't get the flags by pkg-config, just guess. (This will work
      # most of the time, including on Mac with Homebrew, and cygwin.)
      if [[ $curses_flags = 0 ]]; then
      if [ $curses_flags = 0 ]; then
        add libraries -lncursesw -lformw
      fi
      if [[ $portmidi_enabled = 1 ]]; then
      if [ $portmidi_enabled = 1 ]; then
        add libraries -lportmidi
        add cc_flags -DFEAT_PORTMIDI
        if [[ $config_mode = debug ]]; then
          echo -e "Warning: The PortMidi library contains code that may trigger address sanitizer in debug builds.\\nThese are not bugs in orca." >&2
        if [ $config_mode = debug ]; then
          cat >&2 <<EOF
Warning: The PortMidi library contains code that may trigger address sanitizer
in debug builds. These are probably not bugs in orca.
EOF
        fi
      fi
      if [[ $mouse_disabled = 1 ]]; then
      if [ $mouse_disabled = 1 ]; then
        add cc_flags -DFEAT_NOMOUSE
      fi
      ;;
    ;;
    *)
      echo -e "Unknown build target '$1'\\nValid targets: orca, cli" >&2
      printf 'Unknown build target %s\nValid build targets: %s\n' \
        "$1" 'orca, cli' >&2
      exit 1
      ;;
    ;;
  esac
  try_make_dir "$build_dir"
  if [[ $config_mode = debug ]]; then
  if [ $config_mode = debug ]; then
    build_dir=$build_dir/debug
    try_make_dir "$build_dir"
  fi
  local out_path=$build_dir/$out_exe
  # bash versions quirk: empty arrays might give error on expansion, use +
  # trick to avoid expanding second operand
  verbose_echo timed_stats "$cc_exe" "${cc_flags[@]}" -o "$out_path" "${source_files[@]}" ${libraries[@]+"${libraries[@]}"}
  if [[ $stats_enabled = 1 ]]; then
    echo "time: $last_time"
    echo "size: $(file_size "$out_path")"
  out_path=$build_dir/$out_exe
  IFS='
'
  # shellcheck disable=SC2086
  verbose_echo timed_stats "$cc_exe" $cc_flags -o "$out_path" $source_files $libraries
  compile_ok=$?
  if [ $stats_enabled = 1 ]; then
    if [ -n "$timed_stats_result" ]; then
      printf '%s\n' "time: $timed_stats_result"
    else
      printf '%s\n' "time: unavailable (missing 'time' command)"
    fi
    if [ $compile_ok = 0 ]; then
      printf '%s\n' "size: $(file_size "$out_path")"
    fi
  fi
}

print_info() {
  local linker_name
  if [[ $lld_detected = 1 ]]; then
  if [ $lld_detected = 1 ]; then
    linker_name=LLD
    # Not sure if we should always print the specific LLD name or not. Or never
    # print it.
    if [ "$lld_name" != lld ]; then
      linker_name="$linker_name ($lld_name)"
    fi
  else
    linker_name=default
  fi


@@ 480,36 474,38 @@ shift $((OPTIND - 1))

case $cmd in
  info)
    if [[ "$#" -gt 1 ]]; then
      fatal "Too many arguments for 'info'"
    fi
    print_info; exit 0;;
    test "$#" -gt 1 && fatal "Too many arguments for 'info'"
    print_info; exit 0
  ;;
  build)
    if [[ "$#" -lt 1 ]]; then
      fatal "Too few arguments for 'build'"
    fi
    if [[ "$#" -gt 1 ]]; then
      echo "Too many arguments for 'build'" >&2
      echo "The syntax has changed. Updated usage examples:" >&2
      echo "./tool build --portmidi orca   (release)" >&2
      echo "./tool build -d orca           (debug)" >&2
    test "$#" -lt 1 && fatal "Too few arguments for 'build'"
    if [ "$#" -gt 1 ]; then
      cat >&2 <<EOF
Too many arguments for 'build'
The syntax has changed. Updated usage examples:
./tool build --portmidi orca   (release)
./tool build -d orca           (debug)
EOF
      exit 1
    fi
    build_target "$1"
    ;;
  ;;
  clean)
    if [[ -d "$build_dir" ]]; then
      verbose_echo rm -rf "$build_dir"
    if [ -d "$build_dir" ]; then
      verbose_echo rm -rf "$build_dir";
    fi
    ;;
  help) print_usage; exit 0;;
  -*)
    echo "The syntax has changed for the 'tool' build script." >&2
    echo "The options now need to come after the command name." >&2
    echo "Do it like this instead:" >&2
    echo "./tool build --portmidi orca" >&2
  ;;
  help)
    print_usage; exit 0
  ;;
  -*) cat >&2 <<EOF
The syntax has changed for the 'tool' build script.
The options now need to come after the command name.
Do it like this instead:
./tool build --portmidi orca
EOF
    exit 1
    ;;
  ;;
  *) fatal "Unrecognized command $cmd";;
esac


M tui_main.c => tui_main.c +1 -0
@@ 3523,6 3523,7 @@ event_loop:;
  }
  case KEY_RESIZE:
    tui_adjust_term_size(&t, &cont_window);
    qnav_adjust_term_size();
    goto event_loop;
#ifndef FEAT_NOMOUSE
  case KEY_MOUSE: {