Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
T
Tidal
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
PLN
Tidal
Commits
794b6313
Commit
794b6313
authored
Sep 28, 2026
by
PLN (Algolia)
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
bridge midiviz: sync the DevCon skin from the standalone repo
parent
1294c6d1
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
276 additions
and
45 deletions
+276
-45
midiviz.py
tools/bridge/midiviz.py
+276
-45
Jersey15-Regular.ttf
tools/bridge/ui/Jersey15-Regular.ttf
+0
-0
No files found.
tools/bridge/midiviz.py
View file @
794b6313
#!/usr/bin/env python3
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""midiviz — the MIDI stream as a picture, not a transcript.
"""midiviz — the MIDI stream as a picture, not a transcript.
`midimon.py` prints one line per event ("14:0 Control change ch0, controller
`midimon.py` prints one line per event ("14:0 Control change ch0, controller
...
@@ -108,6 +109,30 @@ import midimon # noqa: E402 parse_line / enrich / resolve_po
...
@@ -108,6 +109,30 @@ import midimon # noqa: E402 parse_line / enrich / resolve_po
import
midistream
# noqa: E402 parse_ports
import
midistream
# noqa: E402 parse_ports
import
oscfeed
# noqa: E402 the Tidal HL feed (SuperDirt mirror)
import
oscfeed
# noqa: E402 the Tidal HL feed (SuperDirt mirror)
# ── brand — whose lens this is ─────────────────────────────────────────────
# midiviz was born inside ParVagues (a livecoding rig) and ships wearing it;
# nothing here NEEDS that name. Override the three at launch to wear your own:
#
# MIDIVIZ_BRAND="Coolnaut" MIDIVIZ_WORDMARK="COOLNAUT" \
# MIDIVIZ_BRAND_SLUG="coolnaut" python3 midiviz.py
#
# * BRAND is the spoken name (menu labels, tray presence);
# * WORDMARK is the block lettering drawn over row D (upper-case on purpose —
# that is what the vendored display face is for, see the SYNE_PATH note);
# * BRAND_SLUG is the directory under $XDG_CONFIG_HOME and $XDG_RUNTIME_DIR
# where THIS instance keeps its memory and latches — one per brand, so two
# brands never fight over one saved theme.
# The watermarked wave swaps the same way: MIDIVIZ_WAVE=/path/to/logo.png
# (any transparent PNG; it is painted faint enough to never compete with a
# digit — see the WAVE_PATH note below).
# What branding does NOT touch: the wire paths midiviz READS
# (`~/.cache/parvagues/surface-state.json`, `eval-events.jsonl`) are contracts
# with the producers that write them — a driver, an editor — not decoration.
# Where those live is documented in the README's "wire" section.
BRAND
=
os
.
environ
.
get
(
"MIDIVIZ_BRAND"
,
"ParVagues"
)
WORDMARK
=
os
.
environ
.
get
(
"MIDIVIZ_WORDMARK"
,
"PARVAGUES"
)
BRAND_SLUG
=
os
.
environ
.
get
(
"MIDIVIZ_BRAND_SLUG"
,
"parvagues"
)
# ── which port to watch ────────────────────────────────────────────────────
# ── which port to watch ────────────────────────────────────────────────────
# `midimon.resolve_port()` OWNS this decision and this file delegates to it. Two
# `midimon.resolve_port()` OWNS this decision and this file delegates to it. Two
# things about it are load-bearing and were nearly re-derived wrong here:
# things about it are load-bearing and were nearly re-derived wrong here:
...
@@ -349,6 +374,26 @@ class Theme(NamedTuple):
...
@@ -349,6 +374,26 @@ class Theme(NamedTuple):
scan
:
"tuple[tuple[int, int, int, int], tuple[int, int, int, int]] | None"
scan
:
"tuple[tuple[int, int, int, int], tuple[int, int, int, int]] | None"
bar_frac
:
float
# value-bar height as a fraction of the cell
bar_frac
:
float
# value-bar height as a fraction of the cell
bold
:
bool
# a heavier face, for a screen in daylight
bold
:
bool
# a heavier face, for a screen in daylight
# The face every digit and label is set in: "mono" (the cockpit's
# monospace stack) or "jersey" (the vendored pixel face, see JERSEY_PATH).
ui_font
:
str
# The eight family hues, in FAM_* order. Every theme but devcon keeps the
# ParVagues violet arc (FAM_HUE); a family still MEANS the same role
# whatever hue it wears, so a theme may re-dress it but never reorder it.
fam_hue
:
tuple
[
float
,
...
]
# How a cell body is made. False: `tint` is an HSV ramp, as always. True:
# the family's vivid colour laid OVER the page, `tint`'s value read as how
# much of it -- a warm hue darkened toward black is brown, the same hue
# thinned over navy stays itself (how the DevCon site layers its colour).
tint_over_page
:
bool
# The texture's pitch, in base px: 0 = the CRT (fine lines, a vertical
# every fourth), anything else = a square grid at that pitch -- the
# DevCon artwork's graph paper.
scan_grid
:
float
# The PARVAGUES wordmark (`l`) as the site's hero headline instead of the
# neon: Jersey 15, hero tracking, navy on a baked gradient plate. The
# theme decides, not --word-face, so no other page ever wears the plate.
word_plate
:
bool
# The dark ink ramp, sampled at levels 1..4, IS the dark surface ramp -- which
# The dark ink ramp, sampled at levels 1..4, IS the dark surface ramp -- which
...
@@ -366,7 +411,9 @@ THEMES: dict[str, Theme] = {
...
@@ -366,7 +411,9 @@ THEMES: dict[str, Theme] = {
spec
=
SpecRamp
(
0.68
,
-
0.055
,
0.80
,
0.30
,
0.70
,
12
,
84
),
spec
=
SpecRamp
(
0.68
,
-
0.055
,
0.80
,
0.30
,
0.70
,
12
,
84
),
spec_peak
=
(
0xc8
,
0xa8
,
0xff
,
90
),
spec_peak
=
(
0xc8
,
0xa8
,
0xff
,
90
),
scan
=
((
0xa0
,
0x60
,
0xff
,
16
),
(
0xa0
,
0x60
,
0xff
,
9
)),
scan
=
((
0xa0
,
0x60
,
0xff
,
16
),
(
0xa0
,
0x60
,
0xff
,
9
)),
bar_frac
=
0.22
,
bold
=
False
,
bar_frac
=
0.22
,
bold
=
False
,
ui_font
=
"mono"
,
fam_hue
=
FAM_HUE
,
tint_over_page
=
False
,
scan_grid
=
0.0
,
word_plate
=
False
,
),
),
# Indoors on a pale desktop. Still a lens, still the violet arc, still a
# Indoors on a pale desktop. Still a lens, still the violet arc, still a
# decay gradient the eye can follow -- the ink simply runs the other way.
# decay gradient the eye can follow -- the ink simply runs the other way.
...
@@ -385,7 +432,47 @@ THEMES: dict[str, Theme] = {
...
@@ -385,7 +432,47 @@ THEMES: dict[str, Theme] = {
spec
=
SpecRamp
(
0.62
,
-
0.05
,
0.55
,
0.62
,
-
0.28
,
34
,
120
),
spec
=
SpecRamp
(
0.62
,
-
0.05
,
0.55
,
0.62
,
-
0.28
,
34
,
120
),
spec_peak
=
(
0x4a
,
0x2a
,
0x78
,
110
),
spec_peak
=
(
0x4a
,
0x2a
,
0x78
,
110
),
scan
=
((
0x50
,
0x3a
,
0x78
,
10
),
(
0x50
,
0x3a
,
0x78
,
6
)),
scan
=
((
0x50
,
0x3a
,
0x78
,
10
),
(
0x50
,
0x3a
,
0x78
,
6
)),
bar_frac
=
0.22
,
bold
=
False
,
bar_frac
=
0.22
,
bold
=
False
,
ui_font
=
"mono"
,
fam_hue
=
FAM_HUE
,
tint_over_page
=
False
,
scan_grid
=
0.0
,
word_plate
=
False
,
),
# Direct sunlight, and built for nothing else. Near-white page, heavy ink
# at every decay level (a "dim" state outdoors is a blank state), thicker
# value bars, and no scanline texture to eat the little contrast there is.
# It is not meant to be pretty; it is meant to be the one that still works
# when the laptop is the brightest thing on a table at noon.
# The DevCon26 stage skin, v2 (PLN, 2026-09-28: "not white theme"): the
# event's OWN tokens, pulled from algolia.com/devcon's CSS -- navy page
# #0d1833, electric blue #003dff / #457aff, orange #ff7700, mint
# #07ffda (gradient #adf6ff->#46dbee->#07ffda). The artwork's corail
# (measured #fd956e, 47% of the intro loop) walks the spec wash: one
# linear hue drift, corail 21 deg down through magenta 296 to cyan --
# wrapped, see _build_palette. Family hues stay the orbit identity.
"devcon"
:
Theme
(
name
=
"devcon"
,
bg
=
(
0x0d
,
0x18
,
0x33
),
# Vivid, not grey (PLN, 2026-09-28: "a bit grey, make this pop"): the
# ink keeps its saturation to the top instead of paling out, and the
# cell bodies carry the hue at every heat step. The grey was low
# saturation at low value -- a family hue at s=0.30 on navy is mud.
ink
=
Ramp
(
0.14
,
0.86
,
1.10
,
1.00
,
-
0.22
,
2.00
),
tint
=
Ramp
(
0.10
,
0.42
,
1.00
,
0.85
,
0.00
,
1.00
),
# page->hue mix, see tint_over_page
tint_t
=
_TINT_T_LINEAR
,
chrome
=
Ramp
(
0.70
,
-
0.40
,
0.80
,
0.25
,
0.30
,
1.00
),
chrome_hue
=
216.0
,
# #457aff, the site's electric blue
spec
=
SpecRamp
(
0.058
,
-
0.45
,
0.85
,
0.75
,
-
0.30
,
40
,
140
),
spec_peak
=
(
0x07
,
0xff
,
0xda
,
160
),
# the site's mint
# Pale cyan #a2edf2 hairlines, the artwork's grid on its gradient.
scan
=
((
0xa2
,
0xed
,
0xf2
,
13
),
(
0xa2
,
0xed
,
0xf2
,
13
)),
# The site sets everything that is not body copy in Jersey 15, so
# every digit here is too.
bar_frac
=
0.34
,
bold
=
True
,
ui_font
=
"jersey"
,
# The families in the event's colours (PLN, 2026-09-28: "DevCon
# palette"), from the site's CSS and the artwork's measured mass:
# level = electric blue #457aff, fx = corail #fd956e, fx2 = magenta
# #df96ef, gate = mint #07ffda, gate2 = cyan #46dbee, family = orange
# #ff7700, other = the chrome's blue, note = the artwork's rose.
fam_hue
=
(
223
,
16
,
290
,
171
,
191
,
28
,
216
,
350
),
tint_over_page
=
True
,
scan_grid
=
14.0
,
word_plate
=
True
,
),
),
# Direct sunlight, and built for nothing else. Near-white page, heavy ink
# Direct sunlight, and built for nothing else. Near-white page, heavy ink
# at every decay level (a "dim" state outdoors is a blank state), thicker
# at every decay level (a "dim" state outdoors is a blank state), thicker
...
@@ -411,12 +498,14 @@ THEMES: dict[str, Theme] = {
...
@@ -411,12 +498,14 @@ THEMES: dict[str, Theme] = {
spec
=
SpecRamp
(
0.60
,
-
0.04
,
0.72
,
0.48
,
-
0.26
,
48
,
150
),
spec
=
SpecRamp
(
0.60
,
-
0.04
,
0.72
,
0.48
,
-
0.26
,
48
,
150
),
spec_peak
=
(
0x18
,
0x0c
,
0x38
,
190
),
spec_peak
=
(
0x18
,
0x0c
,
0x38
,
190
),
scan
=
None
,
scan
=
None
,
bar_frac
=
0.34
,
bold
=
True
,
bar_frac
=
0.34
,
bold
=
True
,
ui_font
=
"mono"
,
fam_hue
=
FAM_HUE
,
tint_over_page
=
False
,
scan_grid
=
0.0
,
word_plate
=
False
,
),
),
}
}
# Cycle order for the `d` key and the menu: darkest to brightest page, so the
# Cycle order for the `d` key and the menu: darkest to brightest page, so the
# key has a direction and "one more press" means "one step sunnier".
# key has a direction and "one more press" means "one step sunnier".
THEME_ORDER
:
tuple
[
str
,
...
]
=
(
"dark"
,
"light"
,
"sun"
)
THEME_ORDER
:
tuple
[
str
,
...
]
=
(
"dark"
,
"light"
,
"
devcon"
,
"
sun"
)
DEFAULT_THEME
=
"dark"
DEFAULT_THEME
=
"dark"
# What each page is FOR, for the menu only. `dark`/`light`/`sun` stay the
# What each page is FOR, for the menu only. `dark`/`light`/`sun` stay the
# vocabulary of the config file, the `--theme` flag and every test; a menu row
# vocabulary of the config file, the `--theme` flag and every test; a menu row
...
@@ -425,6 +514,7 @@ THEME_MENU: dict[str, str] = {
...
@@ -425,6 +514,7 @@ THEME_MENU: dict[str, str] = {
"dark"
:
"Dark · the cockpit, indoors"
,
"dark"
:
"Dark · the cockpit, indoors"
,
"light"
:
"Light · a pale desktop"
,
"light"
:
"Light · a pale desktop"
,
"sun"
:
"Sun · daylight, maximum ink"
,
"sun"
:
"Sun · daylight, maximum ink"
,
"devcon"
:
"DevCon · the Algolia stage skin"
,
}
}
TAU
=
0.85
# seconds; brightness e-folding time
TAU
=
0.85
# seconds; brightness e-folding time
...
@@ -475,7 +565,8 @@ BASE_W, BASE_H = 420, 260
...
@@ -475,7 +565,8 @@ BASE_W, BASE_H = 420, 260
# (Perso/www/public/images/parvagues/logo_transparent.png, downscaled to
# (Perso/www/public/images/parvagues/logo_transparent.png, downscaled to
# 640px) because midiviz must run standalone from a checkout with no www
# 640px) because midiviz must run standalone from a checkout with no www
# tree on the box at all.
# tree on the box at all.
WAVE_PATH
=
Path
(
__file__
)
.
resolve
()
.
parent
/
"ui"
/
"parvagues_wave.png"
WAVE_PATH
=
(
Path
(
os
.
environ
[
"MIDIVIZ_WAVE"
])
if
os
.
environ
.
get
(
"MIDIVIZ_WAVE"
)
else
Path
(
__file__
)
.
resolve
()
.
parent
/
"ui"
/
"parvagues_wave.png"
)
WAVE_ALPHA
=
0.12
WAVE_ALPHA
=
0.12
# …and its wordmark, over D1-D4 (PLN, 2026-09-24: "PARVAGUES in nice our
# …and its wordmark, over D1-D4 (PLN, 2026-09-24: "PARVAGUES in nice our
# usual site font bloc letters over D1/2/3/4 alongside the wave same
# usual site font bloc letters over D1/2/3/4 alongside the wave same
...
@@ -484,7 +575,27 @@ WAVE_ALPHA = 0.12
...
@@ -484,7 +575,27 @@ WAVE_ALPHA = 0.12
# recipe) and the pink is --neon-high. The variable font's latin subset is
# recipe) and the pink is --neon-high. The variable font's latin subset is
# vendored out of the built site for the same standalone reason as the wave.
# vendored out of the built site for the same standalone reason as the wave.
SYNE_PATH
=
Path
(
__file__
)
.
resolve
()
.
parent
/
"ui"
/
"syne-extrabold.ttf"
SYNE_PATH
=
Path
(
__file__
)
.
resolve
()
.
parent
/
"ui"
/
"syne-extrabold.ttf"
# Jersey 15 (Soft Type Project, OFL, vendored from google/fonts) — the face of
# the Algolia DevCon26 stage kit, measured, not eyed: algolia.com/devcon's CSS
# sets every headline in 'Jersey 15' (Jersey 25 was a guess, and wrong). Same
# standalone reason as Syne; `--word-face jersey` puts the wordmark in it,
# default stays the brand's Syne.
JERSEY_PATH
=
Path
(
__file__
)
.
resolve
()
.
parent
/
"ui"
/
"Jersey15-Regular.ttf"
WORD_FACES
=
(
"syne"
,
"jersey"
)
JERSEY_UI_SIZE
=
1.3
# px multiplier when Jersey sets the digits
WORD_ALPHA
=
0.8
WORD_ALPHA
=
0.8
# The DevCon hero, read off algolia.com/devcon (.devcon-hero__headline): Jersey
# 15 at weight 400, letter-spacing -2.72px on 136px (-2%), --devcon-navy. The
# plate is the artwork's own gradient (corail -> magenta -> cyan, measured on
# the intro loop and the header) with its pale grid, because navy type needs
# the bright page the site sets it on.
PLATE_INK
=
"#14283f"
PLATE_STOPS
=
((
0.0
,
"#fd956e"
),
(
0.55
,
"#df96ef"
),
(
1.0
,
"#46dbee"
))
PLATE_GRID
=
(
0xff
,
0xff
,
0xff
,
46
)
# Only the gradient is translucent; the letters stay solid. PLN, 2026-09-28:
# "ensure that we still see the D1-3 under the lettering" -- those three cells
# are wired controls, and a sticker that hid them would read as unwired.
PLATE_ALPHA
=
0.5
# ── the HL feed ────────────────────────────────────────────────────────────
# ── the HL feed ────────────────────────────────────────────────────────────
# PLN, 2026-09-24: "please do indeed sub from the events, to indeed feed
# PLN, 2026-09-24: "please do indeed sub from the events, to indeed feed
...
@@ -784,7 +895,7 @@ USER_CLOSE_EXIT = 78
...
@@ -784,7 +895,7 @@ USER_CLOSE_EXIT = 78
#
#
# Writes go through a temp file and `os.replace`, so a crash mid-save leaves
# Writes go through a temp file and `os.replace`, so a crash mid-save leaves
# the previous choice rather than the corrupt file this paragraph is about.
# the previous choice rather than the corrupt file this paragraph is about.
CONFIG_DIR
=
"parvagues"
CONFIG_DIR
=
BRAND_SLUG
# one config dir per brand (see the brand block)
CONFIG_NAME
=
"midiviz.json"
CONFIG_NAME
=
"midiviz.json"
...
@@ -843,7 +954,7 @@ def save_config(theme: str, scale: float) -> bool:
...
@@ -843,7 +954,7 @@ def save_config(theme: str, scale: float) -> bool:
def
latch_close_path
():
def
latch_close_path
():
rt
=
os
.
environ
.
get
(
"XDG_RUNTIME_DIR"
)
or
f
"/run/user/{os.getuid()}"
rt
=
os
.
environ
.
get
(
"XDG_RUNTIME_DIR"
)
or
f
"/run/user/{os.getuid()}"
return
os
.
path
.
join
(
rt
,
"parvagues"
,
f
"{UNIT_NAME}.closed"
)
return
os
.
path
.
join
(
rt
,
CONFIG_DIR
,
f
"{UNIT_NAME}.closed"
)
def
_latch_close
():
def
_latch_close
():
...
@@ -1113,7 +1224,8 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
...
@@ -1113,7 +1224,8 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
pinned_port
:
str
|
None
=
None
,
watch
:
bool
=
False
,
pinned_port
:
str
|
None
=
None
,
watch
:
bool
=
False
,
spectro
:
bool
=
False
,
theme
:
str
=
DEFAULT_THEME
,
spectro
:
bool
=
False
,
theme
:
str
=
DEFAULT_THEME
,
persist
:
bool
=
False
,
tray
:
bool
=
False
,
persist
:
bool
=
False
,
tray
:
bool
=
False
,
hl
:
"oscfeed.HLFeed | None"
=
None
):
hl
:
"oscfeed.HLFeed | None"
=
None
,
word_face
:
str
=
"syne"
):
QtCore
,
QtGui
,
QtWidgets
=
_qt
()
QtCore
,
QtGui
,
QtWidgets
=
_qt
()
Qt
=
QtCore
.
Qt
Qt
=
QtCore
.
Qt
...
@@ -1200,6 +1312,14 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
...
@@ -1200,6 +1312,14 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
str
(
SYNE_PATH
))
str
(
SYNE_PATH
))
except
Exception
:
except
Exception
:
self
.
_syne_id
=
-
1
self
.
_syne_id
=
-
1
# Same deal, same non-crash rule: a checkout without Jersey loses
# only the alternate wordmark face, never the lens.
try
:
self
.
_jersey_id
=
QtGui
.
QFontDatabase
.
addApplicationFont
(
str
(
JERSEY_PATH
))
except
Exception
:
self
.
_jersey_id
=
-
1
self
.
_word_face
=
word_face
if
word_face
in
WORD_FACES
else
"syne"
self
.
_word_font
=
None
self
.
_word_font
=
None
self
.
_word_font_key
=
None
self
.
_word_font_key
=
None
self
.
_word_pm
=
None
self
.
_word_pm
=
None
...
@@ -1226,7 +1346,7 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
...
@@ -1226,7 +1346,7 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
self
.
_ref_w
=
max
(
1
,
round
(
BASE_W
*
self
.
scale
))
self
.
_ref_w
=
max
(
1
,
round
(
BASE_W
*
self
.
scale
))
self
.
_ref_h
=
max
(
1
,
round
(
BASE_H
*
self
.
scale
))
self
.
_ref_h
=
max
(
1
,
round
(
BASE_H
*
self
.
scale
))
self
.
dens
=
self
.
scale
self
.
dens
=
self
.
scale
self
.
_metrics_key
=
None
# (main px, micro px, bold) last measured
self
.
_metrics_key
=
None
# (main px, micro px, bold
, face
) last measured
self
.
setMinimumSize
(
MIN_W
,
MIN_H
)
self
.
setMinimumSize
(
MIN_W
,
MIN_H
)
self
.
resize
(
self
.
_ref_w
,
self
.
_ref_h
)
self
.
resize
(
self
.
_ref_w
,
self
.
_ref_h
)
# …then read it BACK off the widget. If a minimumSize ever clamped
# …then read it BACK off the widget. If a minimumSize ever clamped
...
@@ -1344,7 +1464,7 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
...
@@ -1344,7 +1464,7 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
# LUT[family][level] — every INK colour this window can ever paint,
# LUT[family][level] — every INK colour this window can ever paint,
# so a frame constructs no QColor at all.
# so a frame constructs no QColor at all.
self
.
lut
=
[]
self
.
lut
=
[]
for
hue
in
FAM_HUE
:
for
hue
in
th
.
fam_hue
:
col
=
[]
col
=
[]
for
lv
in
range
(
LEVELS
):
for
lv
in
range
(
LEVELS
):
val
,
sat
=
th
.
ink
.
at
(
lv
/
(
LEVELS
-
1
))
val
,
sat
=
th
.
ink
.
at
(
lv
/
(
LEVELS
-
1
))
...
@@ -1357,10 +1477,16 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
...
@@ -1357,10 +1477,16 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
# separate, shallower ramp, because a panel must stay near the page
# separate, shallower ramp, because a panel must stay near the page
# while the ink on it walks away from it.
# while the ink on it walks away from it.
self
.
tint
=
[]
self
.
tint
=
[]
for
hue
in
FAM_HUE
:
for
hue
in
th
.
fam_hue
:
row
=
[]
row
=
[]
for
t
in
th
.
tint_t
:
for
t
in
th
.
tint_t
:
val
,
sat
=
th
.
tint
.
at
(
t
)
val
,
sat
=
th
.
tint
.
at
(
t
)
if
th
.
tint_over_page
:
c
=
QtGui
.
QColor
.
fromHsvF
(
hue
/
360.0
,
sat
,
1.0
)
row
.
append
(
QtGui
.
QColor
(
*
(
round
(
b
+
(
f
-
b
)
*
val
)
for
b
,
f
in
zip
(
th
.
bg
,
(
c
.
red
(),
c
.
green
(),
c
.
blue
())))))
else
:
row
.
append
(
QtGui
.
QColor
.
fromHsvF
(
hue
/
360.0
,
sat
,
val
))
row
.
append
(
QtGui
.
QColor
.
fromHsvF
(
hue
/
360.0
,
sat
,
val
))
self
.
tint
.
append
(
row
)
self
.
tint
.
append
(
row
)
# Chrome (border, row letters, CC reference digits, the two header
# Chrome (border, row letters, CC reference digits, the two header
...
@@ -1383,7 +1509,11 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
...
@@ -1383,7 +1509,11 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
# else, so a spectrum frame constructs no QColor.
# else, so a spectrum frame constructs no QColor.
sp
=
th
.
spec
sp
=
th
.
spec
self
.
spec_lut
=
[
self
.
spec_lut
=
[
QtGui
.
QColor
.
fromHsvF
(
sp
.
h0
+
sp
.
hspan
*
(
lv
/
(
SPEC_LEVELS
-
1
)),
# The hue drift wraps (`% 1.0`): a theme may walk the wheel
# through 0 — DevCon26's does, corail 21 deg descending to
# cyan past magenta — and fromHsvF rejects a negative hue.
# Identity on every theme that never crosses zero.
QtGui
.
QColor
.
fromHsvF
((
sp
.
h0
+
sp
.
hspan
*
(
lv
/
(
SPEC_LEVELS
-
1
)))
%
1.0
,
sp
.
sat
,
sp
.
v0
+
sp
.
vspan
*
(
lv
/
(
SPEC_LEVELS
-
1
)),
sp
.
sat
,
sp
.
v0
+
sp
.
vspan
*
(
lv
/
(
SPEC_LEVELS
-
1
)),
(
sp
.
a0
+
sp
.
aspan
*
(
lv
/
(
SPEC_LEVELS
-
1
)))
/
255.0
)
(
sp
.
a0
+
sp
.
aspan
*
(
lv
/
(
SPEC_LEVELS
-
1
)))
/
255.0
)
for
lv
in
range
(
SPEC_LEVELS
)
for
lv
in
range
(
SPEC_LEVELS
)
...
@@ -1401,10 +1531,10 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
...
@@ -1401,10 +1531,10 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
th
=
THEMES
.
get
(
name
)
th
=
THEMES
.
get
(
name
)
if
th
is
None
or
th
is
self
.
theme
:
if
th
is
None
or
th
is
self
.
theme
:
return
return
was_
bold
=
self
.
theme
.
bold
was_
face
=
(
self
.
theme
.
bold
,
self
.
theme
.
ui_font
)
self
.
theme
=
th
self
.
theme
=
th
self
.
_build_palette
()
self
.
_build_palette
()
if
th
.
bold
!=
was_bold
:
if
(
th
.
bold
,
th
.
ui_font
)
!=
was_face
:
self
.
_apply_scale
()
# new weight, new metrics, new layout
self
.
_apply_scale
()
# new weight, new metrics, new layout
if
getattr
(
self
,
"tray"
,
None
)
is
not
None
:
if
getattr
(
self
,
"tray"
,
None
)
is
not
None
:
# The tray icon is painted from the palette, so it goes stale
# The tray icon is painted from the palette, so it goes stale
...
@@ -1429,6 +1559,21 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
...
@@ -1429,6 +1559,21 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
save_config
(
self
.
theme
.
name
,
self
.
scale
)
save_config
(
self
.
theme
.
name
,
self
.
scale
)
def
_font
(
self
,
px
:
int
):
def
_font
(
self
,
px
:
int
):
if
self
.
theme
.
ui_font
==
"jersey"
and
self
.
_jersey_id
>=
0
:
fams
=
QtGui
.
QFontDatabase
.
applicationFontFamilies
(
self
.
_jersey_id
)
if
fams
:
f
=
QtGui
.
QFont
(
fams
[
0
])
# Jersey's caps stand shorter in their em than the mono's,
# so the same pixel size reads a size smaller.
f
.
setPixelSize
(
max
(
5
,
round
(
px
*
JERSEY_UI_SIZE
)))
# Tabular figures, so every digit shares one advance. A-F
# still do not (see `_paint_stream`). And no bold: Jersey
# ships a Regular only, and a synthesized bold smears the
# pixel grid that is the whole point of the face.
f
.
setFeature
(
QtGui
.
QFont
.
Tag
(
"tnum"
),
1
)
return
f
# No Jersey on this checkout (id -1): the mono stack, silently,
# the same non-crash rule as the wordmark.
f
=
QtGui
.
QFont
()
f
=
QtGui
.
QFont
()
f
.
setFamilies
([
"Geist Mono"
,
"JetBrains Mono"
,
"Fira Code"
,
f
.
setFamilies
([
"Geist Mono"
,
"JetBrains Mono"
,
"Fira Code"
,
"DejaVu Sans Mono"
,
"monospace"
])
"DejaVu Sans Mono"
,
"monospace"
])
...
@@ -1470,7 +1615,7 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
...
@@ -1470,7 +1615,7 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
"""
"""
self
.
dens
=
d
=
self
.
_density
()
self
.
dens
=
d
=
self
.
_density
()
px
,
upx
=
max
(
5
,
round
(
11
*
d
)),
max
(
5
,
round
(
8
*
d
))
px
,
upx
=
max
(
5
,
round
(
11
*
d
)),
max
(
5
,
round
(
8
*
d
))
key
=
(
px
,
upx
,
self
.
theme
.
bold
)
key
=
(
px
,
upx
,
self
.
theme
.
bold
,
self
.
theme
.
ui_font
)
if
key
!=
self
.
_metrics_key
:
if
key
!=
self
.
_metrics_key
:
# Re-measured only when the FACE actually changed. The weight
# Re-measured only when the FACE actually changed. The weight
# belongs in that key because `set_theme` calls this precisely
# belongs in that key because `set_theme` calls this precisely
...
@@ -1481,7 +1626,11 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
...
@@ -1481,7 +1626,11 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
self
.
f_micro
=
self
.
_font
(
upx
)
self
.
f_micro
=
self
.
_font
(
upx
)
fm
=
QtGui
.
QFontMetricsF
(
self
.
f_main
)
fm
=
QtGui
.
QFontMetricsF
(
self
.
f_main
)
fmm
=
QtGui
.
QFontMetricsF
(
self
.
f_micro
)
fmm
=
QtGui
.
QFontMetricsF
(
self
.
f_micro
)
self
.
mw
,
self
.
mh
=
fm
.
horizontalAdvance
(
"0"
),
fm
.
height
()
# The WIDEST hex glyph is the ribbon's grid step; on a
# monospaced face they are all the same and this is "0".
self
.
mw
=
max
(
fm
.
horizontalAdvance
(
c
)
for
c
in
HEX
)
self
.
mh
=
fm
.
height
()
self
.
_hex_mono
=
len
({
fm
.
horizontalAdvance
(
c
)
for
c
in
HEX
})
==
1
self
.
uw
,
self
.
uh
=
fmm
.
horizontalAdvance
(
"0"
),
fmm
.
height
()
self
.
uw
,
self
.
uh
=
fmm
.
horizontalAdvance
(
"0"
),
fmm
.
height
()
self
.
m_asc
,
self
.
u_asc
=
fm
.
ascent
(),
fmm
.
ascent
()
self
.
m_asc
,
self
.
u_asc
=
fm
.
ascent
(),
fmm
.
ascent
()
self
.
_relayout
()
self
.
_relayout
()
...
@@ -1538,13 +1687,14 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
...
@@ -1538,13 +1687,14 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
pm
=
QtGui
.
QPixmap
(
w
,
h
)
pm
=
QtGui
.
QPixmap
(
w
,
h
)
pm
.
fill
(
QtGui
.
QColor
(
0
,
0
,
0
,
0
))
pm
.
fill
(
QtGui
.
QColor
(
0
,
0
,
0
,
0
))
p
=
QtGui
.
QPainter
(
pm
)
p
=
QtGui
.
QPainter
(
pm
)
step
=
max
(
2
,
round
(
3
*
self
.
dens
))
grid
=
self
.
theme
.
scan_grid
step
=
max
(
2
,
round
((
grid
or
3
)
*
self
.
dens
))
hz
,
vt
=
self
.
theme
.
scan
hz
,
vt
=
self
.
theme
.
scan
p
.
setPen
(
QtGui
.
QColor
(
*
hz
))
p
.
setPen
(
QtGui
.
QColor
(
*
hz
))
for
y
in
range
(
0
,
h
,
step
):
for
y
in
range
(
0
,
h
,
step
):
p
.
drawLine
(
0
,
y
,
w
,
y
)
p
.
drawLine
(
0
,
y
,
w
,
y
)
p
.
setPen
(
QtGui
.
QColor
(
*
vt
))
p
.
setPen
(
QtGui
.
QColor
(
*
vt
))
for
x
in
range
(
0
,
w
,
step
*
4
):
for
x
in
range
(
0
,
w
,
step
if
grid
else
step
*
4
):
p
.
drawLine
(
x
,
0
,
x
,
h
)
p
.
drawLine
(
x
,
0
,
x
,
h
)
p
.
end
()
p
.
end
()
self
.
_scan
=
pm
self
.
_scan
=
pm
...
@@ -1729,17 +1879,20 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
...
@@ -1729,17 +1879,20 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
spacing and kerned sub-advances disagreed, and that arithmetic is
spacing and kerned sub-advances disagreed, and that arithmetic is
exactly how "PARVAGU" lost its tail. The bake measures its own
exactly how "PARVAGU" lost its tail. The bake measures its own
ink instead (see _word_pixmap)."""
ink instead (see _word_pixmap)."""
key
=
(
round
(
target_w
),
round
(
target_h
))
plate
=
self
.
theme
.
word_plate
face
=
"jersey"
if
plate
else
self
.
_word_face
key
=
(
face
,
plate
,
round
(
target_w
),
round
(
target_h
))
if
self
.
_word_font_key
==
key
:
if
self
.
_word_font_key
==
key
:
return
self
.
_word_font
return
self
.
_word_font
fam_id
=
self
.
_syne_id
fam_id
=
self
.
_
jersey_id
if
face
==
"jersey"
else
self
.
_
syne_id
if
fam_id
<
0
:
if
fam_id
<
0
:
return
None
return
None
fam
=
QtGui
.
QFontDatabase
.
applicationFontFamilies
(
fam_id
)[
0
]
fam
=
QtGui
.
QFontDatabase
.
applicationFontFamilies
(
fam_id
)[
0
]
f
=
QtGui
.
QFont
(
fam
)
f
=
QtGui
.
QFont
(
fam
)
f
.
setPixelSize
(
max
(
6
,
target_h
*
0.92
))
f
.
setPixelSize
(
max
(
6
,
target_h
*
0.92
))
f
.
setLetterSpacing
(
QtGui
.
QFont
.
SpacingType
.
PercentageSpacing
,
f
.
setLetterSpacing
(
QtGui
.
QFont
.
SpacingType
.
PercentageSpacing
,
106.0
)
# +0.06em, as the hero tracks
98.0
if
plate
# -2%, the DevCon hero
else
106.0
)
# +0.06em, as the hero tracks
self
.
_word_font
,
self
.
_word_font_key
=
f
,
key
self
.
_word_font
,
self
.
_word_font_key
=
f
,
key
return
f
return
f
...
@@ -1747,7 +1900,7 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
...
@@ -1747,7 +1900,7 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
"""One pass of the word: glyphs drawn one by one with the tracked
"""One pass of the word: glyphs drawn one by one with the tracked
advance, so the letter spacing is OURS and identical under the
advance, so the letter spacing is OURS and identical under the
glow rings and the core."""
glow rings and the core."""
word
=
"PARVAGUES"
word
=
WORDMARK
# the brand's block lettering, one glyph at a time
qp
.
setPen
(
pen
)
qp
.
setPen
(
pen
)
x
=
float
(
pad
)
x
=
float
(
pad
)
for
c
in
word
:
for
c
in
word
:
...
@@ -1782,14 +1935,16 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
...
@@ -1782,14 +1935,16 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
PARVAGU"), and it must FIT the D1 cell at the cell's margin —
PARVAGU"), and it must FIT the D1 cell at the cell's margin —
which means the TYPE shrinks to the width, honestly, rather than
which means the TYPE shrinks to the width, honestly, rather than
a squash that would make the letters strangers to themselves."""
a squash that would make the letters strangers to themselves."""
key
=
(
round
(
span_w
),
round
(
span_h
))
plate
=
self
.
theme
.
word_plate
key
=
(
round
(
span_w
),
round
(
span_h
),
plate
)
if
self
.
_word_pm_key
==
key
:
if
self
.
_word_pm_key
==
key
:
return
self
.
_word_pm
return
self
.
_word_pm
f
=
self
.
_wave_font
(
span_w
,
span_h
)
f
=
self
.
_wave_font
(
span_w
,
span_h
)
if
f
is
None
:
if
f
is
None
:
return
None
return
None
ink_col
=
QtGui
.
QColor
(
PLATE_INK
if
plate
else
"#d900ff"
)
def
bake
(
px
,
canvas_w
,
glow
=
Tru
e
):
def
bake
(
px
,
canvas_w
,
glow
=
not
plat
e
):
fb
=
QtGui
.
QFont
(
f
)
fb
=
QtGui
.
QFont
(
f
)
fb
.
setPixelSize
(
max
(
4
,
px
))
fb
.
setPixelSize
(
max
(
4
,
px
))
fmb
=
QtGui
.
QFontMetricsF
(
fb
)
fmb
=
QtGui
.
QFontMetricsF
(
fb
)
...
@@ -1815,9 +1970,10 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
...
@@ -1815,9 +1970,10 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
qp
.
translate
(
math
.
cos
(
a
)
*
rad
,
math
.
sin
(
a
)
*
rad
)
qp
.
translate
(
math
.
cos
(
a
)
*
rad
,
math
.
sin
(
a
)
*
rad
)
self
.
_draw_word_layer
(
qp
,
base
,
pad_b
,
fmb
,
glow_pen
)
self
.
_draw_word_layer
(
qp
,
base
,
pad_b
,
fmb
,
glow_pen
)
qp
.
restore
()
qp
.
restore
()
# the core: --neon-high itself, the site's own pink
# the core: --neon-high itself, the site's own pink (or,
# on the plate, the DevCon navy -- pure, no glow)
self
.
_draw_word_layer
(
qp
,
base
,
pad_b
,
fmb
,
self
.
_draw_word_layer
(
qp
,
base
,
pad_b
,
fmb
,
QtGui
.
QPen
(
QtGui
.
QColor
(
"#d900ff"
)
))
QtGui
.
QPen
(
ink_col
))
finally
:
finally
:
qp
.
end
()
qp
.
end
()
return
img
,
pad_b
return
img
,
pad_b
...
@@ -1837,7 +1993,14 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
...
@@ -1837,7 +1993,14 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
px
=
max
(
4.0
,
px
*
fit
/
word_w
)
px
=
max
(
4.0
,
px
*
fit
/
word_w
)
img
,
pad
=
bake
(
px
,
span_w
+
int
(
px
*
2
))
img
,
pad
=
bake
(
px
,
span_w
+
int
(
px
*
2
))
else
:
else
:
img
,
pad
=
bake
(
px
,
word_w
+
pad
*
2
)
# pad1, not pad: pad is not bound on this path. It went unseen
# behind PARVAGUES (long enough to always take the fit branch
# at every geometry the selftest tries) — a SHORTER wordmark,
# i.e. exactly what the brand override invites, lands here.
# pad1 came from the probe bake at the same px, and bake's
# margin is a pure function of px, so it is the same number
# this bake is about to use.
img
,
pad
=
bake
(
px
,
word_w
+
pad1
*
2
)
# crop to the true ink so the margins are the cell's, no more
# crop to the true ink so the margins are the cell's, no more
left
=
0
left
=
0
w_
=
img
.
width
()
w_
=
img
.
width
()
...
@@ -1850,9 +2013,38 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
...
@@ -1850,9 +2013,38 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
min
(
w_
,
right
-
left
+
1
+
pad
),
min
(
w_
,
right
-
left
+
1
+
pad
),
img
.
height
())
img
.
height
())
pm
=
QtGui
.
QPixmap
.
fromImage
(
cropped
)
pm
=
QtGui
.
QPixmap
.
fromImage
(
cropped
)
if
plate
:
pm
=
self
.
_plate_pixmap
(
span_w
,
span_h
,
pm
)
self
.
_word_pm
,
self
.
_word_pm_key
=
pm
,
key
self
.
_word_pm
,
self
.
_word_pm_key
=
pm
,
key
return
pm
return
pm
def
_plate_pixmap
(
self
,
span_w
,
span_h
,
word_pm
):
"""The DevCon hero as one baked picture: gradient, grid, word.
Built once per geometry like the word itself, so a frame is still
one blit and constructs no gradient."""
pw
,
ph
=
max
(
1
,
round
(
span_w
)),
max
(
1
,
round
(
span_h
))
pm
=
QtGui
.
QPixmap
(
pw
,
ph
)
pm
.
fill
(
QtGui
.
QColor
(
0
,
0
,
0
,
0
))
qp
=
QtGui
.
QPainter
(
pm
)
try
:
g
=
QtGui
.
QLinearGradient
(
0
,
0
,
pw
,
ph
)
for
at
,
col
in
PLATE_STOPS
:
g
.
setColorAt
(
at
,
QtGui
.
QColor
(
col
))
qp
.
setOpacity
(
PLATE_ALPHA
)
qp
.
fillRect
(
0
,
0
,
pw
,
ph
,
QtGui
.
QBrush
(
g
))
qp
.
setPen
(
QtGui
.
QColor
(
*
PLATE_GRID
))
step
=
max
(
4
,
round
(
ph
/
5
))
for
x
in
range
(
step
,
pw
,
step
):
qp
.
drawLine
(
x
,
0
,
x
,
ph
)
for
y
in
range
(
step
,
ph
,
step
):
qp
.
drawLine
(
0
,
y
,
pw
,
y
)
qp
.
setOpacity
(
1.0
)
qp
.
drawPixmap
(
round
((
pw
-
word_pm
.
width
())
/
2.0
),
round
((
ph
-
word_pm
.
height
())
/
2.0
),
word_pm
)
finally
:
qp
.
end
()
return
pm
def
_col_has_ink
(
self
,
img
,
x
):
def
_col_has_ink
(
self
,
img
,
x
):
return
any
(
img
.
pixelColor
(
x
,
y
)
.
alpha
()
>
8
return
any
(
img
.
pixelColor
(
x
,
y
)
.
alpha
()
>
8
for
y
in
range
(
img
.
height
()))
for
y
in
range
(
img
.
height
()))
...
@@ -1868,7 +2060,8 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
...
@@ -1868,7 +2060,8 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
if
pm
is
None
:
if
pm
is
None
:
return
return
row_y
=
self
.
my
+
3
*
self
.
ch
# PHYSICAL_ORDER index of D
row_y
=
self
.
my
+
3
*
self
.
ch
# PHYSICAL_ORDER index of D
p
.
setOpacity
(
WORD_ALPHA
)
# the plate carries its own alpha (see PLATE_ALPHA)
p
.
setOpacity
(
1.0
if
self
.
theme
.
word_plate
else
WORD_ALPHA
)
try
:
try
:
# clamped to the span's outer rect: at the smallest windows
# clamped to the span's outer rect: at the smallest windows
# the glow ring can reach 1px past the inner margin, and a
# the glow ring can reach 1px past the inner margin, and a
...
@@ -2545,9 +2738,8 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
...
@@ -2545,9 +2738,8 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
One drawText per glyph was 2.1 ms of an 8.9 ms frame. Consecutive
One drawText per glyph was 2.1 ms of an 8.9 ms frame. Consecutive
entries sharing a colour AND a decay level are concatenated into one
entries sharing a colour AND a decay level are concatenated into one
call — which is legitimate only for hex glyphs, because those are
call — which is legitimate only for hex glyphs, and only on a
guaranteed to come from the monospaced face at exactly `self.mw`
monospaced face, where they all advance exactly `self.mw`. A class glyph (◆ ≈ ≡ …) may be served by a fallback face
advance. A class glyph (◆ ≈ ≡ …) may be served by a fallback face
with a different advance, so those still draw one at a time or the
with a different advance, so those still draw one at a time or the
ribbon would drift out of its grid.
ribbon would drift out of its grid.
"""
"""
...
@@ -2561,7 +2753,9 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
...
@@ -2561,7 +2753,9 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
run_x
,
run_key
=
x
,
(
0
,
0
)
run_x
,
run_key
=
x
,
(
0
,
0
)
for
g
,
fam
,
t
in
self
.
stream
:
for
g
,
fam
,
t
in
self
.
stream
:
lv
=
self
.
_lv
(
now
-
t
,
floor
=
2
)
lv
=
self
.
_lv
(
now
-
t
,
floor
=
2
)
key
=
(
fam
,
lv
)
if
g
in
HEX
else
None
# Runs need every hex glyph at exactly `mw`; a proportional
# face (Jersey's A-F) gets one call per glyph instead.
key
=
(
fam
,
lv
)
if
g
in
HEX
and
self
.
_hex_mono
else
None
if
run
and
key
==
run_key
:
if
run
and
key
==
run_key
:
run
.
append
(
g
)
run
.
append
(
g
)
else
:
else
:
...
@@ -2738,7 +2932,7 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
...
@@ -2738,7 +2932,7 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
# on black read as the two different pages they are.
# on black read as the two different pages they are.
for
i
,
fam
in
enumerate
((
FAM_LEVEL
,
FAM_FX
,
FAM_NOTE
)):
for
i
,
fam
in
enumerate
((
FAM_LEVEL
,
FAM_FX
,
FAM_NOTE
)):
val
,
sat
=
th
.
ink
.
at
(
1.0
-
i
*
0.30
)
val
,
sat
=
th
.
ink
.
at
(
1.0
-
i
*
0.30
)
p
.
setBrush
(
QtGui
.
QColor
.
fromHsvF
(
FAM_HUE
[
fam
]
/
360.0
,
sat
,
val
))
p
.
setBrush
(
QtGui
.
QColor
.
fromHsvF
(
th
.
fam_hue
[
fam
]
/
360.0
,
sat
,
val
))
p
.
drawRect
(
6
,
5
+
i
*
9
,
n
-
13
-
i
*
4
,
6
)
p
.
drawRect
(
6
,
5
+
i
*
9
,
n
-
13
-
i
*
4
,
6
)
edge
=
th
.
chrome
or
th
.
ink
edge
=
th
.
chrome
or
th
.
ink
val
,
sat
=
edge
.
at
(
0.55
)
val
,
sat
=
edge
.
at
(
0.55
)
...
@@ -2788,7 +2982,7 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
...
@@ -2788,7 +2982,7 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
grp
.
addAction
(
a
)
grp
.
addAction
(
a
)
m
.
addAction
(
a
)
m
.
addAction
(
a
)
self
.
_theme_acts
[
name
]
=
a
self
.
_theme_acts
[
name
]
=
a
self
.
_act_cycle
=
QtGui
.
QAction
(
"Next
pag
e
\t
d"
,
self
)
self
.
_act_cycle
=
QtGui
.
QAction
(
"Next
them
e
\t
d"
,
self
)
self
.
_act_cycle
.
triggered
.
connect
(
lambda
_c
=
False
:
self
.
cycle_theme
())
self
.
_act_cycle
.
triggered
.
connect
(
lambda
_c
=
False
:
self
.
cycle_theme
())
m
.
addAction
(
self
.
_act_cycle
)
m
.
addAction
(
self
.
_act_cycle
)
...
@@ -2831,15 +3025,15 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
...
@@ -2831,15 +3025,15 @@ def build_widget(port_label: str, reader: "Reader | None", scale: float = 1.0,
self
.
_act_spec
.
setCheckable
(
True
)
self
.
_act_spec
.
setCheckable
(
True
)
self
.
_act_spec
.
triggered
.
connect
(
lambda
_c
=
False
:
self
.
toggle_spectro
())
self
.
_act_spec
.
triggered
.
connect
(
lambda
_c
=
False
:
self
.
toggle_spectro
())
m
.
addAction
(
self
.
_act_spec
)
m
.
addAction
(
self
.
_act_spec
)
self
.
_act_hl
=
QtGui
.
QAction
(
"Tidal
HL
feed
\t
h"
,
self
)
self
.
_act_hl
=
QtGui
.
QAction
(
"Tidal
orbit
feed
\t
h"
,
self
)
self
.
_act_hl
.
setCheckable
(
True
)
self
.
_act_hl
.
setCheckable
(
True
)
self
.
_act_hl
.
triggered
.
connect
(
lambda
_c
=
False
:
self
.
toggle_hl
())
self
.
_act_hl
.
triggered
.
connect
(
lambda
_c
=
False
:
self
.
toggle_hl
())
m
.
addAction
(
self
.
_act_hl
)
m
.
addAction
(
self
.
_act_hl
)
self
.
_act_wave
=
QtGui
.
QAction
(
"ParVagues
wave
\t
w"
,
self
)
self
.
_act_wave
=
QtGui
.
QAction
(
f
"{BRAND}
wave
\t
w"
,
self
)
self
.
_act_wave
.
setCheckable
(
True
)
self
.
_act_wave
.
setCheckable
(
True
)
self
.
_act_wave
.
triggered
.
connect
(
lambda
_c
=
False
:
self
.
toggle_wave
())
self
.
_act_wave
.
triggered
.
connect
(
lambda
_c
=
False
:
self
.
toggle_wave
())
m
.
addAction
(
self
.
_act_wave
)
m
.
addAction
(
self
.
_act_wave
)
self
.
_act_word
=
QtGui
.
QAction
(
"PARVAGUES
lettering
\t
l"
,
self
)
self
.
_act_word
=
QtGui
.
QAction
(
f
"{WORDMARK}
lettering
\t
l"
,
self
)
self
.
_act_word
.
setCheckable
(
True
)
self
.
_act_word
.
setCheckable
(
True
)
self
.
_act_word
.
triggered
.
connect
(
lambda
_c
=
False
:
self
.
toggle_word
())
self
.
_act_word
.
triggered
.
connect
(
lambda
_c
=
False
:
self
.
toggle_word
())
m
.
addAction
(
self
.
_act_word
)
m
.
addAction
(
self
.
_act_word
)
...
@@ -3443,7 +3637,9 @@ def selftest_config() -> tuple[bool, str]:
...
@@ -3443,7 +3637,9 @@ def selftest_config() -> tuple[bool, str]:
def
selftest
(
seconds
:
float
=
3.0
,
show
:
bool
=
False
,
def
selftest
(
seconds
:
float
=
3.0
,
show
:
bool
=
False
,
theme
:
str
=
DEFAULT_THEME
)
->
int
:
theme
:
str
=
DEFAULT_THEME
,
shot_dir
:
str
|
None
=
None
)
->
int
:
if
shot_dir
:
os
.
makedirs
(
shot_dir
,
exist_ok
=
True
)
# img.save() no-ops on a missing dir
if
not
show
:
if
not
show
:
os
.
environ
.
setdefault
(
"QT_QPA_PLATFORM"
,
"offscreen"
)
os
.
environ
.
setdefault
(
"QT_QPA_PLATFORM"
,
"offscreen"
)
os
.
environ
.
setdefault
(
"QT_LOGGING_RULES"
,
"qt.qpa.*=false"
)
os
.
environ
.
setdefault
(
"QT_LOGGING_RULES"
,
"qt.qpa.*=false"
)
...
@@ -3516,7 +3712,30 @@ def selftest(seconds: float = 3.0, show: bool = False,
...
@@ -3516,7 +3712,30 @@ def selftest(seconds: float = 3.0, show: bool = False,
w
.
resize
(
sw
,
sh
)
w
.
resize
(
sw
,
sh
)
app
.
processEvents
()
app
.
processEvents
()
w
.
tick
()
w
.
tick
()
# Re-feed before every grab: the decay is seconds-long and the loop
# above already spent most of them, so a picture taken now would show
# the resting state rather than the lens working.
for
ev
in
parsed
[:
24
]:
w
.
ingest
(
ev
)
app
.
processEvents
()
img
=
w
.
grab
()
.
toImage
()
img
=
w
.
grab
()
.
toImage
()
if
shot_dir
and
(
sw
,
sh
)
==
(
1600
,
1000
):
img
.
save
(
os
.
path
.
join
(
shot_dir
,
"hero-
%
s.png"
%
w
.
theme
.
name
))
# The brand layer is OFF by default (it is atmosphere, and the
# default lens must be pure measurement), so a screenshot set that
# never turns it on would fail to document it. Enable once here;
# every theme grab after this carries the watermark too.
if
not
w
.
wave
:
w
.
toggle_wave
()
if
not
w
.
word
:
w
.
toggle_word
()
app
.
processEvents
()
for
ev
in
parsed
[:
24
]:
w
.
ingest
(
ev
)
w
.
tick
()
app
.
processEvents
()
w
.
grab
()
.
toImage
()
.
save
(
os
.
path
.
join
(
shot_dir
,
"hero-branded-
%
s.png"
%
w
.
theme
.
name
))
shapes
[
"
%
dx
%
d"
%
(
sw
,
sh
)]
=
round
(
w
.
dens
,
2
)
shapes
[
"
%
dx
%
d"
%
(
sw
,
sh
)]
=
round
(
w
.
dens
,
2
)
if
img
.
width
()
!=
sw
or
img
.
height
()
!=
sh
:
if
img
.
width
()
!=
sw
or
img
.
height
()
!=
sh
:
shapes_ok
=
False
shapes_ok
=
False
...
@@ -3540,8 +3759,12 @@ def selftest(seconds: float = 3.0, show: bool = False,
...
@@ -3540,8 +3759,12 @@ def selftest(seconds: float = 3.0, show: bool = False,
QtCore
.
Qt
.
Key
.
Key_D
,
QtCore
.
Qt
.
Key
.
Key_D
,
QtCore
.
Qt
.
KeyboardModifier
.
NoModifier
))
QtCore
.
Qt
.
KeyboardModifier
.
NoModifier
))
w
.
tick
()
w
.
tick
()
for
ev
in
parsed
[:
24
]:
w
.
ingest
(
ev
)
app
.
processEvents
()
app
.
processEvents
()
img
=
w
.
grab
()
.
toImage
()
img
=
w
.
grab
()
.
toImage
()
if
shot_dir
:
img
.
save
(
os
.
path
.
join
(
shot_dir
,
"theme-
%
s.png"
%
w
.
theme
.
name
))
sx
,
sy
=
max
(
1
,
img
.
width
()
//
12
),
max
(
1
,
img
.
height
()
//
12
)
sx
,
sy
=
max
(
1
,
img
.
width
()
//
12
),
max
(
1
,
img
.
height
()
//
12
)
seen
=
{
img
.
pixel
(
xx
,
yy
)
seen
=
{
img
.
pixel
(
xx
,
yy
)
for
yy
in
range
(
0
,
img
.
height
(),
sy
)
for
yy
in
range
(
0
,
img
.
height
(),
sy
)
...
@@ -3592,9 +3815,12 @@ def main(argv=None) -> int:
...
@@ -3592,9 +3815,12 @@ def main(argv=None) -> int:
help
=
"initial scale, 0.6-2.4 (also +/- at runtime; "
help
=
"initial scale, 0.6-2.4 (also +/- at runtime; "
"remembered between runs)"
)
"remembered between runs)"
)
ap
.
add_argument
(
"--theme"
,
choices
=
sorted
(
THEMES
),
default
=
None
,
ap
.
add_argument
(
"--theme"
,
choices
=
sorted
(
THEMES
),
default
=
None
,
help
=
"
palett
e: dark (the cockpit), light (pale desktop), "
help
=
"
them
e: dark (the cockpit), light (pale desktop), "
"sun (maximum contrast, for playing outdoors); "
"sun (maximum contrast, for playing outdoors); "
"'d' cycles at runtime and the choice is remembered"
)
"'d' cycles at runtime and the choice is remembered"
)
ap
.
add_argument
(
"--word-face"
,
choices
=
WORD_FACES
,
default
=
"syne"
,
help
=
"face of the PARVAGUES wordmark; jersey is the "
"DevCon26 stage face"
)
ap
.
add_argument
(
"--no-tray"
,
action
=
"store_true"
,
ap
.
add_argument
(
"--no-tray"
,
action
=
"store_true"
,
help
=
"skip the system-tray presence even where one exists"
)
help
=
"skip the system-tray presence even where one exists"
)
ap
.
add_argument
(
"--spectro"
,
action
=
"store_true"
,
ap
.
add_argument
(
"--spectro"
,
action
=
"store_true"
,
...
@@ -3603,11 +3829,14 @@ def main(argv=None) -> int:
...
@@ -3603,11 +3829,14 @@ def main(argv=None) -> int:
ap
.
add_argument
(
"--spectro-target"
,
ap
.
add_argument
(
"--spectro-target"
,
help
=
"capture this node instead of the default sink"
)
help
=
"capture this node instead of the default sink"
)
ap
.
add_argument
(
"--no-hl"
,
action
=
"store_true"
,
ap
.
add_argument
(
"--no-hl"
,
action
=
"store_true"
,
help
=
"do not subscribe to the Tidal
HL
feed (SuperDirt "
help
=
"do not subscribe to the Tidal
orbit
feed (SuperDirt "
"mirror on port
%
d; 'h' toggles it at runtime)"
"mirror on port
%
d; 'h' toggles it at runtime)"
%
oscfeed
.
VIZ_PORT
)
%
oscfeed
.
VIZ_PORT
)
ap
.
add_argument
(
"--selftest"
,
action
=
"store_true"
,
ap
.
add_argument
(
"--selftest"
,
action
=
"store_true"
,
help
=
"render synthetic events for --seconds, then exit"
)
help
=
"render synthetic events for --seconds, then exit"
)
ap
.
add_argument
(
"--screenshot"
,
metavar
=
"DIR"
,
help
=
"with --selftest: save rendered frames (one per "
"theme, plus a large hero shot) as PNGs into DIR"
)
ap
.
add_argument
(
"--seconds"
,
type
=
float
,
default
=
3.0
)
ap
.
add_argument
(
"--seconds"
,
type
=
float
,
default
=
3.0
)
ap
.
add_argument
(
"--show"
,
action
=
"store_true"
,
ap
.
add_argument
(
"--show"
,
action
=
"store_true"
,
help
=
"run the selftest on the real display, not offscreen"
)
help
=
"run the selftest on the real display, not offscreen"
)
...
@@ -3619,7 +3848,8 @@ def main(argv=None) -> int:
...
@@ -3619,7 +3848,8 @@ def main(argv=None) -> int:
print
(
"->
%
s"
%
(
resolve_watch_port
()[
1
],))
print
(
"->
%
s"
%
(
resolve_watch_port
()[
1
],))
return
0
return
0
if
a
.
selftest
:
if
a
.
selftest
:
return
selftest
(
a
.
seconds
,
a
.
show
,
a
.
theme
or
DEFAULT_THEME
)
return
selftest
(
a
.
seconds
,
a
.
show
,
a
.
theme
or
DEFAULT_THEME
,
shot_dir
=
a
.
screenshot
)
# PLN, 2026-09-24: "do ensure never more than one instance, midiviz
# PLN, 2026-09-24: "do ensure never more than one instance, midiviz
# should check unique else restart not duplicate". flock on a runtime
# should check unique else restart not duplicate". flock on a runtime
...
@@ -3628,14 +3858,14 @@ def main(argv=None) -> int:
...
@@ -3628,14 +3858,14 @@ def main(argv=None) -> int:
# because Restart=always plus exit 0 would be a restart storm against
# because Restart=always plus exit 0 would be a restart storm against
# the live instance.
# the live instance.
rt
=
os
.
environ
.
get
(
"XDG_RUNTIME_DIR"
)
or
"/run/user/
%
d"
%
os
.
getuid
()
rt
=
os
.
environ
.
get
(
"XDG_RUNTIME_DIR"
)
or
"/run/user/
%
d"
%
os
.
getuid
()
lock_path
=
Path
(
rt
)
/
"parvagues"
/
"midiviz.instance"
lock_path
=
Path
(
rt
)
/
CONFIG_DIR
/
"midiviz.instance"
try
:
try
:
lock_path
.
parent
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
lock_path
.
parent
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
fh
=
open
(
lock_path
,
"w"
)
fh
=
open
(
lock_path
,
"w"
)
try
:
try
:
fcntl
.
flock
(
fh
,
fcntl
.
LOCK_EX
|
fcntl
.
LOCK_NB
)
fcntl
.
flock
(
fh
,
fcntl
.
LOCK_EX
|
fcntl
.
LOCK_NB
)
except
OSError
:
except
OSError
:
print
(
"midiviz: already running
—
refusing to duplicate "
print
(
"midiviz: already running
,
refusing to duplicate "
"(focus the existing window)"
,
file
=
sys
.
stderr
)
"(focus the existing window)"
,
file
=
sys
.
stderr
)
try
:
try
:
import
launchers
import
launchers
...
@@ -3665,7 +3895,7 @@ def main(argv=None) -> int:
...
@@ -3665,7 +3895,7 @@ def main(argv=None) -> int:
# The theme joins it for the same reason: a `--theme` that lost to a saved
# The theme joins it for the same reason: a `--theme` that lost to a saved
# one, or a saved one that lost to a corrupt file, is otherwise a silent
# one, or a saved one that lost to a corrupt file, is otherwise a silent
# surprise, and the window has no words to explain itself with.
# surprise, and the window has no words to explain itself with.
print
(
"⚓ midiviz
—
%
s · theme
%
s ·
%.2
f× · q/Esc/Ctrl-C to quit · right-click for the menu"
print
(
"⚓ midiviz
·
%
s · theme
%
s ·
%.2
f× · q/Esc/Ctrl-C to quit · right-click for the menu"
%
(
label
or
"port
%
s"
%
port
,
theme
,
scale
),
file
=
sys
.
stderr
)
%
(
label
or
"port
%
s"
%
port
,
theme
,
scale
),
file
=
sys
.
stderr
)
# A blind `aseqdump` (no -p) subscribes to NOTHING -- see the
# A blind `aseqdump` (no -p) subscribes to NOTHING -- see the
# WATCH_PREFERENCE comment above -- so start a Reader only once a real
# WATCH_PREFERENCE comment above -- so start a Reader only once a real
...
@@ -3692,7 +3922,8 @@ def main(argv=None) -> int:
...
@@ -3692,7 +3922,8 @@ def main(argv=None) -> int:
app
.
setQuitOnLastWindowClosed
(
False
)
app
.
setQuitOnLastWindowClosed
(
False
)
w
=
build_widget
(
port
or
"--"
,
reader
,
scale
=
scale
,
w
=
build_widget
(
port
or
"--"
,
reader
,
scale
=
scale
,
pinned_port
=
pinned
,
watch
=
True
,
theme
=
theme
,
pinned_port
=
pinned
,
watch
=
True
,
theme
=
theme
,
persist
=
True
,
tray
=
not
a
.
no_tray
,
hl
=
hl
)
persist
=
True
,
tray
=
not
a
.
no_tray
,
hl
=
hl
,
word_face
=
a
.
word_face
)
if
a
.
spectro
:
if
a
.
spectro
:
src
=
None
src
=
None
if
a
.
spectro_target
:
if
a
.
spectro_target
:
...
...
tools/bridge/ui/Jersey15-Regular.ttf
0 → 100644
View file @
794b6313
File added
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment