diff --git a/.idea/runConfigurations/AppLoader.xml b/.idea/runConfigurations/AppLoader.xml
index 6389b7f..76ebb6e 100644
--- a/.idea/runConfigurations/AppLoader.xml
+++ b/.idea/runConfigurations/AppLoader.xml
@@ -3,7 +3,7 @@
-
+
diff --git a/.idea/runConfigurations/TsvmEmulator.xml b/.idea/runConfigurations/TsvmEmulator.xml
index 629868f..18139ae 100644
--- a/.idea/runConfigurations/TsvmEmulator.xml
+++ b/.idea/runConfigurations/TsvmEmulator.xml
@@ -2,7 +2,7 @@
-
+
diff --git a/assets/disk0/tvdos/bin/taut.js b/assets/disk0/tvdos/bin/taut.js
index 2eec239..1f20679 100644
--- a/assets/disk0/tvdos/bin/taut.js
+++ b/assets/disk0/tvdos/bin/taut.js
@@ -552,6 +552,10 @@ const VOCSIZE_ORDERS = Math.floor((SCRW - 8) / 4)
const VIEW_TIMELINE = 0
const VIEW_CUES = 1
const VIEW_PATTERN_DETAILS = 2
+const VIEW_SAMPLES = 3
+const VIEW_INSTRMNT = 4
+const VIEW_PROJECT = 5
+const VIEW_FILE = 6
const colPlayback = 86
const colHighlight = 41
@@ -840,7 +844,8 @@ function drawControlHint() {
// ['q','Quit'],
]
- let hintElems = [hintElemTimeline, hintElemOrders, hintElemPatterns]
+ const hintElemExternal = [['Tab','Panel']]
+ let hintElems = [hintElemTimeline, hintElemOrders, hintElemPatterns, hintElemExternal, hintElemExternal, hintElemExternal, hintElemExternal]
// erase current line
con.move(SCRH, 1)
@@ -1156,6 +1161,22 @@ function resetAudioDevice() {
audio.stop(PLAYHEAD)
}
+function applyMuteTransition(toPanel) {
+ if (toPanel === VIEW_PATTERN_DETAILS) {
+ timelineMuteSnapshot = voiceMutes.slice()
+ if (voiceMutes[0]) {
+ voiceMutes[0] = false
+ audio.setVoiceMute(PLAYHEAD, 0, false)
+ }
+ } else if (toPanel === VIEW_TIMELINE && timelineMuteSnapshot !== null) {
+ for (let i = 0; i < song.numVoices; i++) {
+ voiceMutes[i] = timelineMuteSnapshot[i]
+ audio.setVoiceMute(PLAYHEAD, i, voiceMutes[i])
+ }
+ timelineMuteSnapshot = null
+ }
+}
+
function redrawFull() { drawAll() }
function redrawPanel() {
@@ -1273,7 +1294,7 @@ function timelineInput(wo, event) {
const oldVoiceOff = voiceOff
const prevVox = cursorVox
let triedCross = false
- if (shiftDown) {
+ if (shiftDown || timelineRowStyle > 0) {
cursorVox += dir * moveDelta
timelineColCursor = dir > 0 ? 0 : 5
} else {
@@ -1750,8 +1771,33 @@ function patternsInput(wo, event) {
const panelTimeline = new win.WindowObject(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, timelineInput, drawTimelineContents, undefined, ()=>{})
const panelOrders = new win.WindowObject(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, ordersInput, drawOrdersContents, undefined, ()=>{})
-const panelPatterns = new win.WindowObject(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, patternsInput, drawPatternsContents, undefined, ()=>{})
-const panels = [panelTimeline, panelOrders, panelPatterns]
+const panelPatterns = new win.WindowObject(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, patternsInput, drawPatternsContents, undefined, ()=>{})
+
+// External sub-program panels: drawContents launches the sub-program synchronously.
+// The sub-program draws rows 4+ and does NOT touch rows 1-3 (drawn by taut.js before launch).
+// On exit, the sub-program sets _G.taut_nextPanel to request a tab switch.
+function makeExternalPanelDraw(progName) {
+ return function(wo) {
+ _G.taut_nextPanel = undefined
+ _G.shell.execute(`${progName} ${fullPathObj.full} ${currentPanel}`)
+ }
+}
+
+function drawProjectContents(wo) {
+ fillLine(PTNVIEW_OFFSET_Y - 1, colVoiceHdr, 255)
+ for (let y = PTNVIEW_OFFSET_Y; y < SCRH; y++) fillLine(y, colBackPtn, 255)
+ con.move(PTNVIEW_OFFSET_Y + 2, 3)
+ con.color_pair(colStatus, 255)
+ print('[Project settings — not yet implemented]')
+}
+function externalPanelInput(wo, event) {}
+
+const panelSamples = new win.WindowObject(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, externalPanelInput, makeExternalPanelDraw('taut_sampleedit'), undefined, ()=>{})
+const panelInstrmnt = new win.WindowObject(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, externalPanelInput, makeExternalPanelDraw('taut_instredit'), undefined, ()=>{})
+const panelProject = new win.WindowObject(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, externalPanelInput, drawProjectContents, undefined, ()=>{})
+const panelFile = new win.WindowObject(1, PTNVIEW_OFFSET_Y, SCRW, PTNVIEW_HEIGHT, externalPanelInput, makeExternalPanelDraw('taut_fileop'), undefined, ()=>{})
+
+const panels = [panelTimeline, panelOrders, panelPatterns, panelSamples, panelInstrmnt, panelProject, panelFile]
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
// PLAYBACK STATE
@@ -1925,6 +1971,11 @@ function updatePlayback() {
clampCursor()
if (currentPanel === VIEW_TIMELINE) redrawPanel()
else if (currentPanel === VIEW_PATTERN_DETAILS && song.numPats > 0) { simStateKey = ''; redrawPanel() }
+ else if (currentPanel === VIEW_CUES) {
+ if (cueIdx < ordersScroll) ordersScroll = cueIdx
+ if (cueIdx >= ordersScroll + PTNVIEW_HEIGHT) ordersScroll = Math.max(0, cueIdx - PTNVIEW_HEIGHT + 1)
+ drawOrdersContents()
+ }
} else if (previewActive || nowCue === cueIdx) {
const oldCursor = cursorRow
const oldScroll = scrollRow
@@ -2152,7 +2203,17 @@ taud.uploadTaudFile(fullPathObj.full, 0, PLAYHEAD)
audio.setMasterVolume(PLAYHEAD, 255)
audio.setMasterPan(PLAYHEAD, 128)
+function isExternalPanel(p) {
+ return p === VIEW_SAMPLES || p === VIEW_INSTRMNT || p === VIEW_FILE
+}
+
+// Launching a sub-program from inside an input.withEvent callback causes the triggering
+// Tab event to leak into the sub-program's own withEvent call (the event hasn't been
+// consumed yet when the callback is still executing). We avoid this by deferring the
+// actual shell.execute until after withEvent returns.
let exitFlag = false
+let pendingExternalDraw = false
+
while (!exitFlag) {
input.withEvent(event => {
if (event[0] !== "key_down") return
@@ -2166,28 +2227,18 @@ while (!exitFlag) {
}
if (keyJustHit && keysym === "") {
- const prevPanel = currentPanel
currentPanel = (currentPanel + (shiftDown ? -1 : 1))
if (currentPanel < 0) currentPanel += panels.length
currentPanel = currentPanel % panels.length
-
- if (currentPanel === VIEW_PATTERN_DETAILS) {
- // Entering Patterns: save mute state and ensure voice 0 (preview voice) is audible
- timelineMuteSnapshot = voiceMutes.slice()
- if (voiceMutes[0]) {
- voiceMutes[0] = false
- audio.setVoiceMute(PLAYHEAD, 0, false)
- }
- } else if (currentPanel === VIEW_TIMELINE && timelineMuteSnapshot !== null) {
- // Returning to Timeline: restore saved mute state
- for (let i = 0; i < song.numVoices; i++) {
- voiceMutes[i] = timelineMuteSnapshot[i]
- audio.setVoiceMute(PLAYHEAD, i, voiceMutes[i])
- }
- timelineMuteSnapshot = null
+ applyMuteTransition(currentPanel)
+ if (isExternalPanel(currentPanel)) {
+ // Redraw header now so the tab highlight is visible immediately,
+ // but defer the actual sub-program launch to after withEvent returns.
+ con.clear(); drawAlwaysOnElems(); drawControlHint()
+ pendingExternalDraw = true
+ } else {
+ drawAll()
}
-
- drawAll()
return
}
@@ -2199,6 +2250,24 @@ while (!exitFlag) {
panels[currentPanel].processInput(event)
})
+ // Launch external sub-program OUTSIDE the withEvent callback so the triggering
+ // Tab event is fully consumed before the sub-program's event loop begins.
+ if (pendingExternalDraw) {
+ pendingExternalDraw = false
+ redrawPanel()
+ while (_G.taut_nextPanel !== undefined && _G.taut_nextPanel !== null) {
+ currentPanel = _G.taut_nextPanel
+ _G.taut_nextPanel = undefined
+ applyMuteTransition(currentPanel)
+ if (isExternalPanel(currentPanel)) {
+ con.clear(); drawAlwaysOnElems(); drawControlHint()
+ redrawPanel()
+ } else {
+ drawAll()
+ }
+ }
+ }
+
if (playbackMode !== PLAYMODE_NONE) updatePlayback()
}
diff --git a/assets/disk0/tvdos/bin/taut_fileop.js b/assets/disk0/tvdos/bin/taut_fileop.js
index 92e040d..023e671 100644
--- a/assets/disk0/tvdos/bin/taut_fileop.js
+++ b/assets/disk0/tvdos/bin/taut_fileop.js
@@ -1 +1,77 @@
-// filesystem navigator for Taut
\ No newline at end of file
+/**
+ * TAUT File Operations
+ * Sub-program launched by taut.js when the File tab is active.
+ * Rows 1-3 are owned by the parent; this program draws rows 4+.
+ *
+ * exec_args[1] = path to .taud file
+ * Sets _G.taut_nextPanel before returning to request a panel switch.
+ *
+ * Created by minjaesong on 2026-04-27
+ */
+
+const win = require("wintex")
+
+const PANEL_COUNT = 7
+const MY_PANEL = 6 // VIEW_FILE
+
+const [SCRH, SCRW] = con.getmaxyx()
+const PANEL_Y = 4
+const PANEL_H = SCRH - PANEL_Y
+
+const colStatus = 253
+const colContent = 240
+const colHdr = 239
+
+function drawFileOpContents(wo) {
+ for (let y = PANEL_Y; y < SCRH; y++) {
+ con.move(y, 1)
+ con.color_pair(colContent, 255)
+ print(' '.repeat(SCRW))
+ }
+ con.move(PANEL_Y + 1, 3)
+ con.color_pair(colHdr, 255)
+ print('[ File ]')
+ con.move(PANEL_Y + 3, 3)
+ con.color_pair(colStatus, 255)
+ print('placeholder — not yet implemented')
+}
+
+function drawHints() {
+ con.move(SCRH, 1)
+ con.color_pair(colStatus, 255)
+ print(' '.repeat(SCRW - 1))
+ con.move(SCRH, 1)
+ con.color_pair(colHdr, 255); print('Tab ')
+ con.color_pair(colStatus, 255); print('Panel')
+}
+
+function fileOpInput(wo, event) {
+ // placeholder — no interaction yet
+}
+
+const panel = new win.WindowObject(1, PANEL_Y, SCRW, PANEL_H, fileOpInput, drawFileOpContents, undefined, ()=>{})
+
+panel.drawContents()
+drawHints()
+
+let done = false
+while (!done) {
+ input.withEvent(event => {
+ if (event[0] !== 'key_down') return
+ const keysym = event[1]
+ const keyJustHit = (1 == event[2])
+ const shiftDown = (event.includes(59) || event.includes(60))
+
+ if (!keyJustHit) return
+
+ if (keysym === '') {
+ _G.taut_nextPanel = (MY_PANEL + (shiftDown ? -1 : 1) + PANEL_COUNT) % PANEL_COUNT
+ done = true
+ return
+ }
+
+ panel.processInput(event)
+ })
+}
+
+return 0
diff --git a/assets/disk0/tvdos/bin/taut_instredit.js b/assets/disk0/tvdos/bin/taut_instredit.js
index e69de29..2f4b77c 100644
--- a/assets/disk0/tvdos/bin/taut_instredit.js
+++ b/assets/disk0/tvdos/bin/taut_instredit.js
@@ -0,0 +1,77 @@
+/**
+ * TAUT Instrument Editor
+ * Sub-program launched by taut.js when the Instrmnt tab is active.
+ * Rows 1-3 are owned by the parent; this program draws rows 4+.
+ *
+ * exec_args[1] = path to .taud file
+ * Sets _G.taut_nextPanel before returning to request a panel switch.
+ *
+ * Created by minjaesong on 2026-04-27
+ */
+
+const win = require("wintex")
+
+const PANEL_COUNT = 7
+const MY_PANEL = 4 // VIEW_INSTRMNT
+
+const [SCRH, SCRW] = con.getmaxyx()
+const PANEL_Y = 4
+const PANEL_H = SCRH - PANEL_Y
+
+const colStatus = 253
+const colContent = 240
+const colHdr = 239
+
+function drawInstEditContents(wo) {
+ for (let y = PANEL_Y; y < SCRH; y++) {
+ con.move(y, 1)
+ con.color_pair(colContent, 255)
+ print(' '.repeat(SCRW))
+ }
+ con.move(PANEL_Y + 1, 3)
+ con.color_pair(colHdr, 255)
+ print('[ Instrument Editor ]')
+ con.move(PANEL_Y + 3, 3)
+ con.color_pair(colStatus, 255)
+ print('placeholder — not yet implemented')
+}
+
+function drawHints() {
+ con.move(SCRH, 1)
+ con.color_pair(colStatus, 255)
+ print(' '.repeat(SCRW - 1))
+ con.move(SCRH, 1)
+ con.color_pair(colHdr, 255); print('Tab ')
+ con.color_pair(colStatus, 255); print('Panel')
+}
+
+function instEditInput(wo, event) {
+ // placeholder — no interaction yet
+}
+
+const panel = new win.WindowObject(1, PANEL_Y, SCRW, PANEL_H, instEditInput, drawInstEditContents, undefined, ()=>{})
+
+panel.drawContents()
+drawHints()
+
+let done = false
+while (!done) {
+ input.withEvent(event => {
+ if (event[0] !== 'key_down') return
+ const keysym = event[1]
+ const keyJustHit = (1 == event[2])
+ const shiftDown = (event.includes(59) || event.includes(60))
+
+ if (!keyJustHit) return
+
+ if (keysym === '') {
+ _G.taut_nextPanel = (MY_PANEL + (shiftDown ? -1 : 1) + PANEL_COUNT) % PANEL_COUNT
+ done = true
+ return
+ }
+
+ panel.processInput(event)
+ })
+}
+
+return 0
diff --git a/assets/disk0/tvdos/bin/taut_sampleedit.js b/assets/disk0/tvdos/bin/taut_sampleedit.js
index e69de29..b7326a5 100644
--- a/assets/disk0/tvdos/bin/taut_sampleedit.js
+++ b/assets/disk0/tvdos/bin/taut_sampleedit.js
@@ -0,0 +1,77 @@
+/**
+ * TAUT Sample Editor
+ * Sub-program launched by taut.js when the Samples tab is active.
+ * Rows 1-3 are owned by the parent; this program draws rows 4+.
+ *
+ * exec_args[1] = path to .taud file
+ * Sets _G.taut_nextPanel before returning to request a panel switch.
+ *
+ * Created by minjaesong on 2026-04-27
+ */
+
+const win = require("wintex")
+
+const PANEL_COUNT = 7
+const MY_PANEL = 3 // VIEW_SAMPLES
+
+const [SCRH, SCRW] = con.getmaxyx()
+const PANEL_Y = 4
+const PANEL_H = SCRH - PANEL_Y
+
+const colStatus = 253
+const colContent = 240
+const colHdr = 239
+
+function drawSampleEditContents(wo) {
+ for (let y = PANEL_Y; y < SCRH; y++) {
+ con.move(y, 1)
+ con.color_pair(colContent, 255)
+ print(' '.repeat(SCRW))
+ }
+ con.move(PANEL_Y + 1, 3)
+ con.color_pair(colHdr, 255)
+ print('[ Sample Editor ]')
+ con.move(PANEL_Y + 3, 3)
+ con.color_pair(colStatus, 255)
+ print('placeholder — not yet implemented')
+}
+
+function drawHints() {
+ con.move(SCRH, 1)
+ con.color_pair(colStatus, 255)
+ print(' '.repeat(SCRW - 1))
+ con.move(SCRH, 1)
+ con.color_pair(colHdr, 255); print('Tab ')
+ con.color_pair(colStatus, 255); print('Panel')
+}
+
+function sampleEditInput(wo, event) {
+ // placeholder — no interaction yet
+}
+
+const panel = new win.WindowObject(1, PANEL_Y, SCRW, PANEL_H, sampleEditInput, drawSampleEditContents, undefined, ()=>{})
+
+panel.drawContents()
+drawHints()
+
+let done = false
+while (!done) {
+ input.withEvent(event => {
+ if (event[0] !== 'key_down') return
+ const keysym = event[1]
+ const keyJustHit = (1 == event[2])
+ const shiftDown = (event.includes(59) || event.includes(60))
+
+ if (!keyJustHit) return
+
+ if (keysym === '') {
+ _G.taut_nextPanel = (MY_PANEL + (shiftDown ? -1 : 1) + PANEL_COUNT) % PANEL_COUNT
+ done = true
+ return
+ }
+
+ panel.processInput(event)
+ })
+}
+
+return 0