Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
23836ce691
|
|||
| e2e643cbc8 |
Generated
+1
-1
@@ -243,7 +243,7 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "orca-rs"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"crossterm",
|
||||
"midir",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "orca-rs"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
description = "Orca live coding environment, ported to Rust"
|
||||
license = "MIT"
|
||||
|
||||
@@ -42,6 +42,15 @@ MIDI is on by default: orca-rs creates a virtual ALSA sequencer port named
|
||||
`orca-rs`, which PipeWire exposes through its Midi-Bridge — connect it to a
|
||||
synth with qpwgraph/Helvum, or find it under `wpctl status`.
|
||||
|
||||
A default MIDI port to connect to (instead of creating a virtual one) can be
|
||||
set in `~/.config/orca-rs/config.toml`:
|
||||
|
||||
```toml
|
||||
midi_name = "IAC Driver"
|
||||
```
|
||||
|
||||
`--midi-name` on the command line overrides it.
|
||||
|
||||
There is also a headless runner, equivalent to orca-c's `cli`:
|
||||
|
||||
```sh
|
||||
@@ -54,14 +63,15 @@ orca-cli -t 60 file.orca # simulate 60 ticks, print the grid
|
||||
|---|---|
|
||||
| Arrow keys | Move cursor (Alt+arrows: leap by 8) |
|
||||
| Shift+arrows | Grow/shrink selection |
|
||||
| Ctrl+arrows | Resize grid |
|
||||
| Ctrl+arrows | Resize grid by 1 cell |
|
||||
| `(` / `)` | Resize grid width, snapped to the nearest ruler division |
|
||||
| `_` / `+` | Resize grid height, snapped to the nearest ruler division |
|
||||
| `0-9 a-z A-Z ! : ; = # * . ?` | Place glyph (fills selection) |
|
||||
| Backspace / Delete | Erase selection |
|
||||
| Space | Play / pause |
|
||||
| Ctrl+F | Step one frame (while paused) |
|
||||
| Ctrl+R | Reset frame counter |
|
||||
| `<` / `>` | BPM −1 / +1 |
|
||||
| `(` / `)` | BPM −10 / +10 |
|
||||
| Ctrl+Z or Ctrl+U | Undo |
|
||||
| Ctrl+X / Ctrl+C / Ctrl+V | Cut / copy / paste selection |
|
||||
| Ctrl+S | Save |
|
||||
@@ -73,6 +83,7 @@ orca-cli -t 60 file.orca # simulate 60 ticks, print the grid
|
||||
- `src/sim/` — the VM: all operators, ported from orca-c's `sim.c`
|
||||
- `src/tui.rs` — terminal UI (crossterm), counterpart of `tui_main.c`
|
||||
- `src/io.rs` — UDP / OSC / MIDI event output
|
||||
- `src/config.rs` — `~/.config/orca-rs/config.toml` (default MIDI port)
|
||||
- `src/bin/orca-cli.rs` — headless runner, counterpart of `cli_main.c`
|
||||
|
||||
## Releasing
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Maintainer: Bendik Aagaard Lynghaug <bendik.lynghaug@gmail.com>
|
||||
pkgname=orca-rs
|
||||
pkgver=0.1.0
|
||||
pkgver=0.1.1
|
||||
pkgrel=1
|
||||
pkgdesc="Orca live coding environment, ported to Rust (terminal UI with MIDI/OSC/UDP output)"
|
||||
arch=('x86_64' 'aarch64')
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
//! Minimal `key = "value"` config file (a subset of TOML), read from
|
||||
//! `$XDG_CONFIG_HOME/orca-rs/config.toml` or `~/.config/orca-rs/config.toml`.
|
||||
//! Currently just holds the default MIDI port to connect to.
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Config {
|
||||
pub midi_name: Option<String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load() -> Config {
|
||||
let text = std::fs::read_to_string(config_path()).unwrap_or_default();
|
||||
let mut cfg = Config::default();
|
||||
for line in text.lines() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let Some((key, value)) = line.split_once('=') else {
|
||||
continue;
|
||||
};
|
||||
let value = value.trim().trim_matches('"').to_string();
|
||||
if key.trim() == "midi_name" {
|
||||
cfg.midi_name = Some(value);
|
||||
}
|
||||
}
|
||||
cfg
|
||||
}
|
||||
}
|
||||
|
||||
fn config_path() -> std::path::PathBuf {
|
||||
let base = std::env::var("XDG_CONFIG_HOME")
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or_else(|_| {
|
||||
std::path::PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| ".".to_string()))
|
||||
.join(".config")
|
||||
});
|
||||
base.join("orca-rs").join("config.toml")
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod config;
|
||||
pub mod io;
|
||||
pub mod sim;
|
||||
pub mod tui;
|
||||
|
||||
+6
-1
@@ -1,5 +1,6 @@
|
||||
use std::process::exit;
|
||||
|
||||
use orca_rs::config::Config;
|
||||
use orca_rs::io::{EventOutputs, MidiOut, OscOut, UdpOut};
|
||||
use orca_rs::tui::{self, TuiConfig};
|
||||
|
||||
@@ -22,6 +23,10 @@ Options:
|
||||
instead of creating a virtual port
|
||||
--list-midi List available MIDI output ports and exit
|
||||
-h, --help Print this message and exit
|
||||
|
||||
A default MIDI port can be set in ~/.config/orca-rs/config.toml:
|
||||
midi_name = \"IAC Driver\"
|
||||
--midi-name on the command line overrides it.
|
||||
";
|
||||
|
||||
fn die(msg: &str) -> ! {
|
||||
@@ -44,7 +49,7 @@ fn main() {
|
||||
let mut osc_host = "127.0.0.1".to_string();
|
||||
let mut osc_port: Option<u16> = None;
|
||||
let mut midi = true;
|
||||
let mut midi_name: Option<String> = None;
|
||||
let mut midi_name: Option<String> = Config::load().midi_name;
|
||||
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(arg) = args.next() {
|
||||
|
||||
+264
-51
@@ -8,7 +8,7 @@ use std::time::{Duration, Instant};
|
||||
|
||||
use crossterm::cursor::{Hide, MoveTo, Show};
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||
use crossterm::style::{Attribute, Print, SetAttribute};
|
||||
use crossterm::style::{Attribute, Color, Print, SetAttribute, SetForegroundColor};
|
||||
use crossterm::terminal::{
|
||||
self, disable_raw_mode, enable_raw_mode, BeginSynchronizedUpdate, Clear, ClearType,
|
||||
EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
@@ -23,8 +23,9 @@ use crate::sim::{
|
||||
|
||||
const MAX_UNDO: usize = 256;
|
||||
const MAX_DIM: usize = 512;
|
||||
const RULER_SPACING: usize = 8;
|
||||
const HELP_LINE: &str =
|
||||
"space play ^F step ^S save ^Z undo ^X/^C/^V clip shift+arrows select ^arrows resize < > ( ) bpm ^Q quit";
|
||||
"space play ^F step ^S save ^Z undo ^X/^C/^V clip shift+arrows select ^arrows/( )/_+ resize < > bpm ^Q quit";
|
||||
|
||||
pub struct TuiConfig {
|
||||
pub path: Option<String>,
|
||||
@@ -71,6 +72,22 @@ fn is_valid_glyph(c: char) -> bool {
|
||||
c.is_ascii_alphanumeric() || matches!(c, '!' | '#' | '%' | '*' | '.' | ':' | ';' | '=' | '?')
|
||||
}
|
||||
|
||||
// Snaps `size` to the next/previous ruler-aligned length: one more or fewer
|
||||
// full `ruler`-cell division, landing one past the division boundary (a
|
||||
// ruler of 8 wants sizes like 25 or 33, not 24 or 32). Ported from orca-c's
|
||||
// adjust_rulers_humanized.
|
||||
fn ruler_snap(ruler: usize, size: usize, delta_rulers: isize) -> usize {
|
||||
if size == 0 {
|
||||
return if delta_rulers > 0 { ruler * delta_rulers as usize } else { 1 };
|
||||
}
|
||||
let mut size = size;
|
||||
if delta_rulers < 0 {
|
||||
size += ruler - 1;
|
||||
}
|
||||
let n = ((size as isize - 1) / ruler as isize + delta_rulers).max(0);
|
||||
(ruler as isize * n + 1) as usize
|
||||
}
|
||||
|
||||
fn tick_period(bpm: usize) -> Duration {
|
||||
// One Orca frame is a 16th note: 60000 ms / bpm / 4.
|
||||
Duration::from_millis((60_000 / bpm.max(1) / 4).max(1) as u64)
|
||||
@@ -227,6 +244,28 @@ impl App {
|
||||
let (h, w) = self.dims();
|
||||
let nh = (h as isize + dy).clamp(1, MAX_DIM as isize) as usize;
|
||||
let nw = (w as isize + dx).clamp(1, MAX_DIM as isize) as usize;
|
||||
self.apply_resize(nh, nw);
|
||||
}
|
||||
|
||||
// orca-c's `( ) _ +`: resize by one ruler division, snapped to the
|
||||
// nearest ruler-aligned size rather than a plain +/-1.
|
||||
fn resize_grid_by_ruler(&mut self, delta_h: isize, delta_w: isize) {
|
||||
let (h, w) = self.dims();
|
||||
let nh = if delta_h != 0 {
|
||||
ruler_snap(RULER_SPACING, h, delta_h).min(MAX_DIM)
|
||||
} else {
|
||||
h
|
||||
};
|
||||
let nw = if delta_w != 0 {
|
||||
ruler_snap(RULER_SPACING, w, delta_w).min(MAX_DIM)
|
||||
} else {
|
||||
w
|
||||
};
|
||||
self.apply_resize(nh, nw);
|
||||
}
|
||||
|
||||
fn apply_resize(&mut self, nh: usize, nw: usize) {
|
||||
let (h, w) = self.dims();
|
||||
if (nh, nw) == (h, w) {
|
||||
return;
|
||||
}
|
||||
@@ -311,11 +350,12 @@ fn default_field() -> Result<Field, String> {
|
||||
|
||||
fn main_loop(app: &mut App, stdout: &mut io::Stdout) -> io::Result<()> {
|
||||
let mut next_tick = Instant::now();
|
||||
let mut renderer = Renderer::new();
|
||||
loop {
|
||||
if app.needs_preview && !app.playing {
|
||||
app.preview();
|
||||
}
|
||||
draw(stdout, app)?;
|
||||
draw(stdout, app, &mut renderer)?;
|
||||
|
||||
let timeout = if app.playing {
|
||||
let now = Instant::now();
|
||||
@@ -385,8 +425,10 @@ fn handle_key(app: &mut App, key: KeyEvent, next_tick: &mut Instant) -> bool {
|
||||
}
|
||||
KeyCode::Char('<') => app.bpm = (app.bpm.saturating_sub(1)).max(10),
|
||||
KeyCode::Char('>') => app.bpm = (app.bpm + 1).min(999),
|
||||
KeyCode::Char('(') => app.bpm = (app.bpm.saturating_sub(10)).max(10),
|
||||
KeyCode::Char(')') => app.bpm = (app.bpm + 10).min(999),
|
||||
KeyCode::Char('(') => app.resize_grid_by_ruler(0, -1),
|
||||
KeyCode::Char(')') => app.resize_grid_by_ruler(0, 1),
|
||||
KeyCode::Char('_') => app.resize_grid_by_ruler(-1, 0),
|
||||
KeyCode::Char('+') => app.resize_grid_by_ruler(1, 0),
|
||||
KeyCode::Esc => {
|
||||
app.sel_h = 1;
|
||||
app.sel_w = 1;
|
||||
@@ -420,51 +462,209 @@ fn handle_key(app: &mut App, key: KeyEvent, next_tick: &mut Instant) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn draw(out: &mut io::Stdout, app: &App) -> io::Result<()> {
|
||||
// A cell's visual style. Colors, not Bold/Dim, carry the contrast — Dim in
|
||||
// particular renders inconsistently (or not at all) across terminal emulators.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Style {
|
||||
Normal,
|
||||
Dim,
|
||||
Input,
|
||||
Output,
|
||||
Selected,
|
||||
Cursor,
|
||||
}
|
||||
|
||||
fn cell_style(is_cursor: bool, in_sel: bool, m: u8, is_dot: bool) -> Style {
|
||||
if is_cursor {
|
||||
Style::Cursor
|
||||
} else if in_sel {
|
||||
Style::Selected
|
||||
} else if m & MARK_OUTPUT != 0 {
|
||||
Style::Output
|
||||
} else if m & (MARK_INPUT | MARK_HASTE) != 0 {
|
||||
Style::Input
|
||||
} else if is_dot || m & (MARK_LOCK | MARK_SLEEP) != 0 {
|
||||
Style::Dim
|
||||
} else {
|
||||
Style::Normal
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit foreground colors everywhere content is unmarked, so cells don't
|
||||
// fall back to the terminal's unset default (which just renders as plain
|
||||
// white on black/transparent). Highlighted cells use Reverse — a swap of
|
||||
// whatever the terminal's own colors are — rather than hardcoded black/white,
|
||||
// so a light-themed terminal doesn't get an invisible white-on-white block.
|
||||
// The cursor tile gets a plain, untinted swap (the strongest contrast, and
|
||||
// literally black-on-white on a standard dark terminal); selection and
|
||||
// output tint the swap so all three stay distinguishable.
|
||||
fn apply_style(out: &mut io::Stdout, s: Style) -> io::Result<()> {
|
||||
queue!(out, SetAttribute(Attribute::Reset))?;
|
||||
match s {
|
||||
Style::Cursor => queue!(out, SetAttribute(Attribute::Reverse))?,
|
||||
Style::Selected => {
|
||||
queue!(out, SetForegroundColor(Color::Blue), SetAttribute(Attribute::Reverse))?
|
||||
}
|
||||
Style::Output => {
|
||||
queue!(out, SetForegroundColor(Color::Grey), SetAttribute(Attribute::Reverse))?
|
||||
}
|
||||
Style::Input => queue!(out, SetForegroundColor(Color::Yellow))?,
|
||||
Style::Dim => queue!(out, SetForegroundColor(Color::DarkGrey))?,
|
||||
Style::Normal => queue!(out, SetForegroundColor(Color::Cyan))?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Tracks what was last drawn so `draw()` can emit a diff patch (only the
|
||||
// cells that actually changed) instead of clearing and reprinting the whole
|
||||
// grid every frame — most ticks only touch a handful of cells.
|
||||
struct Renderer {
|
||||
cells: Vec<(char, Style)>,
|
||||
vis_w: usize,
|
||||
vis_h: usize,
|
||||
tw: u16,
|
||||
th: u16,
|
||||
field_h: usize,
|
||||
field_w: usize,
|
||||
line1: String,
|
||||
help_drawn: bool,
|
||||
}
|
||||
|
||||
impl Renderer {
|
||||
fn new() -> Self {
|
||||
Renderer {
|
||||
cells: Vec::new(),
|
||||
vis_w: 0,
|
||||
vis_h: 0,
|
||||
tw: 0,
|
||||
th: 0,
|
||||
field_h: 0,
|
||||
field_w: 0,
|
||||
line1: String::new(),
|
||||
help_drawn: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ruler-tick glyph at a `.` intersection, framing the field's actual edges —
|
||||
// orca-c's fancy ruler border (┌┐└┘┬┴├┤), not a plain '+' that keeps going
|
||||
// past where the grid really ends. Mirrors orca-c's exact bit-flag lookup:
|
||||
// only a single edge (or exactly one corner pair) gets a special glyph, so a
|
||||
// 1-row/1-column field — top and bottom edge at once — still falls to '+'.
|
||||
fn ruler_glyph(y: usize, x: usize, gh: usize, gw: usize) -> char {
|
||||
const T: u8 = 1;
|
||||
const B: u8 = 2;
|
||||
const L: u8 = 4;
|
||||
const R: u8 = 8;
|
||||
let mut p = 0u8;
|
||||
if y == 0 {
|
||||
p |= T;
|
||||
}
|
||||
if y + 1 == gh {
|
||||
p |= B;
|
||||
}
|
||||
if x == 0 {
|
||||
p |= L;
|
||||
}
|
||||
if x + 1 == gw {
|
||||
p |= R;
|
||||
}
|
||||
match p {
|
||||
v if v == T | L => '┌',
|
||||
v if v == T | R => '┐',
|
||||
v if v == B | L => '└',
|
||||
v if v == B | R => '┘',
|
||||
T => '┬',
|
||||
B => '┴',
|
||||
L => '├',
|
||||
R => '┤',
|
||||
_ => '+',
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(out: &mut io::Stdout, app: &App, r: &mut Renderer) -> io::Result<()> {
|
||||
let (tw, th) = terminal::size()?;
|
||||
let (gh, gw) = app.dims();
|
||||
let grid_rows = th.saturating_sub(2) as usize;
|
||||
let vis_h = gh.min(grid_rows);
|
||||
let vis_w = gw.min(tw as usize);
|
||||
|
||||
queue!(out, BeginSynchronizedUpdate, Hide, Clear(ClearType::All))?;
|
||||
// The canvas always matches the live viewport: it fills the terminal at
|
||||
// launch, and re-measuring it every frame (rather than freezing it at
|
||||
// startup) is what lets a grid resize actually stay centered — a frozen
|
||||
// canvas has no headroom to grow into once the starting field already
|
||||
// fills it.
|
||||
let vis_h = th.saturating_sub(2) as usize;
|
||||
let vis_w = tw as usize;
|
||||
let offset_y = vis_h.saturating_sub(gh) / 2;
|
||||
let offset_x = vis_w.saturating_sub(gw) / 2;
|
||||
|
||||
let dims_changed = vis_w != r.vis_w
|
||||
|| vis_h != r.vis_h
|
||||
|| gh != r.field_h
|
||||
|| gw != r.field_w
|
||||
|| tw != r.tw
|
||||
|| th != r.th;
|
||||
if dims_changed {
|
||||
queue!(out, Clear(ClearType::All))?;
|
||||
r.cells = vec![(' ', Style::Normal); vis_w * vis_h];
|
||||
r.vis_w = vis_w;
|
||||
r.vis_h = vis_h;
|
||||
r.field_h = gh;
|
||||
r.field_w = gw;
|
||||
r.tw = tw;
|
||||
r.th = th;
|
||||
r.line1.clear();
|
||||
r.help_drawn = false;
|
||||
}
|
||||
|
||||
queue!(out, BeginSynchronizedUpdate, Hide)?;
|
||||
|
||||
for y in 0..vis_h {
|
||||
queue!(out, MoveTo(0, y as u16))?;
|
||||
let mut need_move = true;
|
||||
for x in 0..vis_w {
|
||||
let g = app.field.buffer[y * gw + x];
|
||||
let m = app.mbuf.buffer.get(y * gw + x).copied().unwrap_or(0);
|
||||
let is_cursor = y == app.cur_y && x == app.cur_x;
|
||||
let in_sel = y >= app.cur_y
|
||||
&& y < app.cur_y + app.sel_h
|
||||
&& x >= app.cur_x
|
||||
&& x < app.cur_x + app.sel_w;
|
||||
let fy_s = y as isize - offset_y as isize;
|
||||
let fx_s = x as isize - offset_x as isize;
|
||||
let in_field = fy_s >= 0 && fx_s >= 0 && (fy_s as usize) < gh && (fx_s as usize) < gw;
|
||||
|
||||
let ch = if g == b'.' {
|
||||
if is_cursor {
|
||||
'@'
|
||||
} else if y % 8 == 0 && x % 8 == 0 {
|
||||
'+'
|
||||
// Outside the field's own bounds is blank canvas, not a dot fill.
|
||||
let (ch, style) = if in_field {
|
||||
let fy = fy_s as usize;
|
||||
let fx = fx_s as usize;
|
||||
let g = app.field.buffer[fy * gw + fx];
|
||||
let m = app.mbuf.buffer.get(fy * gw + fx).copied().unwrap_or(0);
|
||||
let is_cursor = fy == app.cur_y && fx == app.cur_x;
|
||||
let in_sel = fy >= app.cur_y
|
||||
&& fy < app.cur_y + app.sel_h
|
||||
&& fx >= app.cur_x
|
||||
&& fx < app.cur_x + app.sel_w;
|
||||
let is_dot = g == b'.';
|
||||
|
||||
let ch = if is_dot {
|
||||
if is_cursor {
|
||||
'@'
|
||||
} else if fy.is_multiple_of(RULER_SPACING) && fx.is_multiple_of(RULER_SPACING) {
|
||||
ruler_glyph(fy, fx, gh, gw)
|
||||
} else {
|
||||
'.'
|
||||
}
|
||||
} else {
|
||||
'.'
|
||||
}
|
||||
g as char
|
||||
};
|
||||
(ch, cell_style(is_cursor, in_sel, m, is_dot))
|
||||
} else {
|
||||
g as char
|
||||
(' ', Style::Normal)
|
||||
};
|
||||
|
||||
queue!(out, SetAttribute(Attribute::Reset))?;
|
||||
if is_cursor {
|
||||
queue!(out, SetAttribute(Attribute::Reverse), SetAttribute(Attribute::Bold))?;
|
||||
} else if in_sel {
|
||||
queue!(out, SetAttribute(Attribute::Reverse))?;
|
||||
} else if m & MARK_OUTPUT != 0 {
|
||||
queue!(out, SetAttribute(Attribute::Reverse))?;
|
||||
} else if m & (MARK_INPUT | MARK_HASTE) != 0 {
|
||||
queue!(out, SetAttribute(Attribute::Bold))?;
|
||||
} else if g == b'.' || m & (MARK_LOCK | MARK_SLEEP) != 0 {
|
||||
queue!(out, SetAttribute(Attribute::Dim))?;
|
||||
let idx = y * vis_w + x;
|
||||
if !dims_changed && r.cells[idx] == (ch, style) {
|
||||
need_move = true;
|
||||
continue;
|
||||
}
|
||||
if need_move {
|
||||
queue!(out, MoveTo(x as u16, y as u16))?;
|
||||
need_move = false;
|
||||
}
|
||||
apply_style(out, style)?;
|
||||
queue!(out, Print(ch))?;
|
||||
r.cells[idx] = (ch, style);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,20 +675,33 @@ fn draw(out: &mut io::Stdout, app: &App) -> io::Result<()> {
|
||||
);
|
||||
let mut line1: String = status.chars().take(tw as usize).collect();
|
||||
line1 = format!("{:<width$}", line1, width = tw as usize);
|
||||
let help: String = HELP_LINE.chars().take(tw as usize).collect();
|
||||
|
||||
queue!(
|
||||
out,
|
||||
SetAttribute(Attribute::Reset),
|
||||
MoveTo(0, th.saturating_sub(2)),
|
||||
SetAttribute(Attribute::Reverse),
|
||||
Print(line1),
|
||||
SetAttribute(Attribute::Reset),
|
||||
MoveTo(0, th.saturating_sub(1)),
|
||||
SetAttribute(Attribute::Dim),
|
||||
Print(help),
|
||||
SetAttribute(Attribute::Reset),
|
||||
EndSynchronizedUpdate
|
||||
)?;
|
||||
if line1 != r.line1 {
|
||||
// Plain default text, no reversed block and no hardcoded color — a
|
||||
// full-row invert reads as "black on white" on a normal dark
|
||||
// terminal, which is exactly what shouldn't be here.
|
||||
queue!(
|
||||
out,
|
||||
SetAttribute(Attribute::Reset),
|
||||
MoveTo(0, th.saturating_sub(2)),
|
||||
Print(&line1),
|
||||
SetAttribute(Attribute::Reset)
|
||||
)?;
|
||||
r.line1 = line1;
|
||||
}
|
||||
|
||||
if !r.help_drawn {
|
||||
let help: String = HELP_LINE.chars().take(tw as usize).collect();
|
||||
queue!(
|
||||
out,
|
||||
MoveTo(0, th.saturating_sub(1)),
|
||||
SetForegroundColor(Color::DarkGrey),
|
||||
Print(help),
|
||||
SetAttribute(Attribute::Reset)
|
||||
)?;
|
||||
r.help_drawn = true;
|
||||
}
|
||||
|
||||
queue!(out, EndSynchronizedUpdate)?;
|
||||
out.flush()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user