Skip to content

Allows to run TIC-80 commands from the system console - #2939

Open
imsys wants to merge 25 commits into
nesbox:mainfrom
imsys:console-console
Open

Allows to run TIC-80 commands from the system console#2939
imsys wants to merge 25 commits into
nesbox:mainfrom
imsys:console-console

Conversation

@imsys

@imsys imsys commented May 10, 2026

Copy link
Copy Markdown
Contributor

I noticed that the output of the TIC-80 console was going to my system console, this is quite nice specially when there are a lot of output.

But then I thought: what if we could run commands from the system console too? just mirror both consoles?

This PR allows to do just that, I can type commands using the system console, or through the SDL console. It even allows to run commands while the SDL screen is on a different screen, like on the studio or running a game.

Tested on: Linux, Windows, and Wine.

Screenshot from 2026-06-02 20-57-40

imsys added 3 commits May 10, 2026 19:37
I had to rename getSpritePixel to getSpritePixelLoc, as adding studio.h
to sdl/main.c made a conflict.

src/system/sdl/main.c:348:11: error: conflicting types for
‘getSpritePixel’; have ‘u8(const tic_tile *, s32,  s32)’ {aka ‘unsigned
char(const tic_tile *, int,  int)’}
  348 | static u8 getSpritePixel(const tic_tile* tiles, s32 x, s32 y)
      |           ^~~~~~~~~~~~~~
In file included from src/system/sdl/main.c:24:
src/studio/studio.h:258:4: note: previous declaration of
‘getSpritePixel’ with type ‘u8(tic_tile *, s32,  s32)’ {aka ‘unsigned
char(tic_tile *, int,  int)’}
  258 | u8 getSpritePixel(tic_tile* tiles, s32 x, s32 y);
      |    ^~~~~~~~~~~~~~
Fix Nintendo Switch and Android build.

Error was:
/__w/TIC-80/TIC-80/src/system/sdl/main.c:419:13: error: conflicting
types for 'map2ram'; have 'void(tic_mem *)'
  419 | static void map2ram(tic_mem* tic)
      |             ^~~~~~~
In file included from /__w/TIC-80/TIC-80/src/system/sdl/main.c:24:
[ 98%] Building C object
CMakeFiles/tic80.dir/src/system/nswitch/runtime.o
/__w/TIC-80/TIC-80/src/studio/studio.h:302:6: note: previous declaration
of 'map2ram' with type 'void(tic_ram *, const tic_map *)'
  302 | void map2ram(tic_ram* ram, const tic_map* src);
      |      ^~~~~~~
@imsys

imsys commented May 11, 2026

Copy link
Copy Markdown
Contributor Author

I got it to work in a way that we can send commands through the system terminal, even if the SDL window is not in the console screen.

I tried to test the windows binary using wine, and this feature was not working there.

imsys added 22 commits May 11, 2026 01:14
there were a newline added in the system console after each command,
that was fixed.
[Gemini] I've modified the build configuration and the system logic to
prevent the console window from appearing when TIC-80 is launched via
double-click on Windows, while still maintaining full terminal
integration when launched from a shell.

### Changes Summary

1.  **CMake Build Configuration**:
*   Updated
[cmake/sdl.cmake](cci:7://file:///home/arthur/Projects/tic80-many/tic80.console-console/cmake/sdl.cmake:0:0-0:0)
to define the TIC-80 executable as a **Windows GUI application** (using
the `WIN32` flag). This prevents Windows from automatically allocating a
new console window on startup.
*   Changed the linker flags for MinGW from `-mconsole` to
`-mwindows`.

2.  **Console Attachment Logic**:
*   Modified
[src/system/sdl/main.c](cci:7://file:///home/arthur/Projects/tic80-many/tic80.console-console/src/system/sdl/main.c:0:0-0:0)
to refine how the application detects and attaches to a console.
*   Added a check to ensure standard handles are valid (`!= NULL`)
before attempting to initialize the console subsystem. This ensures that
when launched from the GUI (where handles are `NULL`), no console is
initialized.
*   When launched from a command prompt or Wine terminal, the
application will still correctly call
`AttachConsole(ATTACH_PARENT_PROCESS)` and route output to the existing
shell terminal.
*   Maintained support for standard input/output redirection (e.g.,
`tic80.exe > out.txt`), which will continue to work correctly as the
logic now respects existing valid handles.

These changes satisfy the requirement for a "smart" console that only
exists when the user is actually using TIC-80 from a terminal, and works
correctly under both native Windows and Wine.
I have reverted the previous attempt to change the application subsystem
to `WIN32` (GUI), as it was causing a crash in Wine. Instead, I have
implemented a more robust "Auto-Hide" logic while keeping TIC-80 a
console application.

### Improved Logic in [src/system/sdl/main.c]:

The application now intelligently decides whether to keep or hide the
console window upon startup:

1.  **Launched from a Terminal (CMD, PowerShell, or Linux Shell via
Wine)**:
*   If the application is successfully attached to a parent process
terminal, it re-routes standard streams to it.
*   If it's running in an inherited console (like running `wine
tic80.exe` directly), it detects that the console is already "in use"
(by checking if the cursor position is not at the start of the buffer)
and keeps it active.

2.  **Launched from Explorer (Double Click)**:
*   The application detects that it is the only process in a
brand-new console window (cursor at `0,0` and no parent attachment).
*   It immediately calls `FreeConsole()`, causing the window to
disappear before the main application window appears.

3.  **Launched with Redirection** (e.g., `tic80.exe > output.txt`):
*   The application detects that the output is directed to a file or
pipe (using `GetFileType`) and ensures that standard handles are
correctly mapped to those files instead of attempting to use the console
interface.

This approach achieves the goal of a clean, console-less experience when
launched via the GUI, while preserving full command-line functionality
and avoiding the stability issues encountered with the `WIN32` subsystem
change in Wine.
[Gemini] I've refined the console handling logic to restore system
terminal output when running under Wine, while still automatically
hiding the console window on Windows when the app is launched via
double-click.

### Key Adjustments:
1.  **Consistent Stream Re-mapping**: The application now consistently
attempts to re-map `stdin`, `stdout`, and `stderr` to the available
handles, regardless of whether it's a "real" console or a character
device (like a Wine terminal bridge).
2.  **Wine Compatibility**: In Wine, `AttachConsole` or
`freopen("CONOUT$")` can sometimes fail even when a valid terminal
handle exists. I've added a fallback that uses `_dup2` with the existing
standard handles in these cases, which ensures that `printf` and other
standard I/O functions correctly reach the host terminal.
3.  **Preserved Auto-Hide**: The logic still checks if the application
is running in a "fresh" console window (started from Explorer) by
verifying the cursor position. If it detects a new, empty console, it
calls `FreeConsole()` to hide it, maintaining the desired behavior on
Windows.

This hybrid approach ensures TIC-80 remains a versatile "Dual Mode"
application that respects the environment it's running in, whether it's
a native Windows shell, a Linux terminal via Wine, or a GUI-based
launch.
I've reverted both [cmake/sdl.cmake] and [src/system/sdl/main.c] to
their original states to eliminate any instability and determine if the
crashes in Wine were caused by the specific logic changes.

The application should now behave exactly as it did initially (working
correctly on Windows but showing the unwanted console window). Once you
confirm that this version is stable on your system and in Wine, I will
apply a more targeted fix that avoids the problematic subsystem changes
while still addressing the console window issue.
[Gemini] I have implemented a robust solution to hide the Windows
console window on launch while maintaining full compatibility with
terminal environments and Wine.

### Implementation Details:
1.  **Preserved Subsystem**: TIC-80 remains compiled as a "Console
Application" (`-mconsole`). This avoids the
`STATUS_STACK_BUFFER_OVERRUN` (c0000409) crashes previously encountered
when switching to the GUI subsystem.
2.  **Intelligent Detection**: In [src/system/sdl/main.c], the
application now uses `GetConsoleProcessList` to determine how it was
launched.
- If it's the only process associated with the current console
(typical of a double-click launch from Explorer), it immediately calls
`FreeConsole()` to hide the window.
- If it's attached to a parent process (like a `cmd.exe`,
PowerShell, or Wine terminal), it keeps the console and proceeds with
standard stream initialization.
3.  **Seamless Integration**: The original, stable stream initialization
logic is preserved for all cases where a console is needed, ensuring
that terminal output continues to work perfectly on both native Windows
and Wine.

This approach provides a clean GUI-only experience for casual users
while preserving the powerful command-line features for advanced users
and cross-platform compatibility.
detecting if opened by double click or from terminal
Result from previous debug:
Win Terminal:
isWine: 0
HWND: 0000000000050570 Title: Command Prompt - C:\tic-test\tic80. exe
hln: 000000000000005C hout: 0000000000000060 (Inv: FFFFFFFFFFFFFFFF)
procCount: 2 In/OutType: 2/2
isatty(0/1): 64/64
Mode[In/Out): 503/7
Cursor: 0, 4
Pathin: N/A
LinkO: N/A

Win GUI:
isWine: 0
HWND: 000000000002057A Title: C:\tic-test\tic80.exe
hln: 000000000000005C hOut: 0000000000000060 (Inv:FFFFFFFFFFFFFFFF)
procCount: 1 In/OutType: 2/2
isatty(0/1): 64/64
Mode(In/Out): 503/7
Cursor: 0, O
Pathin: N/A
LinkO: N/A

Wine Terminal:
isWine: 1
HWND: 000000000002004E Title: N/A
hIn: 000000000000000C hout: 0000000000000010 (Inv: FFFFFFFFFFFFFFFF)
procCount: 1 In/OutType: 2/2
isatty(0/1): 64/64
Mode(In/Out): 503/3
Cursor: 0, 0
PathIn: N/A
Link0: No syscall wrapper

Wine GUI:
isWine: 1
HWND: 0000000000030052 Title: Z:\tmp\test-tic\tic80.exe
hIn: 000000000000003C hout: 0000000000000048 (Inv: FFFFFFFFFFFFFFFF)
procCount: 1 In/OutType: 2/2
isatty(0/1): 64/64
Mode(In/Out): 503/3
Cursor: 0, 0
PathIn: N/A
Link0: No syscall wrapper
Win Terminal:
isWine: 0
HWND: 00000000000206D8
Class: Pseudo ConsoleWindow
Title: Command Prompt - C:\tic-test\tic80.exe
hln: 000000000000005C hOut: 0000000000000060
procCount: 2 In/OutType: 2/2
Mode(In/Out): 503/7
BufSize: 120 x 30 Cursor: 0, 4
LinkO: N/A
LinkProc: N/A

Win GUI:
isWine: 0
HWND: 00000000000106EE
Class: Pseudo ConsoleWindow
Title: C:\tic-test\tic80.exe
hln: 000000000000005C h0ut: 0000000000000060
procCount: 1 In/OutType: 2/2
Mode(In/Out): 503/7
BufSize: 120 x 30 Cursor: 0, O
LinkO: N/A
LinkProc: N/A

--
Wine Terminal:
isWine: 1 HWND: 000000000002004E
Class: WineConsoleClass
Title: N/A
hIn: 000000000000000C hout: 0000000000000010
procCount: 1 In/OutType: 2/2
Mode(In/Out): 503 / 3
BufSize: 156 x 12 Cursor: 0, 0
Link0: No syscall wrapper
LinkProc: \\?\Z:\proc\self\fd\0

Wine GUI:
isWine: 1 HWND: 0000000000030052
Class: WineConsoleClass
Title: Z:\tmp\tic-test\tic80.exe
hIn: 000000000000003C hout: 0000000000000048
procCount: 1 In/OutType: 2/2
Mode(In/Out): 503/3
BufSize: 80 x 150 Cursor: 0,0
Link0: No syscall wrapper
LinkProc: \\?\Z:\proc\self\fd\0
Tests done, conclusion:
On Wine, terminal launches don't have a visible console window
(IsWindowVisible(consoleWnd): 0).
GUI launches (e.g. from KDE) create a visible virtual console
(IsWindowVisible(consoleWnd): 1).
there were an extra > showing in the system console at startup:

 TIC-80 tiny computer
 version 1.2.3101-dev (240d0a1)
 https://tic80.com (C) 2017-2026
>
 hello! type help for help

>
Comment thread src/system/sdl/main.c
Comment on lines +38 to +46
#if defined(__TIC_WINDOWS__)
#include <conio.h>
#include <io.h>
#include <fcntl.h>
#elif defined(__TIC_LINUX__) || defined(__APPLE__) || defined(__TIC_MACOSX__)
#include <signal.h>
#include <unistd.h>
#include <sys/select.h>
#include <termios.h>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Windows (__TIC_WINDOWS__)

  • <conio.h>: Needed for _kbhit() (to check if a key is waiting without blocking) and _getch() (to read a single character without waiting for Enter).
  • <io.h>: Needed for _open_osfhandle() and _dup2(), which are used to reconnect the standard streams (stdin, stdout, stderr) when the console is hidden or redirected.
  • <fcntl.h>: Needed for the _O_TEXT constant used during the stream redirection process.

POSIX (__TIC_LINUX__, __APPLE__, __TIC_MACOSX__)

  • <unistd.h>: Provides read() for reading from the terminal and the STDIN_FILENO constant (the standard input file descriptor).
  • <sys/select.h>: Required for the select() function and the associated macros (fd_set, FD_ZERO, FD_SET). This is used to poll the terminal for input without blocking the entire game loop.
  • <termios.h>: Provides the tcgetattr, tcsetattr, and terminal mode constants (ICANON, ECHO, TCSANOW). These are used to switch the terminal into "Raw Mode" so TIC-80 can capture keystrokes immediately (like arrow keys) without waiting for the user to press Enter.

Comment on lines +4006 to +4008
#ifdef BAREMETALPI
printf("%s", console->input.text);
#endif

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this proposed request makes a sync between tic80 console and the system console, we have to remove the output here, otherwise we get a duplicated command output.

else printBack(console, "\n loading cart...");
}

processKeyboard(console);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to move it down here because it was causing an extra > at the startup:


 TIC-80 tiny computer
 version 1.2.3101-dev (240d0a1)
 https://tic80.com (C) 2017-2026
>
 hello! type help for help

>

Comment thread src/system/sdl/main.c
}

static u8 getSpritePixel(const tic_tile* tiles, s32 x, s32 y)
static u8 getSpritePixelLoc(const tic_tile* tiles, s32 x, s32 y)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to rename it because it started to cause a conflict with the one in studio/studio.c

src/system/sdl/main.c:348:11: error: conflicting types for ‘getSpritePixel’; have ‘u8(const tic_tile *, s32,  s32)’ {aka ‘unsigned char(const tic_tile *, int,  int)’}
  348 | static u8 getSpritePixel(const tic_tile* tiles, s32 x, s32 y)
      |           ^~~~~~~~~~~~~~
In file included from src/system/sdl/main.c:24:
src/studio/studio.h:258:4: note: previous declaration of ‘getSpritePixel’ with type ‘u8(tic_tile *, s32,  s32)’ {aka ‘unsigned char(tic_tile *, int,  int)’}
  258 | u8 getSpritePixel(tic_tile* tiles, s32 x, s32 y);
      |    ^~~~~~~~~~~~~~
make[2]: *** [CMakeFiles/tic80.dir/build.make:79: CMakeFiles/tic80.dir/src/system/sdl/main.c.o] Error 1
make[1]: *** [CMakeFiles/Makefile2:1064: CMakeFiles/tic80.dir/all] Error 2
make: *** [Makefile:156: all] Error 2

Comment thread src/system/sdl/main.c
}

static void map2ram(tic_mem* tic)
static void map2ramLoc(tic_mem* tic)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to rename it because it started to cause a conflict with the one in studio/studio.c on the Android and Nintendo Switch builds.

/__w/TIC-80/TIC-80/src/system/sdl/main.c:419:13: error: conflicting types for 'map2ram'; have 'void(tic_mem *)'
  419 | static void map2ram(tic_mem* tic)
      |             ^~~~~~~
In file included from /__w/TIC-80/TIC-80/src/system/sdl/main.c:24:
[ 98%] Building C object CMakeFiles/tic80.dir/src/system/nswitch/runtime.o
/__w/TIC-80/TIC-80/src/studio/studio.h:302:6: note: previous declaration of 'map2ram' with type 'void(tic_ram *, const tic_map *)'
  302 | void map2ram(tic_ram* ram, const tic_map* src);
      |      ^~~~~~~

@imsys
imsys marked this pull request as ready for review June 2, 2026 23:55
@imsys

imsys commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

The Windows version was working quite nice, but I added extra code to get it properly work on Wine. It is desirable to it work nice on Wine for consistency and to be easier to test while on Linux.

@imsys

imsys commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

I also noticed that on Windows, all versions of TIC-80 seems to popup the terminal for a few milliseconds, and then it disappears when double clicking on the binary. It seems this is due to the way it is compiled.
It seems that it is being compiled as -mconsole (gcc) / /SUBSYSTEM:CONSOLE (MSVC), default for main() and wmain(). The alternative would be: -mwindows / /SUBSYSTEM:WINDOWS, default for WINAPI WinMain() and WINAPI wWinMain().

I don't really work on windows and I don't really want to set a windows dev environment for now. And it is a bit out of scope for this PR.

Anyway, to test this PR on windows, we can just open CMD and start tic80 from there. (drag and drop to the terminal windows.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant