rsemu 0.0.4

A multiplatform emulator in pure Rust, built bottom-up on a generic framework.
Documentation
<script setup>
// What is interesting about a running machine.
//
// The state hash earns its place here: virtual time is computed inside the
// emulator and never read from the host clock, so this number is the same one
// `rsemu run` prints for the same session (ROADMAP.md §11.6). It is the page's
// evidence that the browser is not a different emulator.

import { computed, ref } from "vue";

const props = defineProps({
  stats: { type: Object, required: true },
  live: { type: Boolean, default: false },
});

const hz = computed(() =>
  props.stats.framePeriodMs ? (1000 / props.stats.framePeriodMs).toFixed(3) : "—",
);

const elapsed = computed(() => {
  const s = props.stats.seconds ?? 0;
  if (s < 60) return `${s.toFixed(2)} s`;
  const m = Math.floor(s / 60);
  return `${m}m ${(s - m * 60).toFixed(1)}s`;
});

const copied = ref(false);
async function copyHash() {
  try {
    await navigator.clipboard.writeText(props.stats.hash);
    copied.value = true;
    setTimeout(() => (copied.value = false), 1400);
  } catch {
    // Clipboard permission is the user's to withhold; the hash is on screen
    // either way, so this is not worth an error message.
  }
}
</script>

<template>
  <dl class="stats">
    <div class="stat">
      <dt>Frames / s</dt>
      <dd class="mono big" :class="{ idle: !live || stats.paused }">
        {{ live ? (stats.paused ? "—" : stats.fps) : "—" }}
      </dd>
    </div>
    <div class="stat">
      <dt>Frame</dt>
      <dd class="mono">{{ live ? stats.frame.toLocaleString() : "—" }}</dd>
    </div>
    <div class="stat">
      <dt>Virtual time</dt>
      <dd class="mono">{{ live ? elapsed : "—" }}</dd>
    </div>
    <div class="stat">
      <dt>Refresh</dt>
      <dd class="mono">{{ live ? `${hz} Hz` : "—" }}</dd>
    </div>
    <div class="stat wide">
      <dt>State hash</dt>
      <dd class="mono hash">
        <span>{{ stats.hash }}</span>
        <button
          v-if="live"
          class="copy"
          type="button"
          @click="copyHash"
          :aria-label="`Copy state hash ${stats.hash} to the clipboard`"
        >
          {{ copied ? "copied" : "copy" }}
        </button>
      </dd>
    </div>
  </dl>
</template>

<style scoped>
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
  gap: 1px;
  margin: 0;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  overflow: hidden;
}

.stat {
  padding: 0.55rem 0.7rem;
  background: var(--panel);
}

.wide {
  grid-column: 1 / -1;
}

dt {
  font-size: 0.68rem;
  font-weight: 650;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--fg-faint);
}

dd {
  margin: 0.15rem 0 0;
  font-size: 0.92rem;
  color: var(--fg);
}

.big {
  font-size: 1.15rem;
  font-weight: 600;
  color: var(--accent);
}

.big.idle {
  color: var(--fg-faint);
}

.hash {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  font-size: 0.86rem;
}

.copy {
  padding: 0.1rem 0.45rem;
  font: 600 0.68rem/1.6 var(--sans);
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--fg-dim);
  background: var(--panel-2);
  border: 1px solid var(--line-strong);
  border-radius: 4px;
  cursor: pointer;
}

.copy:hover {
  color: var(--accent);
  border-color: var(--accent);
}
</style>