-- v2.60 local zero_point = 0 local reset_counter = 0 local current_reset_number = nil local current_reset_time = nil local last_mouse_state = 0 local time_input = "00:00:02" local text_field_active = false local text_field_x = 10 local text_field_y = 190 local text_field_w = 310 local text_field_h = 30 local blink_timer = 0 local blink_state = false local BLINK_SPEED = 30 local error_message = "" local lastPressedButton = nil local BUTTON_PRESS_DURATION = 0.08 local recordingShortcut = false local ext_section = "TimeAtCursorAndDynamicTimer" local closeKey = reaper.GetExtState(ext_section, "closeKey") or "" function format_time(seconds) local abs_seconds = math.abs(seconds) local hours = math.floor(abs_seconds / 3600) local minutes = math.floor((abs_seconds % 3600) / 60) local secs = math.floor(abs_seconds % 60) local millis = math.floor((abs_seconds % 1) * 1000) local sign = (seconds < 0) and "-" or "" return string.format("%s%02d:%02d:%02d.%03d", sign, hours, minutes, secs, millis) end function draw_button(x, y, w, h, label, hover) if hover then gfx.set(0.6, 0.6, 0.6, 1) else gfx.set(0.4, 0.4, 0.4, 1) end gfx.roundrect(x, y, w, h, 4, true) gfx.set(1, 1, 1, 1) gfx.roundrect(x, y, w, h, 4, false) local fontSize = 18 if lastPressedButton and math.abs(x - lastPressedButton.x) < 0.001 and math.abs(y - lastPressedButton.y) < 0.001 and math.abs(w - lastPressedButton.w) < 0.001 and math.abs(h - lastPressedButton.h) < 0.001 then if reaper.time_precise() - lastPressedButton.pressTime < BUTTON_PRESS_DURATION then fontSize = fontSize - 2 end end gfx.setfont(1, "Arial", fontSize) local tw, th = gfx.measurestr(label) gfx.x = x + (w - tw) / 2 gfx.y = y + (h - th) / 2 gfx.drawstr(label) gfx.setfont(1, "Arial", 18) end function is_mouse_over(x, y, w, h) return (gfx.mouse_x >= x and gfx.mouse_x <= (x + w) and gfx.mouse_y >= y and gfx.mouse_y <= (y + h)) end function draw_text_field(x, y, w, h, text, active) if active then gfx.set(0.6, 0.6, 0.6, 1) else gfx.set(0.3, 0.3, 0.3, 1) end gfx.roundrect(x, y, w, h, 4, true) gfx.set(1, 1, 1, 1) gfx.roundrect(x, y, w, h, 4, false) gfx.set(1, 1, 1, 1) local tw, th = gfx.measurestr(text) gfx.x = x + (w - tw) / 2 gfx.y = y + (h - th) / 2 gfx.drawstr(text) if active and blink_state then local cx = x + (w - tw) / 2 + tw local cy = y + (h - th) / 2 gfx.line(cx, cy, cx, cy + th) end end function parse_time_input(input) if input == "" then return 0 end local parts = {} for p in string.gmatch(input, "[^:]+") do table.insert(parts, p) end local h, m, s = 0, 0, 0 if #parts == 1 then s = tonumber(parts[1]) or 0 elseif #parts == 2 then m = tonumber(parts[1]) or 0 s = tonumber(parts[2]) or 0 elseif #parts == 3 then h = tonumber(parts[1]) or 0 m = tonumber(parts[2]) or 0 s = tonumber(parts[3]) or 0 else return 0 end return (h * 3600) + (m * 60) + s end function keyCodeToStr(k) k = tonumber(k) if k and k >= 32 and k <= 126 then return string.char(k) else return "Key:" .. k end end function create_reset_marker() local pos = reaper.GetCursorPosition() local mc = reaper.CountProjectMarkers(0) local max_num = 0 local found = false for i = 0, mc - 1 do local _, isr, pos_i, _, name, _ = reaper.EnumProjectMarkers(i) if (not isr) and string.match(name, "^Reset %d+$") then local num = tonumber(string.match(name, "%d+$")) if num and num > max_num then max_num = num end if math.abs(pos_i - pos) < 0.001 then found = true current_reset_number = num current_reset_time = pos_i zero_point = pos_i break end end end if not found then local new_num = max_num + 1 local marker_name = string.format("Reset %03d", new_num) reaper.AddProjectMarker2(0, false, pos, 0, marker_name, -1, 0) reset_counter = new_num current_reset_number = new_num current_reset_time = pos zero_point = pos end save_state() end function remove_reset_markers() local mc = reaper.CountProjectMarkers(0) for i = mc - 1, 0, -1 do local _, isr, _, _, name, idx = reaper.EnumProjectMarkers(i) if string.match(name, "^Reset %d+$") then reaper.DeleteProjectMarker(0, idx, isr) end end reset_counter = 0 current_reset_number = nil current_reset_time = nil zero_point = 0 end function move_play_to_reset() if current_reset_time then reaper.SetEditCurPos(current_reset_time, true, false) reaper.OnPlayButton() else error_message = "No Reset set. Click 'Reset' first!" end end function move_cursor_forward_from_reset_by(offset) if current_reset_time then local new_pos = current_reset_time + offset reaper.SetEditCurPos(new_pos, true, false) else error_message = "No Reset set. Click 'Reset' first!" end end function move_play_from_offset(offset) if current_reset_time then local new_pos = current_reset_time + offset reaper.SetEditCurPos(new_pos, true, false) reaper.OnPlayButton() else error_message = "No Reset set. Click 'Reset' first!" end end function update_current_reset_from_markers() local mc = reaper.CountProjectMarkers(0) if current_reset_number then for i = 0, mc - 1 do local retval, isr, pos, _, name, _ = reaper.EnumProjectMarkers(i) if (not isr) then local num = name:match("Reset%s*(%d+)") if num and tonumber(num) == current_reset_number then current_reset_time = pos zero_point = pos return end end end end local max_num = 0 local last_pos = nil for i = 0, mc - 1 do local retval, isr, pos, _, name, _ = reaper.EnumProjectMarkers(i) if (not isr) then local num = name:match("Reset%s*(%d+)") if num then num = tonumber(num) if num > max_num then max_num = num last_pos = pos end end end end if max_num > 0 then current_reset_number = max_num current_reset_time = last_pos zero_point = last_pos end end function jump_to_next_reset() local mc = reaper.CountProjectMarkers(0) local resets = {} for i = 0, mc - 1 do local _, isr, pos, _, name, _ = reaper.EnumProjectMarkers(i) if (not isr) then local num = name:match("Reset%s*(%d+)") if num then table.insert(resets, {time = pos, number = tonumber(num)}) end end end table.sort(resets, function(a, b) return a.time < b.time end) if #resets == 0 then error_message = "No reset markers found." return end local next_reset = nil for i, r in ipairs(resets) do if r.time > current_reset_time then next_reset = r break end end if not next_reset then next_reset = resets[1] end current_reset_time = next_reset.time current_reset_number = next_reset.number zero_point = next_reset.time reaper.SetEditCurPos(current_reset_time, true, false) save_state() end function jump_to_prev_reset() local mc = reaper.CountProjectMarkers(0) local resets = {} for i = 0, mc - 1 do local _, isr, pos, _, name, _ = reaper.EnumProjectMarkers(i) if (not isr) then local num = name:match("Reset%s*(%d+)") if num then table.insert(resets, {time = pos, number = tonumber(num)}) end end end table.sort(resets, function(a, b) return a.time < b.time end) if #resets == 0 then error_message = "No reset markers found." return end local prev_reset = nil for i = #resets, 1, -1 do if resets[i].time < current_reset_time then prev_reset = resets[i] break end end if not prev_reset then prev_reset = resets[#resets] end current_reset_time = prev_reset.time current_reset_number = prev_reset.number zero_point = prev_reset.time reaper.SetEditCurPos(current_reset_time, true, false) save_state() end function draw_gui() local cursor_pos = reaper.GetCursorPosition() local cursor_relative = cursor_pos - zero_point local play_state = reaper.GetPlayState() local displayed_play_position if play_state == 0 then displayed_play_position = cursor_pos else displayed_play_position = reaper.GetPlayPositionEx(0) end local play_relative = displayed_play_position - zero_point gfx.set(1, 1, 1, 1) gfx.x, gfx.y = 10, 10 gfx.printf("Time at Cursor: %s", format_time(cursor_relative)) gfx.x, gfx.y = 10, 40 gfx.printf("Timer (Play Pos): %s", format_time(play_relative)) local button_width = 150 local button_height = 30 local gap = 10 local reset_x, reset_y = 10, 70 local remove_x, remove_y = reset_x + button_width + gap, 70 local reset_hovered = is_mouse_over(reset_x, reset_y, button_width, button_height) local remove_hovered = is_mouse_over(remove_x, remove_y, button_width, button_height) draw_button(reset_x, reset_y, button_width, button_height, "Reset", reset_hovered) draw_button(remove_x, remove_y, button_width, button_height, "Remove Markers", remove_hovered) local cursor_to_x, cursor_to_y = 10, 110 local play_from_reset_x, play_from_reset_y = remove_x, 110 local cursor_to_hovered = is_mouse_over(cursor_to_x, cursor_to_y, button_width, button_height) local play_from_reset_hovered = is_mouse_over(play_from_reset_x, play_from_reset_y, button_width, button_height) draw_button(cursor_to_x, cursor_to_y, button_width, button_height, "Cursor to Reset", cursor_to_hovered) draw_button(play_from_reset_x, play_from_reset_y, button_width, button_height, "Play from Reset", play_from_reset_hovered) gfx.set(0.2, 0.2, 0.2, 1) gfx.rect(text_field_x, 150, text_field_w, 70, true) local cursor_to_offset_x, cursor_to_offset_y = 10, 150 local play_from_offset_x, play_from_offset_y = remove_x, 150 local cursor_to_offset_hovered = is_mouse_over(cursor_to_offset_x, cursor_to_offset_y, button_width, button_height) local play_from_offset_hovered = is_mouse_over(play_from_offset_x, play_from_offset_y, button_width, button_height) draw_button(cursor_to_offset_x, cursor_to_offset_y, button_width, button_height, "Cursor to:", cursor_to_offset_hovered) draw_button(play_from_offset_x, play_from_offset_y, button_width, button_height, "Play from:", play_from_offset_hovered) draw_text_field(text_field_x, text_field_y, text_field_w, text_field_h, time_input, text_field_active) local prev_reset_x, prev_reset_y = 10, 230 local next_reset_x, next_reset_y = remove_x, 230 local prev_reset_hovered = is_mouse_over(prev_reset_x, prev_reset_y, button_width, button_height) local next_reset_hovered = is_mouse_over(next_reset_x, next_reset_y, button_width, button_height) draw_button(prev_reset_x, prev_reset_y, button_width, button_height, "I<", prev_reset_hovered) draw_button(next_reset_x, next_reset_y, button_width, button_height, ">I", next_reset_hovered) local stop_x, stop_y = 10, 270 local stop_hovered = is_mouse_over(stop_x, stop_y, text_field_w, button_height) draw_button(stop_x, stop_y, text_field_w, button_height, "Stop", stop_hovered) local short_x, short_y, short_w, short_h = 10, stop_y + 40, 100, 30 local short_hovered = is_mouse_over(short_x, short_y, short_w, short_h) local shortLabel = "SHORT" if recordingShortcut then shortLabel = "Press key" elseif closeKey ~= "" then shortLabel = "SHORT: " .. keyCodeToStr(closeKey) end draw_button(short_x, short_y, short_w, short_h, shortLabel, short_hovered) local mouse_state = gfx.mouse_cap & 1 if mouse_state == 1 and last_mouse_state == 0 then error_message = "" if reset_hovered then create_reset_marker() lastPressedButton = {x = reset_x, y = reset_y, w = button_width, h = button_height, pressTime = reaper.time_precise()} elseif remove_hovered then remove_reset_markers() lastPressedButton = {x = remove_x, y = remove_y, w = button_width, h = button_height, pressTime = reaper.time_precise()} elseif cursor_to_hovered then if current_reset_time then reaper.SetEditCurPos(current_reset_time, true, false) else error_message = "No Reset set. Click 'Reset' first!" end lastPressedButton = {x = cursor_to_x, y = cursor_to_y, w = button_width, h = button_height, pressTime = reaper.time_precise()} elseif play_from_reset_hovered then move_play_to_reset() lastPressedButton = {x = play_from_reset_x, y = play_from_reset_y, w = button_width, h = button_height, pressTime = reaper.time_precise()} elseif cursor_to_offset_hovered then local offset = parse_time_input(time_input) if offset == 0 and time_input ~= "" then error_message = "Invalid time format." else move_cursor_forward_from_reset_by(offset) end lastPressedButton = {x = cursor_to_offset_x, y = cursor_to_offset_y, w = button_width, h = button_height, pressTime = reaper.time_precise()} elseif play_from_offset_hovered then local offset = parse_time_input(time_input) if offset == 0 and time_input ~= "" then error_message = "Invalid time format." else move_play_from_offset(offset) end lastPressedButton = {x = play_from_offset_x, y = play_from_offset_y, w = button_width, h = button_height, pressTime = reaper.time_precise()} elseif prev_reset_hovered then jump_to_prev_reset() lastPressedButton = {x = prev_reset_x, y = prev_reset_y, w = button_width, h = button_height, pressTime = reaper.time_precise()} elseif next_reset_hovered then jump_to_next_reset() lastPressedButton = {x = next_reset_x, y = next_reset_y, w = button_width, h = button_height, pressTime = reaper.time_precise()} elseif stop_hovered then reaper.Main_OnCommand(40044, 0) lastPressedButton = {x = stop_x, y = stop_y, w = text_field_w, h = button_height, pressTime = reaper.time_precise()} elseif short_hovered then recordingShortcut = true end if is_mouse_over(text_field_x, text_field_y, text_field_w, text_field_h) then text_field_active = true else if not (reset_hovered or remove_hovered or cursor_to_hovered or play_from_reset_hovered or cursor_to_offset_hovered or play_from_offset_hovered or prev_reset_hovered or next_reset_hovered or stop_hovered or short_hovered) then text_field_active = false end end end last_mouse_state = mouse_state gfx.x, gfx.y = 10, short_y + 40 if current_reset_number then gfx.printf("Current 0 position (name): Reset %03d", current_reset_number) gfx.x, gfx.y = 10, short_y + 70 gfx.printf("Current 0 position: %s", format_time(current_reset_time)) end if error_message ~= "" then gfx.set(1, 0.4, 0.4, 1) gfx.x, gfx.y = 10, short_y + 100 gfx.printf("Error: %s", error_message) end end function handle_text_input(char) if recordingShortcut then if char > 0 and char ~= 27 and char ~= -1 then closeKey = tostring(char) reaper.SetExtState(ext_section, "closeKey", closeKey, true) recordingShortcut = false return 0 end end if closeKey ~= "" and tonumber(closeKey) and char == tonumber(closeKey) then gfx.quit() return -1 end if text_field_active then if char >= 32 then local c = string.char(char) if c:match("^[0-9:-]$") then time_input = time_input .. c else error_message = "Invalid character: '" .. c .. "' (only digits, ':' and '-' allowed)" end elseif char == 8 then if #time_input > 0 then time_input = time_input:sub(1, -2) end elseif char == 13 then text_field_active = false reaper.SetProjExtState(0, ext_section, "time_input", time_input, true) end end return 0 end function save_state() reaper.SetProjExtState(0, ext_section, "zero_point", tostring(zero_point)) reaper.SetProjExtState(0, ext_section, "reset_counter", tostring(reset_counter)) reaper.SetProjExtState(0, ext_section, "current_reset_number", current_reset_number and tostring(current_reset_number) or "") reaper.SetProjExtState(0, ext_section, "current_reset_time", current_reset_time and tostring(current_reset_time) or "") reaper.SetProjExtState(0, ext_section, "time_input", time_input) end function load_state() local retval, zp = reaper.GetProjExtState(0, ext_section, "zero_point") if zp ~= "" then zero_point = tonumber(zp) or 0 end local retval, rc = reaper.GetProjExtState(0, ext_section, "reset_counter") if rc ~= "" then reset_counter = tonumber(rc) or 0 end local retval, crn = reaper.GetProjExtState(0, ext_section, "current_reset_number") if crn ~= "" then current_reset_number = tonumber(crn) end local retval, crt = reaper.GetProjExtState(0, ext_section, "current_reset_time") if crt ~= "" then current_reset_time = tonumber(crt) end local retval, ti = reaper.GetProjExtState(0, ext_section, "time_input") if ti ~= "" then time_input = ti else time_input = "00:00:02" end update_current_reset_from_markers() end function main() local char = gfx.getchar() if char == 27 or char == -1 then return end blink_timer = blink_timer + 1 if blink_timer >= BLINK_SPEED then blink_timer = 0; blink_state = not blink_state end gfx.clear = reaper.ColorToNative(0, 0, 0) gfx.setfont(1, "Arial", 18) draw_gui() local ret = handle_text_input(char) if ret == -1 then return end gfx.update() reaper.defer(main) end load_state() reaper.atexit(save_state) gfx.init("Time at Cursor and Timer", 330, 480, 0, 20, 150) main()