Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
23836ce691
|
|||
| e2e643cbc8 |
Generated
+1
-1
@@ -243,7 +243,7 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "orca-rs"
|
name = "orca-rs"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"midir",
|
"midir",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "orca-rs"
|
name = "orca-rs"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Orca live coding environment, ported to Rust"
|
description = "Orca live coding environment, ported to Rust"
|
||||||
license = "MIT"
|
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
|
`orca-rs`, which PipeWire exposes through its Midi-Bridge — connect it to a
|
||||||
synth with qpwgraph/Helvum, or find it under `wpctl status`.
|
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`:
|
There is also a headless runner, equivalent to orca-c's `cli`:
|
||||||
|
|
||||||
```sh
|
```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) |
|
| Arrow keys | Move cursor (Alt+arrows: leap by 8) |
|
||||||
| Shift+arrows | Grow/shrink selection |
|
| 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) |
|
| `0-9 a-z A-Z ! : ; = # * . ?` | Place glyph (fills selection) |
|
||||||
| Backspace / Delete | Erase selection |
|
| Backspace / Delete | Erase selection |
|
||||||
| Space | Play / pause |
|
| Space | Play / pause |
|
||||||
| Ctrl+F | Step one frame (while paused) |
|
| Ctrl+F | Step one frame (while paused) |
|
||||||
| Ctrl+R | Reset frame counter |
|
| Ctrl+R | Reset frame counter |
|
||||||
| `<` / `>` | BPM −1 / +1 |
|
| `<` / `>` | BPM −1 / +1 |
|
||||||
| `(` / `)` | BPM −10 / +10 |
|
|
||||||
| Ctrl+Z or Ctrl+U | Undo |
|
| Ctrl+Z or Ctrl+U | Undo |
|
||||||
| Ctrl+X / Ctrl+C / Ctrl+V | Cut / copy / paste selection |
|
| Ctrl+X / Ctrl+C / Ctrl+V | Cut / copy / paste selection |
|
||||||
| Ctrl+S | Save |
|
| 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/sim/` — the VM: all operators, ported from orca-c's `sim.c`
|
||||||
- `src/tui.rs` — terminal UI (crossterm), counterpart of `tui_main.c`
|
- `src/tui.rs` — terminal UI (crossterm), counterpart of `tui_main.c`
|
||||||
- `src/io.rs` — UDP / OSC / MIDI event output
|
- `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`
|
- `src/bin/orca-cli.rs` — headless runner, counterpart of `cli_main.c`
|
||||||
|
|
||||||
## Releasing
|
## Releasing
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# Maintainer: Bendik Aagaard Lynghaug <bendik.lynghaug@gmail.com>
|
# Maintainer: Bendik Aagaard Lynghaug <bendik.lynghaug@gmail.com>
|
||||||
pkgname=orca-rs
|
pkgname=orca-rs
|
||||||
pkgver=0.1.0
|
pkgver=0.1.1
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Orca live coding environment, ported to Rust (terminal UI with MIDI/OSC/UDP output)"
|
pkgdesc="Orca live coding environment, ported to Rust (terminal UI with MIDI/OSC/UDP output)"
|
||||||
arch=('x86_64' 'aarch64')
|
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 io;
|
||||||
pub mod sim;
|
pub mod sim;
|
||||||
pub mod tui;
|
pub mod tui;
|
||||||
|
|||||||
+6
-1
@@ -1,5 +1,6 @@
|
|||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
|
|
||||||
|
use orca_rs::config::Config;
|
||||||
use orca_rs::io::{EventOutputs, MidiOut, OscOut, UdpOut};
|
use orca_rs::io::{EventOutputs, MidiOut, OscOut, UdpOut};
|
||||||
use orca_rs::tui::{self, TuiConfig};
|
use orca_rs::tui::{self, TuiConfig};
|
||||||
|
|
||||||
@@ -22,6 +23,10 @@ Options:
|
|||||||
instead of creating a virtual port
|
instead of creating a virtual port
|
||||||
--list-midi List available MIDI output ports and exit
|
--list-midi List available MIDI output ports and exit
|
||||||
-h, --help Print this message 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) -> ! {
|
fn die(msg: &str) -> ! {
|
||||||
@@ -44,7 +49,7 @@ fn main() {
|
|||||||
let mut osc_host = "127.0.0.1".to_string();
|
let mut osc_host = "127.0.0.1".to_string();
|
||||||
let mut osc_port: Option<u16> = None;
|
let mut osc_port: Option<u16> = None;
|
||||||
let mut midi = true;
|
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);
|
let mut args = std::env::args().skip(1);
|
||||||
while let Some(arg) = args.next() {
|
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::cursor::{Hide, MoveTo, Show};
|
||||||
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
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::{
|
use crossterm::terminal::{
|
||||||
self, disable_raw_mode, enable_raw_mode, BeginSynchronizedUpdate, Clear, ClearType,
|
self, disable_raw_mode, enable_raw_mode, BeginSynchronizedUpdate, Clear, ClearType,
|
||||||
EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen,
|
EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen,
|
||||||
@@ -23,8 +23,9 @@ use crate::sim::{
|
|||||||
|
|
||||||
const MAX_UNDO: usize = 256;
|
const MAX_UNDO: usize = 256;
|
||||||
const MAX_DIM: usize = 512;
|
const MAX_DIM: usize = 512;
|
||||||
|
const RULER_SPACING: usize = 8;
|
||||||
const HELP_LINE: &str =
|
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 struct TuiConfig {
|
||||||
pub path: Option<String>,
|
pub path: Option<String>,
|
||||||
@@ -71,6 +72,22 @@ fn is_valid_glyph(c: char) -> bool {
|
|||||||
c.is_ascii_alphanumeric() || matches!(c, '!' | '#' | '%' | '*' | '.' | ':' | ';' | '=' | '?')
|
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 {
|
fn tick_period(bpm: usize) -> Duration {
|
||||||
// One Orca frame is a 16th note: 60000 ms / bpm / 4.
|
// One Orca frame is a 16th note: 60000 ms / bpm / 4.
|
||||||
Duration::from_millis((60_000 / bpm.max(1) / 4).max(1) as u64)
|
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 (h, w) = self.dims();
|
||||||
let nh = (h as isize + dy).clamp(1, MAX_DIM as isize) as usize;
|
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;
|
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) {
|
if (nh, nw) == (h, w) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -311,11 +350,12 @@ fn default_field() -> Result<Field, String> {
|
|||||||
|
|
||||||
fn main_loop(app: &mut App, stdout: &mut io::Stdout) -> io::Result<()> {
|
fn main_loop(app: &mut App, stdout: &mut io::Stdout) -> io::Result<()> {
|
||||||
let mut next_tick = Instant::now();
|
let mut next_tick = Instant::now();
|
||||||
|
let mut renderer = Renderer::new();
|
||||||
loop {
|
loop {
|
||||||
if app.needs_preview && !app.playing {
|
if app.needs_preview && !app.playing {
|
||||||
app.preview();
|
app.preview();
|
||||||
}
|
}
|
||||||
draw(stdout, app)?;
|
draw(stdout, app, &mut renderer)?;
|
||||||
|
|
||||||
let timeout = if app.playing {
|
let timeout = if app.playing {
|
||||||
let now = Instant::now();
|
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.saturating_sub(1)).max(10),
|
||||||
KeyCode::Char('>') => app.bpm = (app.bpm + 1).min(999),
|
KeyCode::Char('>') => app.bpm = (app.bpm + 1).min(999),
|
||||||
KeyCode::Char('(') => app.bpm = (app.bpm.saturating_sub(10)).max(10),
|
KeyCode::Char('(') => app.resize_grid_by_ruler(0, -1),
|
||||||
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(-1, 0),
|
||||||
|
KeyCode::Char('+') => app.resize_grid_by_ruler(1, 0),
|
||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
app.sel_h = 1;
|
app.sel_h = 1;
|
||||||
app.sel_w = 1;
|
app.sel_w = 1;
|
||||||
@@ -420,51 +462,209 @@ fn handle_key(app: &mut App, key: KeyEvent, next_tick: &mut Instant) -> bool {
|
|||||||
true
|
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 (tw, th) = terminal::size()?;
|
||||||
let (gh, gw) = app.dims();
|
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 {
|
for y in 0..vis_h {
|
||||||
queue!(out, MoveTo(0, y as u16))?;
|
let mut need_move = true;
|
||||||
for x in 0..vis_w {
|
for x in 0..vis_w {
|
||||||
let g = app.field.buffer[y * gw + x];
|
let fy_s = y as isize - offset_y as isize;
|
||||||
let m = app.mbuf.buffer.get(y * gw + x).copied().unwrap_or(0);
|
let fx_s = x as isize - offset_x as isize;
|
||||||
let is_cursor = y == app.cur_y && x == app.cur_x;
|
let in_field = fy_s >= 0 && fx_s >= 0 && (fy_s as usize) < gh && (fx_s as usize) < gw;
|
||||||
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 ch = if g == b'.' {
|
// Outside the field's own bounds is blank canvas, not a dot fill.
|
||||||
if is_cursor {
|
let (ch, style) = if in_field {
|
||||||
'@'
|
let fy = fy_s as usize;
|
||||||
} else if y % 8 == 0 && x % 8 == 0 {
|
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 {
|
} else {
|
||||||
'.'
|
g as char
|
||||||
}
|
};
|
||||||
|
(ch, cell_style(is_cursor, in_sel, m, is_dot))
|
||||||
} else {
|
} else {
|
||||||
g as char
|
(' ', Style::Normal)
|
||||||
};
|
};
|
||||||
|
|
||||||
queue!(out, SetAttribute(Attribute::Reset))?;
|
let idx = y * vis_w + x;
|
||||||
if is_cursor {
|
if !dims_changed && r.cells[idx] == (ch, style) {
|
||||||
queue!(out, SetAttribute(Attribute::Reverse), SetAttribute(Attribute::Bold))?;
|
need_move = true;
|
||||||
} else if in_sel {
|
continue;
|
||||||
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))?;
|
|
||||||
}
|
}
|
||||||
|
if need_move {
|
||||||
|
queue!(out, MoveTo(x as u16, y as u16))?;
|
||||||
|
need_move = false;
|
||||||
|
}
|
||||||
|
apply_style(out, style)?;
|
||||||
queue!(out, Print(ch))?;
|
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();
|
let mut line1: String = status.chars().take(tw as usize).collect();
|
||||||
line1 = format!("{:<width$}", line1, width = tw as usize);
|
line1 = format!("{:<width$}", line1, width = tw as usize);
|
||||||
let help: String = HELP_LINE.chars().take(tw as usize).collect();
|
|
||||||
|
|
||||||
queue!(
|
if line1 != r.line1 {
|
||||||
out,
|
// Plain default text, no reversed block and no hardcoded color — a
|
||||||
SetAttribute(Attribute::Reset),
|
// full-row invert reads as "black on white" on a normal dark
|
||||||
MoveTo(0, th.saturating_sub(2)),
|
// terminal, which is exactly what shouldn't be here.
|
||||||
SetAttribute(Attribute::Reverse),
|
queue!(
|
||||||
Print(line1),
|
out,
|
||||||
SetAttribute(Attribute::Reset),
|
SetAttribute(Attribute::Reset),
|
||||||
MoveTo(0, th.saturating_sub(1)),
|
MoveTo(0, th.saturating_sub(2)),
|
||||||
SetAttribute(Attribute::Dim),
|
Print(&line1),
|
||||||
Print(help),
|
SetAttribute(Attribute::Reset)
|
||||||
SetAttribute(Attribute::Reset),
|
)?;
|
||||||
EndSynchronizedUpdate
|
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()
|
out.flush()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user