Browse Source

Merge pull request #5189 from Feoramund/fix-ansi-log-terminal

Add `core:terminal`, fix test runner/`core:log` ANSI code issues
Jeroen van Rijn 3 months ago
parent
commit
f65e418dc9

+ 53 - 11
core/log/file_console_logger.odin

@@ -2,10 +2,12 @@
 #+build !orca
 package log
 
-import "core:encoding/ansi"
+import "base:runtime"
 import "core:fmt"
 import "core:strings"
 import "core:os"
+import "core:terminal"
+import "core:terminal/ansi"
 import "core:time"
 
 Level_Headers := [?]string{
@@ -37,11 +39,36 @@ File_Console_Logger_Data :: struct {
 	ident: string,
 }
 
+@(private) global_subtract_stdout_options: Options
+@(private) global_subtract_stderr_options: Options
+
+@(init, private)
+init_standard_stream_status :: proc() {
+	// NOTE(Feoramund): While it is technically possible for these streams to
+	// be redirected during the runtime of the program, the cost of checking on
+	// every single log message is not worth it to support such an
+	// uncommonly-used feature.
+	if terminal.color_enabled {
+		// This is done this way because it's possible that only one of these
+		// streams could be redirected to a file.
+		if !terminal.is_terminal(os.stdout) {
+			global_subtract_stdout_options = {.Terminal_Color}
+		}
+		if !terminal.is_terminal(os.stderr) {
+			global_subtract_stderr_options = {.Terminal_Color}
+		}
+	} else {
+		// Override any terminal coloring.
+		global_subtract_stdout_options = {.Terminal_Color}
+		global_subtract_stderr_options = {.Terminal_Color}
+	}
+}
+
 create_file_logger :: proc(h: os.Handle, lowest := Level.Debug, opt := Default_File_Logger_Opts, ident := "", allocator := context.allocator) -> Logger {
 	data := new(File_Console_Logger_Data, allocator)
 	data.file_handle = h
 	data.ident = ident
-	return Logger{file_console_logger_proc, data, lowest, opt}
+	return Logger{file_logger_proc, data, lowest, opt}
 }
 
 destroy_file_logger :: proc(log: Logger, allocator := context.allocator) {
@@ -56,19 +83,15 @@ create_console_logger :: proc(lowest := Level.Debug, opt := Default_Console_Logg
 	data := new(File_Console_Logger_Data, allocator)
 	data.file_handle = os.INVALID_HANDLE
 	data.ident = ident
-	return Logger{file_console_logger_proc, data, lowest, opt}
+	return Logger{console_logger_proc, data, lowest, opt}
 }
 
 destroy_console_logger :: proc(log: Logger, allocator := context.allocator) {
 	free(log.data, allocator)
 }
 
-file_console_logger_proc :: proc(logger_data: rawptr, level: Level, text: string, options: Options, location := #caller_location) {
-	data := cast(^File_Console_Logger_Data)logger_data
-	h: os.Handle = os.stdout if level <= Level.Error else os.stderr
-	if data.file_handle != os.INVALID_HANDLE {
-		h = data.file_handle
-	}
+@(private)
+_file_console_logger_proc :: proc(h: os.Handle, ident: string, level: Level, text: string, options: Options, location: runtime.Source_Code_Location) {
 	backing: [1024]byte //NOTE(Hoej): 1024 might be too much for a header backing, unless somebody has really long paths.
 	buf := strings.builder_from_bytes(backing[:])
 
@@ -86,13 +109,32 @@ file_console_logger_proc :: proc(logger_data: rawptr, level: Level, text: string
 		fmt.sbprintf(&buf, "[{}] ", os.current_thread_id())
 	}
 
-	if data.ident != "" {
-		fmt.sbprintf(&buf, "[%s] ", data.ident)
+	if ident != "" {
+		fmt.sbprintf(&buf, "[%s] ", ident)
 	}
 	//TODO(Hoej): When we have better atomics and such, make this thread-safe
 	fmt.fprintf(h, "%s%s\n", strings.to_string(buf), text)
 }
 
+file_logger_proc :: proc(logger_data: rawptr, level: Level, text: string, options: Options, location := #caller_location) {
+	data := cast(^File_Console_Logger_Data)logger_data
+	_file_console_logger_proc(data.file_handle, data.ident, level, text, options, location)
+}
+
+console_logger_proc :: proc(logger_data: rawptr, level: Level, text: string, options: Options, location := #caller_location) {
+	options := options
+	data := cast(^File_Console_Logger_Data)logger_data
+	h: os.Handle = ---
+	if level < Level.Error {
+		h = os.stdout
+		options -= global_subtract_stdout_options
+	} else {
+		h = os.stderr
+		options -= global_subtract_stderr_options
+	}
+	_file_console_logger_proc(h, data.ident, level, text, options, location)
+}
+
 do_level_header :: proc(opts: Options, str: ^strings.Builder, level: Level) {
 
 	RESET     :: ansi.CSI + ansi.RESET           + ansi.SGR

+ 0 - 0
core/encoding/ansi/ansi.odin → core/terminal/ansi/ansi.odin


+ 0 - 0
core/encoding/ansi/doc.odin → core/terminal/ansi/doc.odin


+ 4 - 0
core/terminal/doc.odin

@@ -0,0 +1,4 @@
+/*
+This package is for interacting with the command line interface of the system.
+*/
+package terminal

+ 87 - 0
core/terminal/internal.odin

@@ -0,0 +1,87 @@
+#+private
+package terminal
+
+import "core:os"
+import "core:strings"
+
+// Reference documentation:
+//
+// - [[ https://no-color.org/ ]]
+// - [[ https://github.com/termstandard/colors ]]
+// - [[ https://invisible-island.net/ncurses/terminfo.src.html ]]
+
+get_no_color :: proc() -> bool {
+	if no_color, ok := os.lookup_env("NO_COLOR"); ok {
+		defer delete(no_color)
+		return no_color != ""
+	}
+	return false
+}
+
+get_environment_color :: proc() -> Color_Depth {
+	// `COLORTERM` is non-standard but widespread and unambiguous.
+	if colorterm, ok := os.lookup_env("COLORTERM"); ok {
+		defer delete(colorterm)
+		// These are the only values that are typically advertised that have
+		// anything to do with color depth.
+		if colorterm == "truecolor" || colorterm == "24bit" {
+			return .True_Color
+		}
+	}
+
+	if term, ok := os.lookup_env("TERM"); ok {
+		defer delete(term)
+		if strings.contains(term, "-truecolor") {
+			return .True_Color
+		}
+		if strings.contains(term, "-256color") {
+			return .Eight_Bit
+		}
+		if strings.contains(term, "-16color") {
+			return .Four_Bit
+		}
+
+		// The `terminfo` database, which is stored in binary on *nix
+		// platforms, has an undocumented format that is not guaranteed to be
+		// portable, so beyond this point, we can only make safe assumptions.
+		//
+		// This section should only be necessary for terminals that do not
+		// define any of the previous environment values.
+		//
+		// Only a small sampling of some common values are checked here.
+		switch term {
+		case "ansi":       fallthrough
+		case "konsole":    fallthrough
+		case "putty":      fallthrough
+		case "rxvt":       fallthrough
+		case "rxvt-color": fallthrough
+		case "screen":     fallthrough
+		case "st":         fallthrough
+		case "tmux":       fallthrough
+		case "vte":        fallthrough
+		case "xterm":      fallthrough
+		case "xterm-color":
+			return .Three_Bit
+		}
+	}
+
+	return .None
+}
+
+@(init)
+init_terminal :: proc() {
+	_init_terminal()
+
+	// We respect `NO_COLOR` specifically as a color-disabler but not as a
+	// blanket ban on any terminal manipulation codes, hence why this comes
+	// after `_init_terminal` which will allow Windows to enable Virtual
+	// Terminal Processing for non-color control sequences.
+	if !get_no_color() {
+		color_enabled = color_depth > .None
+	}
+}
+
+@(fini)
+fini_terminal :: proc() {
+	_fini_terminal()
+}

+ 36 - 0
core/terminal/terminal.odin

@@ -0,0 +1,36 @@
+package terminal
+
+import "core:os"
+
+/*
+This describes the range of colors that a terminal is capable of supporting.
+*/
+Color_Depth :: enum {
+	None,       // No color support
+	Three_Bit,  // 8 colors
+	Four_Bit,   // 16 colors
+	Eight_Bit,  // 256 colors
+	True_Color, // 24-bit true color
+}
+
+/*
+Returns true if the file `handle` is attached to a terminal.
+
+This is normally true for `os.stdout` and `os.stderr` unless they are
+redirected to a file.
+*/
+@(require_results)
+is_terminal :: proc(handle: os.Handle) -> bool {
+	return _is_terminal(handle)
+}
+
+/*
+This is true if the terminal is accepting any form of colored text output.
+*/
+color_enabled: bool
+
+/*
+This value reports the color depth support as reported by the terminal at the
+start of the program.
+*/
+color_depth: Color_Depth

+ 16 - 0
core/terminal/terminal_posix.odin

@@ -0,0 +1,16 @@
+#+private
+#+build linux, darwin, netbsd, openbsd, freebsd, haiku
+package terminal
+
+import "core:os"
+import "core:sys/posix"
+
+_is_terminal :: proc(handle: os.Handle) -> bool {
+	return bool(posix.isatty(posix.FD(handle)))
+}
+
+_init_terminal :: proc() {
+	color_depth = get_environment_color()
+}
+
+_fini_terminal :: proc() { }

+ 60 - 0
core/terminal/terminal_windows.odin

@@ -0,0 +1,60 @@
+#+private
+package terminal
+
+import "core:os"
+import "core:sys/windows"
+
+_is_terminal :: proc(handle: os.Handle) -> bool {
+	is_tty := windows.GetFileType(windows.HANDLE(handle)) == windows.FILE_TYPE_CHAR
+	return is_tty
+}
+
+old_modes: [2]struct{
+	handle: windows.DWORD,
+	mode: windows.DWORD,
+} = {
+	{windows.STD_OUTPUT_HANDLE, 0},
+	{windows.STD_ERROR_HANDLE, 0},
+}
+
+@(init)
+_init_terminal :: proc() {
+	vtp_enabled: bool
+
+	for &v in old_modes {
+		handle := windows.GetStdHandle(v.handle)
+		if handle == windows.INVALID_HANDLE || handle == nil {
+			return
+		}
+		if windows.GetConsoleMode(handle, &v.mode) {
+			windows.SetConsoleMode(handle, v.mode | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING)
+
+			new_mode: windows.DWORD
+			windows.GetConsoleMode(handle, &new_mode)
+
+			if new_mode & windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING != 0 {
+				vtp_enabled = true
+			}
+		}
+	}
+
+	if vtp_enabled {
+		// This color depth is available on Windows 10 since build 10586.
+		color_depth = .Four_Bit
+	} else {
+		// The user may be on a non-default terminal emulator.
+		color_depth = get_environment_color()
+	}
+}
+
+@(fini)
+_fini_terminal :: proc() {
+	for v in old_modes {
+		handle := windows.GetStdHandle(v.handle)
+		if handle == windows.INVALID_HANDLE || handle == nil {
+			return
+		}
+		
+		windows.SetConsoleMode(handle, v.mode)
+	}
+}

+ 1 - 1
core/testing/reporting.odin

@@ -10,12 +10,12 @@ package testing
 */
 
 import "base:runtime"
-import "core:encoding/ansi"
 import "core:fmt"
 import "core:io"
 import "core:mem"
 import "core:path/filepath"
 import "core:strings"
+import "core:terminal/ansi"
 
 // Definitions of colors for use in the test runner.
 SGR_RESET   :: ansi.CSI + ansi.RESET           + ansi.SGR

+ 94 - 70
core/testing/runner.odin

@@ -13,7 +13,6 @@ package testing
 import "base:intrinsics"
 import "base:runtime"
 import "core:bytes"
-import "core:encoding/ansi"
 @require import "core:encoding/base64"
 @require import "core:encoding/json"
 import "core:fmt"
@@ -25,6 +24,8 @@ import "core:os"
 import "core:slice"
 @require import "core:strings"
 import "core:sync/chan"
+import "core:terminal"
+import "core:terminal/ansi"
 import "core:thread"
 import "core:time"
 
@@ -44,6 +45,7 @@ PER_THREAD_MEMORY     : int    : #config(ODIN_TEST_THREAD_MEMORY, mem.ROLLBACK_S
 // The format is: `package.test_name,test_name_only,...`
 TEST_NAMES            : string : #config(ODIN_TEST_NAMES, "")
 // Show the fancy animated progress report.
+// This requires terminal color support, as well as STDOUT to not be redirected to a file.
 FANCY_OUTPUT          : bool   : #config(ODIN_TEST_FANCY, true)
 // Copy failed tests to the clipboard when done.
 USE_CLIPBOARD         : bool   : #config(ODIN_TEST_CLIPBOARD, false)
@@ -70,6 +72,9 @@ get_log_level :: #force_inline proc() -> runtime.Logger_Level {
 	}
 }
 
+@(private) global_log_colors_disabled: bool
+@(private) global_ansi_disabled: bool
+
 JSON :: struct {
 	total:    int,
 	success:  int,
@@ -129,11 +134,16 @@ run_test_task :: proc(task: thread.Task) {
 	
 	context.assertion_failure_proc = test_assertion_failure_proc
 
+	logger_options := Default_Test_Logger_Opts
+	if global_log_colors_disabled {
+		logger_options -= {.Terminal_Color}
+	}
+
 	context.logger = {
 		procedure = test_logger_proc,
 		data = &data.t,
 		lowest_level = get_log_level(),
-		options = Default_Test_Logger_Opts,
+		options = logger_options,
 	}
 
 	random_generator_state: runtime.Default_Random_State
@@ -204,13 +214,16 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 		}
 	}
 
-	when ODIN_OS == .Windows {
-		console_ansi_init()
-	}
-
 	stdout := io.to_writer(os.stream_from_handle(os.stdout))
 	stderr := io.to_writer(os.stream_from_handle(os.stderr))
 
+	// The animations are only ever shown through STDOUT;
+	// STDERR is used exclusively for logging regardless of error level.
+	global_log_colors_disabled = !terminal.color_enabled || !terminal.is_terminal(os.stderr)
+	global_ansi_disabled       = !terminal.is_terminal(os.stdout)
+
+	should_show_animations := FANCY_OUTPUT && terminal.color_enabled && !global_ansi_disabled
+
 	// -- Prepare test data.
 
 	alloc_error: mem.Allocator_Error
@@ -268,12 +281,12 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 	total_done_count    := 0
 	total_test_count    := len(internal_tests)
 
-	when !FANCY_OUTPUT {
-		// This is strictly for updating the window title when the progress
-		// report is disabled. We're otherwise able to depend on the call to
-		// `needs_to_redraw`.
-		last_done_count := -1
-	}
+
+	// This is strictly for updating the window title when the progress
+	// report is disabled. We're otherwise able to depend on the call to
+	// `needs_to_redraw`.
+	last_done_count := -1
+
 
 	if total_test_count == 0 {
 		// Exit early.
@@ -342,31 +355,31 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 	fmt.assertf(alloc_error == nil, "Error allocating memory for test report: %v", alloc_error)
 	defer destroy_report(&report)
 
-	when FANCY_OUTPUT {
-		// We cannot make use of the ANSI save/restore cursor codes, because they
-		// work by absolute screen coordinates. This will cause unnecessary
-		// scrollback if we print at the bottom of someone's terminal.
-		ansi_redraw_string := fmt.aprintf(
-			// ANSI for "go up N lines then erase the screen from the cursor forward."
-			ansi.CSI + "%i" + ansi.CPL + ansi.CSI + ansi.ED +
-			// We'll combine this with the window title format string, since it
-			// can be printed at the same time.
-			"%s",
-			// 1 extra line for the status bar.
-			1 + len(report.packages), OSC_WINDOW_TITLE)
-		assert(len(ansi_redraw_string) > 0, "Error allocating ANSI redraw string.")
-		defer delete(ansi_redraw_string)
-
-		thread_count_status_string: string = ---
-		{
-			PADDING :: PROGRESS_COLUMN_SPACING + PROGRESS_WIDTH
 
-			unpadded := fmt.tprintf("%i thread%s", thread_count, "" if thread_count == 1 else "s")
-			thread_count_status_string = fmt.aprintf("%- *[1]s", unpadded, report.pkg_column_len + PADDING)
-			assert(len(thread_count_status_string) > 0, "Error allocating thread count status string.")
-		}
-		defer delete(thread_count_status_string)
+	// We cannot make use of the ANSI save/restore cursor codes, because they
+	// work by absolute screen coordinates. This will cause unnecessary
+	// scrollback if we print at the bottom of someone's terminal.
+	ansi_redraw_string := fmt.aprintf(
+		// ANSI for "go up N lines then erase the screen from the cursor forward."
+		ansi.CSI + "%i" + ansi.CPL + ansi.CSI + ansi.ED +
+		// We'll combine this with the window title format string, since it
+		// can be printed at the same time.
+		"%s",
+		// 1 extra line for the status bar.
+		1 + len(report.packages), OSC_WINDOW_TITLE)
+	assert(len(ansi_redraw_string) > 0, "Error allocating ANSI redraw string.")
+	defer delete(ansi_redraw_string)
+
+	thread_count_status_string: string = ---
+	{
+		PADDING :: PROGRESS_COLUMN_SPACING + PROGRESS_WIDTH
+
+		unpadded := fmt.tprintf("%i thread%s", thread_count, "" if thread_count == 1 else "s")
+		thread_count_status_string = fmt.aprintf("%- *[1]s", unpadded, report.pkg_column_len + PADDING)
+		assert(len(thread_count_status_string) > 0, "Error allocating thread count status string.")
 	}
+	defer delete(thread_count_status_string)
+
 
 	task_data_slots: []Task_Data = ---
 	task_data_slots, alloc_error = make([]Task_Data, thread_count)
@@ -442,11 +455,16 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 	// digging through the source to divine everywhere it is used for that.
 	shared_log_allocator := context.allocator
 
+	logger_options := Default_Test_Logger_Opts - {.Short_File_Path, .Line, .Procedure}
+	if global_log_colors_disabled {
+		logger_options -= {.Terminal_Color}
+	}
+
 	context.logger = {
 		procedure = runner_logger_proc,
 		data = &log_messages,
 		lowest_level = get_log_level(),
-		options = Default_Test_Logger_Opts - {.Short_File_Path, .Line, .Procedure},
+		options = logger_options,
 	}
 
 	run_index: int
@@ -481,11 +499,13 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 
 	setup_signal_handler()
 
-	fmt.wprint(stdout, ansi.CSI + ansi.DECTCEM_HIDE)
+	if !global_ansi_disabled {
+		fmt.wprint(stdout, ansi.CSI + ansi.DECTCEM_HIDE)
+	}
 
-	when FANCY_OUTPUT {
-		signals_were_raised := false
+	signals_were_raised := false
 
+	if should_show_animations {
 		redraw_report(stdout, report)
 		draw_status_bar(stdout, thread_count_status_string, total_done_count, total_test_count)
 	}
@@ -703,22 +723,22 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 			break main_loop
 		}
 
-		when FANCY_OUTPUT {
-			// Because the bounds checking procs send directly to STDERR with
-			// no way to redirect or handle them, we need to at least try to
-			// let the user see those messages when using the animated progress
-			// report. This flag may be set by the block of code below if a
-			// signal is raised.
-			//
-			// It'll be purely by luck if the output is interleaved properly,
-			// given the nature of non-thread-safe printing.
-			//
-			// At worst, if Odin did not print any error for this signal, we'll
-			// just re-display the progress report. The fatal log error message
-			// should be enough to clue the user in that something dire has
-			// occurred.
-			bypass_progress_overwrite := false
-		}
+
+		// Because the bounds checking procs send directly to STDERR with
+		// no way to redirect or handle them, we need to at least try to
+		// let the user see those messages when using the animated progress
+		// report. This flag may be set by the block of code below if a
+		// signal is raised.
+		//
+		// It'll be purely by luck if the output is interleaved properly,
+		// given the nature of non-thread-safe printing.
+		//
+		// At worst, if Odin did not print any error for this signal, we'll
+		// just re-display the progress report. The fatal log error message
+		// should be enough to clue the user in that something dire has
+		// occurred.
+		bypass_progress_overwrite := false
+
 
 		if test_index, reason, ok := should_stop_test(); ok {
 			#no_bounds_check report.all_test_states[test_index] = .Failed
@@ -752,7 +772,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 					log.fatalf("Caught signal to stop test #%i %s.%s for: %v.", test_index, it.pkg, it.name, reason)
 				}
 
-				when FANCY_OUTPUT {
+				if should_show_animations {
 					bypass_progress_overwrite = true
 					signals_were_raised = true
 				}
@@ -766,7 +786,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 
 		// -- Redraw.
 
-		when FANCY_OUTPUT {
+		if should_show_animations {
 			if len(log_messages) == 0 && !needs_to_redraw(report) {
 				continue main_loop
 			}
@@ -776,7 +796,9 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 			}
 		} else {
 			if total_done_count != last_done_count {
-				fmt.wprintf(stdout, OSC_WINDOW_TITLE, total_done_count, total_test_count)
+				if !global_ansi_disabled {
+					fmt.wprintf(stdout, OSC_WINDOW_TITLE, total_done_count, total_test_count)
+				}
 				last_done_count = total_done_count
 			}
 
@@ -801,7 +823,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 		clear(&log_messages)
 		bytes.buffer_reset(&batch_buffer)
 
-		when FANCY_OUTPUT {
+		if should_show_animations {
 			redraw_report(batch_writer, report)
 			draw_status_bar(batch_writer, thread_count_status_string, total_done_count, total_test_count)
 			fmt.wprint(stdout, bytes.buffer_to_string(&batch_buffer))
@@ -822,7 +844,7 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 
 	finished_in := time.since(start_time)
 
-	when !FANCY_OUTPUT {
+	if !should_show_animations || !terminal.is_terminal(os.stderr) {
 		// One line to space out the results, since we don't have the status
 		// bar in plain mode.
 		fmt.wprintln(batch_writer)
@@ -836,24 +858,28 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 	
 	if total_done_count != total_test_count {
 		not_run_count := total_test_count - total_done_count
+		message := " %i %s left undone." if global_log_colors_disabled else " " + SGR_READY + "%i" + SGR_RESET + " %s left undone."
 		fmt.wprintf(batch_writer,
-			" " + SGR_READY + "%i" + SGR_RESET + " %s left undone.",
+			message,
 			not_run_count,
 			"test was" if not_run_count == 1 else "tests were")
 	}
 
 	if total_success_count == total_test_count {
+		message := " %s successful." if global_log_colors_disabled else " %s " + SGR_SUCCESS + "successful." + SGR_RESET
 		fmt.wprintfln(batch_writer,
-			" %s " + SGR_SUCCESS + "successful." + SGR_RESET,
+			message,
 			"The test was" if total_test_count == 1 else "All tests were")
 	} else if total_failure_count > 0 {
 		if total_failure_count == total_test_count {
+			message := " %s failed." if global_log_colors_disabled else " %s " + SGR_FAILED + "failed." + SGR_RESET
 			fmt.wprintfln(batch_writer,
-				" %s " + SGR_FAILED + "failed." + SGR_RESET,
+				message,
 				"The test" if total_test_count == 1 else "All tests")
 		} else {
+			message := " %i test%s failed." if global_log_colors_disabled else " " + SGR_FAILED + "%i" + SGR_RESET + " test%s failed."
 			fmt.wprintfln(batch_writer,
-				" " + SGR_FAILED + "%i" + SGR_RESET + " test%s failed.",
+				message,
 				total_failure_count,
 				"" if total_failure_count == 1 else "s")
 		}
@@ -907,9 +933,11 @@ runner :: proc(internal_tests: []Internal_Test) -> bool {
 		}
 	}
 
-	fmt.wprint(stdout, ansi.CSI + ansi.DECTCEM_SHOW)
+	if !global_ansi_disabled {
+		fmt.wprint(stdout, ansi.CSI + ansi.DECTCEM_SHOW)
+	}
 
-	when FANCY_OUTPUT {
+	if should_show_animations {
 		if signals_were_raised {
 			fmt.wprintln(batch_writer, `
 Signals were raised during this test run. Log messages are likely to have collided with each other.
@@ -949,9 +977,5 @@ To partly mitigate this, redirect STDERR to a file or use the -define:ODIN_TEST_
 		fmt.assertf(err == nil, "Error writing JSON report: %v", err)
 	}
 
-	when ODIN_OS == .Windows {
-		console_ansi_fini()
-	}
-
 	return total_success_count == total_test_count
 }

+ 0 - 36
core/testing/runner_windows.odin

@@ -1,36 +0,0 @@
-#+private
-package testing
-
-import win32 "core:sys/windows"
-
-old_stdout_mode: u32
-old_stderr_mode: u32
-
-console_ansi_init :: proc() {
-	stdout := win32.GetStdHandle(win32.STD_OUTPUT_HANDLE)
-	if stdout != win32.INVALID_HANDLE && stdout != nil {
-		if win32.GetConsoleMode(stdout, &old_stdout_mode) {
-			win32.SetConsoleMode(stdout, old_stdout_mode | win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING)
-		}
-	}
-
-	stderr := win32.GetStdHandle(win32.STD_ERROR_HANDLE)
-	if stderr != win32.INVALID_HANDLE && stderr != nil {
-		if win32.GetConsoleMode(stderr, &old_stderr_mode) {
-			win32.SetConsoleMode(stderr, old_stderr_mode | win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING)
-		}
-	}
-}
-
-// Restore the cursor on exit
-console_ansi_fini :: proc() {
-	stdout := win32.GetStdHandle(win32.STD_OUTPUT_HANDLE)
-	if stdout != win32.INVALID_HANDLE && stdout != nil {
-		win32.SetConsoleMode(stdout, old_stdout_mode)
-	}
-
-	stderr := win32.GetStdHandle(win32.STD_ERROR_HANDLE)
-	if stderr != win32.INVALID_HANDLE && stderr != nil {
-		win32.SetConsoleMode(stderr, old_stderr_mode)
-	}
-}

+ 7 - 5
core/testing/signal_handler_libc.odin

@@ -12,9 +12,9 @@ package testing
 
 import "base:intrinsics"
 import "core:c/libc"
-import "core:encoding/ansi"
-import "core:sync"
 import "core:os"
+import "core:sync"
+import "core:terminal/ansi"
 
 @(private="file") stop_runner_flag: libc.sig_atomic_t
 
@@ -63,9 +63,11 @@ stop_test_callback :: proc "c" (sig: libc.int) {
 		// NOTE(Feoramund): Using these write calls in a signal handler is
 		// undefined behavior in C99 but possibly tolerated in POSIX 2008.
 		// Either way, we may as well try to salvage what we can.
-		show_cursor := ansi.CSI + ansi.DECTCEM_SHOW
-		libc.fwrite(raw_data(show_cursor), size_of(byte), len(show_cursor), libc.stdout)
-		libc.fflush(libc.stdout)
+		if !global_ansi_disabled {
+			show_cursor := ansi.CSI + ansi.DECTCEM_SHOW
+			libc.fwrite(raw_data(show_cursor), size_of(byte), len(show_cursor), libc.stdout)
+			libc.fflush(libc.stdout)
+		}
 
 		// This is an attempt at being compliant by avoiding printf.
 		sigbuf: [8]byte

+ 5 - 2
examples/all/all_main.odin

@@ -58,7 +58,6 @@ import trace            "core:debug/trace"
 import dynlib           "core:dynlib"
 import net              "core:net"
 
-import ansi             "core:encoding/ansi"
 import base32           "core:encoding/base32"
 import base64           "core:encoding/base64"
 import cbor             "core:encoding/cbor"
@@ -129,6 +128,9 @@ import strings          "core:strings"
 import sync             "core:sync"
 import testing          "core:testing"
 
+import terminal         "core:terminal"
+import ansi             "core:terminal/ansi"
+
 import edit             "core:text/edit"
 import i18n             "core:text/i18n"
 import match            "core:text/match"
@@ -201,7 +203,6 @@ _ :: pe
 _ :: trace
 _ :: dynlib
 _ :: net
-_ :: ansi
 _ :: base32
 _ :: base64
 _ :: csv
@@ -257,6 +258,8 @@ _ :: strconv
 _ :: strings
 _ :: sync
 _ :: testing
+_ :: terminal
+_ :: ansi
 _ :: scanner
 _ :: i18n
 _ :: match