summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorruki <[email protected]>2025-10-27 22:54:33 +0800
committerruki <[email protected]>2025-10-27 22:54:33 +0800
commit1dc87c3ed7069e7d0ab21b107ef470e3c793ca9e (patch)
treef41648ac043d00e5e1a8d491bcb6f7e29968a038
parent954944d14d86d2ca4747887150a35b440864c03d (diff)
improve tty
-rw-r--r--tests/modules/tty/cursor_control.lua134
-rw-r--r--tests/modules/tty/live_dashboard.lua156
-rw-r--r--tests/modules/tty/quick_example.lua140
-rw-r--r--tests/modules/tty/test.lua219
-rw-r--r--xmake/core/base/tty.lua72
5 files changed, 721 insertions, 0 deletions
diff --git a/tests/modules/tty/cursor_control.lua b/tests/modules/tty/cursor_control.lua
new file mode 100644
index 000000000..2a28bed93
--- /dev/null
+++ b/tests/modules/tty/cursor_control.lua
@@ -0,0 +1,134 @@
+import("core.base.tty")
+import("core.base.scheduler")
+
+-- Demo TTY cursor position control features
+-- Implement partial screen refresh
+
+function _progress_bar_inline(label, progress)
+ local bar_width = 40
+ local filled = math.floor(progress * bar_width)
+ local bar = string.rep("█", filled) .. string.rep("░", bar_width - filled)
+
+ -- Clear the line and output content
+ tty.cr()
+ tty.erase_line()
+ io.write(string.format("%s: [%s] %3d%%", label, bar, math.floor(progress * 100)))
+ io.flush()
+end
+
+function main()
+ -- Check if ANSI control codes are supported
+ if not tty.has_vtansi() then
+ print("Terminal does not support ANSI control codes")
+ return
+ end
+
+ print("=== TTY Cursor Control Demo ===")
+ print("This demo shows partial screen refresh using cursor positioning")
+ print("\n")
+
+ -- Hide cursor for smoother display
+ tty.cursor_hide()
+
+ print("Demo 1: Multiple Progress Bars (Parallel Updates)")
+ print("------------------------------------------------")
+
+ -- Reserve lines for progress bars
+ print("Task 1: Downloading")
+ print("Task 2: Compiling ")
+ print("Task 3: Linking ")
+
+ -- Simulate three parallel task progress bars
+ for step = 1, 100 do
+ -- Task 1 progress
+ local progress1 = step / 100
+ tty.cursor_save()
+ tty.cursor_move_up(3)
+ _progress_bar_inline("Task 1: Downloading", progress1)
+ tty.cursor_restore()
+
+ -- Task 2 progress (starts later, progresses faster)
+ local progress2 = math.max(0, math.min(1.0, (step - 10) / 80))
+ tty.cursor_save()
+ tty.cursor_move_up(2)
+ _progress_bar_inline("Task 2: Compiling ", progress2)
+ tty.cursor_restore()
+
+ -- Task 3 progress (starts even later)
+ local progress3 = math.max(0, math.min(1.0, (step - 30) / 60))
+ tty.cursor_save()
+ tty.cursor_move_up(1)
+ _progress_bar_inline("Task 3: Linking ", progress3)
+ tty.cursor_restore()
+
+ os.sleep(30) -- 30ms delay
+ end
+
+ print("\n\nDemo 2: Dynamic Status Updates")
+ print("--------------------------------")
+
+ -- Print initial status and counter
+ print("Status: Waiting...")
+ print("Counter: 0")
+
+ local statuses = {
+ "Initializing...",
+ "Loading configuration...",
+ "Parsing files...",
+ "Building dependencies...",
+ "Compiling sources...",
+ "Linking executable...",
+ "Done!"
+ }
+
+ for i, status in ipairs(statuses) do
+ -- Update status line
+ tty.cursor_save()
+ tty.cursor_move_up(2)
+ tty.cr()
+ tty.erase_line()
+ io.write(string.format("Status: %s", status))
+ io.flush()
+ tty.cursor_restore()
+
+ -- Update counter line
+ tty.cursor_save()
+ tty.cursor_move_up(1)
+ tty.cr()
+ tty.erase_line()
+ io.write(string.format("Counter: %d", i * 100))
+ io.flush()
+ tty.cursor_restore()
+
+ os.sleep(500)
+ end
+
+ -- Restore cursor visibility
+ tty.cursor_show()
+
+ print("\n\nDemo 3: Erase and Update Specific Line")
+ print("----------------------------------------")
+
+ print("Line 1: This line will stay")
+ print("Line 2: This line will be updated...")
+ print("Line 3: This line will stay too")
+
+ os.sleep(1000)
+
+ -- Update only the middle line
+ tty.cursor_save()
+ tty.cursor_move_up(2)
+ tty.cr()
+ tty.erase_line()
+ io.write("Line 2: Updated content! ✓")
+ io.flush()
+ tty.cursor_restore()
+ print("\n\nAll demos completed!")
+ print("\nKey features demonstrated:")
+ print(" - cursor_move_up/down/left/right: Move cursor relatively")
+ print(" - cursor_save/restore: Save and restore cursor position")
+ print(" - erase_line(): Clear current line")
+ print(" - cursor_hide()/cursor_show(): Control cursor visibility")
+ print(" - Partial screen refresh without clearing entire screen")
+end
+
diff --git a/tests/modules/tty/live_dashboard.lua b/tests/modules/tty/live_dashboard.lua
new file mode 100644
index 000000000..fa239de87
--- /dev/null
+++ b/tests/modules/tty/live_dashboard.lua
@@ -0,0 +1,156 @@
+import("core.base.tty")
+import("core.base.scheduler")
+
+-- Demo advanced usage: Live updating dashboard
+-- Similar to build tool real-time output
+
+function simulate_build_process()
+ if not tty.has_vtansi() then
+ print("Terminal does not support ANSI control codes")
+ return
+ end
+
+ print("\nBuild Dashboard - Live Updates")
+ print(string.rep("=", 60))
+
+ -- Hide cursor
+ tty.cursor_hide()
+
+ -- Print task list
+ print("⏸ Parse project files [" .. string.rep("░", 30) .. "] 0%")
+ print("⏸ Resolve dependencies [" .. string.rep("░", 30) .. "] 0%")
+ print("⏸ Compile sources [" .. string.rep("░", 30) .. "] 0%")
+ print("⏸ Link executable [" .. string.rep("░", 30) .. "] 0%")
+ print("⏸ Create package [" .. string.rep("░", 30) .. "] 0%")
+ print("\nRecent logs:")
+
+ local function update_task(row_offset, icon, name, progress)
+ tty.cursor_save()
+ tty.cursor_move_up(row_offset)
+ tty.cr()
+ tty.erase_line()
+ local bar_width = 30
+ local filled = math.floor(progress * bar_width)
+ local bar = string.rep("█", filled) .. string.rep("░", bar_width - filled)
+ io.write(string.format("%s %-24s [%s] %3d%%", icon, name, bar, math.floor(progress * 100)))
+ io.flush()
+ tty.cursor_restore()
+ end
+
+ local function add_log(message)
+ io.write(string.format("[%s] %s\n", os.date("%H:%M:%S"), message))
+ io.flush()
+ end
+
+ scheduler.co_start(function()
+ -- Task 1: Parse
+ add_log("Starting project parsing...")
+ for i = 1, 20 do
+ update_task(7, "▶", "Parse project files", i / 20)
+ os.sleep(50)
+ end
+ update_task(7, "✓", "Parse project files", 1.0)
+ add_log("Project parsed successfully")
+ os.sleep(200)
+
+ -- Task 2: Dependencies
+ add_log("Resolving dependencies...")
+ for i = 1, 15 do
+ update_task(6, "▶", "Resolve dependencies", i / 15)
+ os.sleep(80)
+ end
+ update_task(6, "✓", "Resolve dependencies", 1.0)
+ add_log("Dependencies resolved: 12 packages")
+ os.sleep(200)
+
+ -- Task 3: Compile
+ add_log("Compiling source files...")
+ local source_files = {"main.cpp", "utils.cpp", "config.cpp", "parser.cpp", "builder.cpp"}
+ for i = 1, #source_files do
+ update_task(5, "▶", "Compile sources", i / #source_files)
+ add_log("Compiling " .. source_files[i])
+ os.sleep(300)
+ end
+ update_task(5, "✓", "Compile sources", 1.0)
+ add_log("Compilation completed: 5 files")
+ os.sleep(200)
+
+ -- Task 4: Link
+ add_log("Linking executable...")
+ for i = 1, 10 do
+ update_task(4, "▶", "Link executable", i / 10)
+ os.sleep(100)
+ end
+ update_task(4, "✓", "Link executable", 1.0)
+ add_log("Executable created: build/myapp")
+ os.sleep(200)
+
+ -- Task 5: Package
+ add_log("Creating package...")
+ for i = 1, 8 do
+ update_task(3, "▶", "Create package", i / 8)
+ os.sleep(120)
+ end
+ update_task(3, "✓", "Create package", 1.0)
+ add_log("Package created: dist/myapp-1.0.0.tar.gz")
+
+ -- Done
+ os.sleep(500)
+ add_log("Build completed successfully! 🎉")
+
+ -- Restore cursor
+ os.sleep(1000)
+ tty.cursor_show()
+
+ print("\nBuild process completed!")
+ end)
+end
+
+function demo_spinner()
+ if not tty.has_vtansi() then
+ return
+ end
+
+ print("\n\n=== Spinner Demo ===")
+ print("Loading")
+
+ local frames = {"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
+ tty.cursor_hide()
+
+ for round = 1, 3 do
+ for _, frame in ipairs(frames) do
+ tty.cursor_save()
+ tty.cursor_move_up(1)
+ tty.erase_line()
+ io.write(string.format("Loading %s [round %d/3]", frame, round))
+ io.flush()
+ tty.cursor_restore()
+ os.sleep(80)
+ end
+ end
+
+ tty.cursor_save()
+ tty.cursor_move_up(1)
+ tty.erase_line()
+ io.write("Loading ✓ Complete!")
+ io.flush()
+ tty.cursor_restore()
+ tty.cursor_show()
+
+ print("")
+end
+
+function main()
+ print("\n" .. string.rep("=", 60))
+ print("TTY Live Dashboard Demo")
+ print(string.rep("=", 60))
+
+ -- Demo 1: Spinner
+ demo_spinner()
+
+ os.sleep(1000)
+
+ -- Demo 2: Build Dashboard
+ simulate_build_process()
+end
+
diff --git a/tests/modules/tty/quick_example.lua b/tests/modules/tty/quick_example.lua
new file mode 100644
index 000000000..6b68394f7
--- /dev/null
+++ b/tests/modules/tty/quick_example.lua
@@ -0,0 +1,140 @@
+-- Quick example: Demonstrates the most common use cases
+
+import("core.base.tty")
+
+function example1_simple_progress()
+ print("\n=== Example 1: Simple Progress Bar ===\n")
+
+ if not tty.has_vtansi() then
+ print("ANSI not supported")
+ return
+ end
+
+ print("Downloading file...")
+ for i = 0, 100, 5 do
+ local width = 40
+ local filled = math.floor(i / 100 * width)
+ local bar = string.rep("█", filled) .. string.rep("░", width - filled)
+
+ -- Move to start of line, clear it, and redraw
+ tty.cr()
+ tty.erase_line()
+ io.write(string.format("[%s] %3d%%", bar, i))
+ io.flush()
+
+ os.sleep(50)
+ end
+ print("") -- New line after progress
+end
+
+function example2_update_previous_line()
+ print("\n=== Example 2: Update Previous Line ===\n")
+
+ if not tty.has_vtansi() then
+ print("ANSI not supported")
+ return
+ end
+
+ print("Building project...")
+ print("Status: Starting...")
+
+ os.sleep(1000)
+
+ -- Go back and update the status line
+ tty.cursor_move_up(1)
+ tty.erase_line()
+ io.write("Status: Compiling files...")
+ io.flush()
+ print("") -- Move to next line
+
+ os.sleep(1000)
+
+ tty.cursor_move_up(1)
+ tty.erase_line()
+ io.write("Status: Done! ✓")
+ io.flush()
+ print("")
+end
+
+function example3_multi_line_update()
+ print("\n=== Example 3: Multi-line Updates ===\n")
+
+ if not tty.has_vtansi() then
+ print("ANSI not supported")
+ return
+ end
+
+ -- Create a simple status board
+ print("Task 1: Waiting...")
+ print("Task 2: Waiting...")
+ print("Task 3: Waiting...")
+
+ tty.cursor_hide()
+
+ -- Update Task 1
+ tty.cursor_save()
+ tty.cursor_move_up(3)
+ tty.erase_line()
+ io.write("Task 1: Running... ")
+ io.flush()
+ tty.cursor_restore()
+ os.sleep(500)
+
+ tty.cursor_save()
+ tty.cursor_move_up(3)
+ tty.erase_line()
+ io.write("Task 1: Done ✓")
+ io.flush()
+ tty.cursor_restore()
+
+ -- Update Task 2
+ tty.cursor_save()
+ tty.cursor_move_up(2)
+ tty.erase_line()
+ io.write("Task 2: Running... ")
+ io.flush()
+ tty.cursor_restore()
+ os.sleep(500)
+
+ tty.cursor_save()
+ tty.cursor_move_up(2)
+ tty.erase_line()
+ io.write("Task 2: Done ✓")
+ io.flush()
+ tty.cursor_restore()
+
+ -- Update Task 3
+ tty.cursor_save()
+ tty.cursor_move_up(1)
+ tty.erase_line()
+ io.write("Task 3: Running... ")
+ io.flush()
+ tty.cursor_restore()
+ os.sleep(500)
+
+ tty.cursor_save()
+ tty.cursor_move_up(1)
+ tty.erase_line()
+ io.write("Task 3: Done ✓")
+ io.flush()
+ tty.cursor_restore()
+
+ tty.cursor_show()
+end
+
+function main()
+ print(string.rep("=", 60))
+ print("TTY Cursor Control - Quick Examples")
+ print(string.rep("=", 60))
+
+ example1_simple_progress()
+ example2_update_previous_line()
+ example3_multi_line_update()
+
+ print("\n" .. string.rep("=", 60))
+ print("All examples completed!")
+ print("Check test.lua, cursor_control.lua, and live_dashboard.lua")
+ print("for more advanced examples.")
+ print(string.rep("=", 60))
+end
+
diff --git a/tests/modules/tty/test.lua b/tests/modules/tty/test.lua
new file mode 100644
index 000000000..af72006ff
--- /dev/null
+++ b/tests/modules/tty/test.lua
@@ -0,0 +1,219 @@
+import("core.base.tty")
+
+function test_cursor_move()
+ if not tty.has_vtansi() then
+ print("Terminal does not support ANSI control codes, skipping test")
+ return
+ end
+
+ print("\n=== Test: cursor_move ===")
+ print("Line 1")
+ print("Line 2")
+ print("Line 3")
+ print("Line 4")
+ print("Line 5")
+
+ os.sleep(1000)
+
+ -- Move to line 3 and update content
+ tty.cursor_save()
+ tty.cursor_move_up(3)
+ tty.erase_line()
+ io.write("Line 3 - UPDATED!")
+ io.flush()
+ tty.cursor_restore()
+
+ print("\n✓ cursor_move_up test passed")
+end
+
+function test_cursor_move_directions()
+ if not tty.has_vtansi() then
+ return
+ end
+
+ print("\n=== Test: cursor movement directions ===")
+
+ -- Create a small coordinate system
+ for i = 1, 5 do
+ print(string.rep(" ", 60))
+ end
+
+ tty.cursor_save()
+
+ -- Move to starting position
+ tty.cursor_move_up(5)
+ tty.cursor_move_right(10)
+ io.write("START")
+ io.flush()
+ os.sleep(500)
+
+ -- Move right
+ tty.cursor_move_right(10)
+ io.write("RIGHT")
+ io.flush()
+ os.sleep(500)
+
+ -- Move down
+ tty.cursor_move_down(2)
+ io.write("DOWN")
+ io.flush()
+ os.sleep(500)
+
+ -- Move left
+ tty.cursor_move_left(5)
+ io.write("LEFT")
+ io.flush()
+ os.sleep(500)
+
+ -- Move up
+ tty.cursor_move_up(1)
+ io.write("UP")
+ io.flush()
+
+ tty.cursor_restore()
+
+ print("\n✓ cursor movement directions test passed")
+end
+
+function test_cursor_hide_show()
+ if not tty.has_vtansi() then
+ return
+ end
+
+ print("\n=== Test: cursor hide/show ===")
+
+ print("Cursor is visible now...")
+ os.sleep(1000)
+
+ tty.cursor_hide()
+ print("Cursor is hidden now (you shouldn't see it blinking)...")
+ os.sleep(2000)
+
+ tty.cursor_show()
+ print("Cursor is visible again!")
+
+ print("✓ cursor hide/show test passed")
+end
+
+function test_erase_operations()
+ if not tty.has_vtansi() then
+ return
+ end
+
+ print("\n=== Test: erase operations ===")
+
+ -- 测试 erase_line_to_end
+ io.write("This is a long line that will be partially erased")
+ io.flush()
+ os.sleep(1000)
+ tty.cursor_move_left(30)
+ tty.erase_line_to_end()
+ io.write("<- erased to end")
+ io.flush()
+ print("")
+
+ os.sleep(1000)
+
+ -- 测试整行擦除
+ io.write("This entire line will be erased")
+ io.flush()
+ os.sleep(1000)
+ tty.cr()
+ tty.erase_line()
+ io.write("Replaced!")
+ io.flush()
+ print("")
+
+ print("✓ erase operations test passed")
+end
+
+function test_partial_screen_update()
+ if not tty.has_vtansi() then
+ return
+ end
+
+ print("\n=== Test: partial screen update ===")
+
+ -- 创建一个表格
+ print("┌────────────────────────────────┐")
+ print("│ Task │ Status │")
+ print("├────────────────────────────────┤")
+ print("│ Compile │ Pending... │")
+ print("│ Link │ Pending... │")
+ print("│ Package │ Pending... │")
+ print("└────────────────────────────────┘")
+
+ os.sleep(1000)
+
+ -- 更新第一个任务状态
+ tty.cursor_save()
+ tty.cursor_move_up(4)
+ tty.cursor_move_to_col(32)
+ tty.erase_line_to_end()
+ io.write("│ Running... │")
+ io.flush()
+ tty.cursor_restore()
+ os.sleep(800)
+
+ tty.cursor_save()
+ tty.cursor_move_up(4)
+ tty.cursor_move_to_col(32)
+ tty.erase_line_to_end()
+ io.write("│ Done! ✓ │")
+ io.flush()
+ tty.cursor_restore()
+ os.sleep(500)
+
+ -- 更新第二个任务状态
+ tty.cursor_save()
+ tty.cursor_move_up(3)
+ tty.cursor_move_to_col(32)
+ tty.erase_line_to_end()
+ io.write("│ Running... │")
+ io.flush()
+ tty.cursor_restore()
+ os.sleep(800)
+
+ tty.cursor_save()
+ tty.cursor_move_up(3)
+ tty.cursor_move_to_col(32)
+ tty.erase_line_to_end()
+ io.write("│ Done! ✓ │")
+ io.flush()
+ tty.cursor_restore()
+ os.sleep(500)
+
+ -- 更新第三个任务状态
+ tty.cursor_save()
+ tty.cursor_move_up(2)
+ tty.cursor_move_to_col(32)
+ tty.erase_line_to_end()
+ io.write("│ Running... │")
+ io.flush()
+ tty.cursor_restore()
+ os.sleep(800)
+
+ tty.cursor_save()
+ tty.cursor_move_up(2)
+ tty.cursor_move_to_col(32)
+ tty.erase_line_to_end()
+ io.write("│ Done! ✓ │")
+ io.flush()
+ tty.cursor_restore()
+
+ print("\n✓ partial screen update test passed")
+end
+
+function main(...)
+ -- 运行所有测试
+ test_cursor_move()
+ test_cursor_move_directions()
+ test_cursor_hide_show()
+ test_erase_operations()
+ test_partial_screen_update()
+
+ print("\n" .. string.rep("=", 50))
+ print("All TTY cursor control tests completed!")
+ print(string.rep("=", 50))
+end
+
diff --git a/xmake/core/base/tty.lua b/xmake/core/base/tty.lua
index 1e44c64bf..a52a4936f 100644
--- a/xmake/core/base/tty.lua
+++ b/xmake/core/base/tty.lua
@@ -135,6 +135,78 @@ function tty.cursor_and_attrs_restore()
return tty
end
+-- move cursor to absolute position (row, col)
+-- row and col are 1-based (1, 1) is the top-left corner
+function tty.cursor_move(row, col)
+ if tty.has_vtansi() then
+ row = row or 1
+ col = col or 1
+ tty._iowrite(string.format("\x1b[%d;%dH", row, col))
+ end
+ return tty
+end
+
+-- move cursor up by n lines
+function tty.cursor_move_up(n)
+ if tty.has_vtansi() then
+ n = n or 1
+ tty._iowrite(string.format("\x1b[%dA", n))
+ end
+ return tty
+end
+
+-- move cursor down by n lines
+function tty.cursor_move_down(n)
+ if tty.has_vtansi() then
+ n = n or 1
+ tty._iowrite(string.format("\x1b[%dB", n))
+ end
+ return tty
+end
+
+-- move cursor forward (right) by n columns
+function tty.cursor_move_right(n)
+ if tty.has_vtansi() then
+ n = n or 1
+ tty._iowrite(string.format("\x1b[%dC", n))
+ end
+ return tty
+end
+
+-- move cursor backward (left) by n columns
+function tty.cursor_move_left(n)
+ if tty.has_vtansi() then
+ n = n or 1
+ tty._iowrite(string.format("\x1b[%dD", n))
+ end
+ return tty
+end
+
+-- move cursor to specified column
+function tty.cursor_move_to_col(col)
+ if tty.has_vtansi() then
+ col = col or 1
+ tty._iowrite(string.format("\x1b[%dG", col))
+ end
+ return tty
+end
+
+-- hide cursor
+function tty.cursor_hide()
+ if tty.has_vtansi() then
+ tty._iowrite("\x1b[?25l")
+ end
+ return tty
+end
+
+-- show cursor
+function tty.cursor_show()
+ if tty.has_vtansi() then
+ tty._iowrite("\x1b[?25h")
+ end
+ return tty
+end
+
-- carriage return
function tty.cr()
tty._iowrite("\r")