Compare commits

...

23 commits

Author SHA1 Message Date
visionmercer
82c859317b fix filename display on windows 2026-06-24 13:37:45 +02:00
visionmercer
bd11dfb7ca nyan 2026-06-24 12:41:56 +02:00
4cf569b01e Update readme.md 2026-06-24 12:19:17 +02:00
visionmercer
52c0e73659 update readme 2026-06-24 12:18:21 +02:00
visionmercer
14d3e11ac1 fixes #9 2026-06-24 12:00:25 +02:00
visionmercer
58844384f2 restructure and additions to IPC 2026-06-24 11:44:10 +02:00
cd290f0d2b Update todo.md 2026-06-23 22:13:08 +02:00
visionmercer
a3c7f66b2f losing faith. 2026-06-23 12:47:05 +02:00
visionmercer
a781ef1453 Getting better 2026-06-23 12:40:40 +02:00
visionmercer
96c694f78d A better fix maybe? 2026-06-23 12:33:49 +02:00
visionmercer
c2d86b6f46 Windows IPC fix, maybe? 2026-06-23 12:22:51 +02:00
visionmercer
301a6423db remove debug print 2026-06-23 12:17:59 +02:00
visionmercer
19701af4ec A few fixes to IPC 2026-06-23 12:10:10 +02:00
visionmercer
19bf4b5288 Add Inter-Process Communication 2026-06-23 10:21:17 +02:00
visionmercer
46cdaf4e63 lint 2026-06-10 10:35:36 +02:00
visionmercer
c1d109e198 reject crlf embrace lf 2026-06-10 10:32:15 +02:00
visionmercer
b52fdf512c reset on exit more. 2026-06-10 08:52:52 +02:00
visionmercer
abc48259bd Reset colors on exit? 2026-06-09 11:23:36 +02:00
visionmercer
0de770ce99 comments not needed here 2026-06-09 11:03:47 +02:00
visionmercer
a894f36ffa baha 2026-06-09 10:58:47 +02:00
visionmercer
8af3bc9649 bah 2026-06-09 10:57:46 +02:00
visionmercer
765372aa6b respect repeat regulation 2026-06-09 10:56:09 +02:00
visionmercer
b6c1a1c1c0 marquee and cleanup 2026-06-09 10:36:00 +02:00
5 changed files with 1047 additions and 448 deletions

1106
cimp.bas

File diff suppressed because it is too large Load diff

View file

@ -2,18 +2,21 @@
**c**onsole **i**nterface **m**usic **p**layer
A lightweight, keyboard-driven music player for the terminal, written in
QuickBASIC (QB64). It plays audio files and M3U playlists directly from the
command line, with a minimal two-line display showing playback state.
A keyboard-driven music player for the terminal, written in QuickBASIC (QB64).
It plays audio files and M3U playlists directly from the command line, featuring
a minimal two-line display showing playback state and an IPC remote control
interface.
## Features
- Plays individual audio files or M3U playlists
- Real-time progress bar and time display (elapsed or remaining)
- Volume control and seeking via arrow keys
- Shuffle and repeat modes
- Cross-platform: works on Linux, macOS, and Windows
- Unicode-aware display
- Plays individual audio files or M3U playlists.
- Real-time progress bar and time display (elapsed or remaining).
- Volume control and seeking via arrow keys.
- Shuffle and repeat modes.
- **Client/Server IPC Architecture**: Control a running instance of `cimp` from
another terminal window or script.
- Cross-platform: works on Linux, macOS, and Windows.
- Unicode-aware display with support for full-width character width matching.
## Building
@ -36,9 +39,12 @@ qb64 -x cimp.bas
cimp [options] <file|playlist> [file2 ...]
```
Pass one or more audio files or `.m3u` playlists. At least one file is required.
Pass one or more audio files or `.m3u` playlists. At least one file is required
unless sending a remote control command.
### Options
### Global & Server Options
These flags control the primary player instance when launched.
| Flag | Long form | Description |
| -------- | -------------- | ---------------------------------------- |
@ -47,21 +53,65 @@ Pass one or more audio files or `.m3u` playlists. At least one file is required.
| `-r [1]` | `--repeat [1]` | Repeat all (`-r`) or repeat one (`-r 1`) |
| `-n` | `--nooutput` | Run silently with no display |
### Remote Control (Client Mode) Commands
If `cimp` is already running in a terminal, running it again with any of the
following arguments will send an instruction to the active player instead of
starting a new one:
| Command | Description |
| ------------------------- | ---------------------------------------------------------- |
| `--play` | Resume playback |
| `--pause` | Pause playback |
| `--stop` | Stop playback |
| `--next` | Skip to the next track |
| `--prev` | Skip to the previous track |
| `-v <n>` / `--volume <n>` | Remotely adjust volume (0100) |
| `--shuffle` | Reshuffle the current playlist queue |
| `--repeat` | Set player to repeat all |
| `--repeat-1` | Set player to repeat current track |
| `--add <file>` | Append a new file or playlist to the active queue |
| `--playlist` | Fetch and print the current playback queue from the server |
| `--quit` | Remotely close the running player instance |
### Examples
#### Play a single file normally
```shell
# Play a single file
cimp song.mp3
```
# Play a playlist shuffled at 80% volume
cimp -s -v 80 playlist.m3u
### Remote control examples (Run from a separate terminal)
# Play multiple files with repeat
cimp -r track1.ogg track2.ogg track3.ogg
#### Pause the music
```shell
cimp --pause
```
#### Lower the volume to 20%
```shell
cimp --volume 20
```
#### Add another track to the background player's active queue
```shell
cimp --add extra_track.flac
```
#### View the active playlist queue
```shell
cimp --playlist
```
## Keybindings
When interacting directly with the player interface terminal, use these keys:
| Key | Action |
| ------------- | ----------------------------------------- |
| `↑` / `↓` | Volume up / down |
@ -85,13 +135,19 @@ both compilers:
## Files
| File | Description |
| ------------- | ---------------------------------------------------------------------------- |
| `cimp.bas` | Main player — QB64-PE source |
| `terminkey.h` | C library for cross-platform raw keyboard input and terminal width detection |
| File | Description |
| ------------- | --------------------------------------------------------------------------------------- |
| `cimp.bas` | Main player — QB64-PE source handling client/server states and UI |
| `terminkey.h` | C library for cross-platform raw keyboard input, IPC pipes/sockets, and terminal sizing |
## Notes
- **IPC Backend**: On Windows, IPC relies on Named Pipes (`\\.\pipe\cimp_ipc`).
On Linux/macOS, it binds to a local Unix Domain Socket at
`/tmp/cimp_ipc_<uid>.sock`.
- M3U paths can be absolute or relative to the playlist file's directory.
- Volume adjustment is non-linear for a more natural response and better control
at lower ranges.
```
```

View file

@ -3,11 +3,23 @@
#ifdef _WIN32
#include <windows.h>
#include <conio.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#else
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <limits.h>
#include <stdint.h>
#endif
// Special key codes (unified across platforms)
@ -21,8 +33,17 @@ void echooff();
void echoon();
int termwidth();
int ipc_init();
int ipc_check_message(uintptr_t buffer_ptr, int max_len);
void ipc_send_message(const char* message);
void ipc_cleanup();
#ifdef _WIN32
static HANDLE hServerPipe = INVALID_HANDLE_VALUE;
static OVERLAPPED oOverlap;
static BOOL fPendingIO = FALSE;
int terminkey() {
if (_kbhit()) {
int ch = _getch();
@ -65,8 +86,126 @@ int termwidth() {
return -1;
}
int ipc_init() {
// Create a regular blocking pipe, but enable FILE_FLAG_OVERLAPPED for safe async I/O
hServerPipe = CreateNamedPipe(
"\\\\.\\pipe\\cimp_ipc",
PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE,
1,
4096,
4096,
0,
NULL
);
if (hServerPipe == INVALID_HANDLE_VALUE) {
DWORD err = GetLastError();
if (err == ERROR_ACCESS_DENIED || err == ERROR_PIPE_BUSY) {
return 0; // Already running (Client Mode)
}
return -1; // Other error
}
// Initialize the overlapped structure and create an event
memset(&oOverlap, 0, sizeof(OVERLAPPED));
oOverlap.hEvent = CreateEvent(NULL, TRUE, TRUE, NULL);
fPendingIO = FALSE;
return 1; // Server mode successfully started
}
int ipc_check_message(uintptr_t buffer_ptr, int max_len) {
if (hServerPipe == INVALID_HANDLE_VALUE) return 0;
char* buffer = (char*)buffer_ptr;
DWORD bytesRead = 0;
if (!fPendingIO) {
// Start an asynchronous listen operation
if (!ConnectNamedPipe(hServerPipe, &oOverlap)) {
DWORD err = GetLastError();
if (err == ERROR_IO_PENDING) {
fPendingIO = TRUE;
} else if (err == ERROR_PIPE_CONNECTED) {
// Client already connected before we listened!
SetEvent(oOverlap.hEvent);
fPendingIO = TRUE;
}
}
}
if (fPendingIO) {
// Check if the connection event has triggered without waiting (0ms timeout)
if (WaitForSingleObject(oOverlap.hEvent, 0) == WAIT_OBJECT_0) {
DWORD cbRet = 0;
// Connection is ready, let's see if there is data
if (GetOverlappedResult(hServerPipe, &oOverlap, &cbRet, FALSE)) {
DWORD bytesAvail = 0;
if (PeekNamedPipe(hServerPipe, NULL, 0, NULL, &bytesAvail, NULL) && bytesAvail > 0) {
// Data has arrived, read it synchronously
BOOL success = ReadFile(hServerPipe, buffer, max_len - 1, &bytesRead, NULL);
if (success && bytesRead > 0) {
buffer[bytesRead] = '\0';
// Clean up and reset for the next client connection
DisconnectNamedPipe(hServerPipe);
ResetEvent(oOverlap.hEvent);
fPendingIO = FALSE;
return (int)bytesRead;
}
}
} else {
// Client disconnected or pipe broke before sending
DisconnectNamedPipe(hServerPipe);
ResetEvent(oOverlap.hEvent);
fPendingIO = FALSE;
}
}
}
return 0;
}
void ipc_send_message(const char* message) {
HANDLE hPipe = CreateFile(
"\\\\.\\pipe\\cimp_ipc",
GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
0,
NULL
);
if (hPipe != INVALID_HANDLE_VALUE) {
DWORD bytesWritten = 0;
WriteFile(hPipe, message, strlen(message), &bytesWritten, NULL);
CloseHandle(hPipe);
}
}
void ipc_cleanup() {
if (hServerPipe != INVALID_HANDLE_VALUE) {
DisconnectNamedPipe(hServerPipe);
CloseHandle(hServerPipe);
hServerPipe = INVALID_HANDLE_VALUE;
}
if (oOverlap.hEvent != NULL) {
CloseHandle(oOverlap.hEvent);
oOverlap.hEvent = NULL;
}
}
#else
static int server_fd = -1;
static char socket_path[256] = "";
static void get_socket_path() {
if (socket_path[0] == '\0') {
uid_t uid = getuid();
snprintf(socket_path, sizeof(socket_path), "/tmp/cimp_ipc_%u.sock", (unsigned int)uid);
}
}
int terminkey() {
struct termios oldt, newt;
int ch;
@ -126,4 +265,102 @@ int termwidth() {
return -1;
}
int ipc_init() {
get_socket_path();
int test_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (test_fd >= 0) {
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1);
if (connect(test_fd, (struct sockaddr*)&addr, sizeof(addr)) == 0) {
close(test_fd);
return 0; // Already running
}
close(test_fd);
}
unlink(socket_path);
server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (server_fd < 0) {
return -1;
}
int flags = fcntl(server_fd, F_GETFL, 0);
fcntl(server_fd, F_SETFL, flags | O_NONBLOCK);
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1);
if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
close(server_fd);
server_fd = -1;
return -1;
}
if (listen(server_fd, 5) < 0) {
close(server_fd);
server_fd = -1;
unlink(socket_path);
return -1;
}
return 1;
}
int ipc_check_message(uintptr_t buffer_ptr, int max_len) {
if (server_fd < 0) return 0;
char* buffer = (char*)buffer_ptr;
int client_fd = accept(server_fd, NULL, NULL);
if (client_fd < 0) {
return 0;
}
int total_read = 0;
while (total_read < max_len - 1) {
int r = read(client_fd, buffer + total_read, max_len - 1 - total_read);
if (r <= 0) break;
total_read += r;
}
buffer[total_read] = '\0';
close(client_fd);
return total_read;
}
void ipc_send_message(const char* message) {
get_socket_path();
int client_fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (client_fd < 0) return;
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1);
if (connect(client_fd, (struct sockaddr*)&addr, sizeof(addr)) == 0) {
int total_written = 0;
int len = strlen(message);
while (total_written < len) {
int w = write(client_fd, message + total_written, len - total_written);
if (w <= 0) break;
total_written += w;
}
}
close(client_fd);
}
void ipc_cleanup() {
if (server_fd >= 0) {
close(server_fd);
server_fd = -1;
}
get_socket_path();
unlink(socket_path);
}
#endif

44
test_ipc.bas Normal file
View file

@ -0,0 +1,44 @@
$console:only
declare library "terminkey"
function ipc_init&()
function ipc_check_message%(byval buf as _offset, byval max_len as long)
sub ipc_send_message(buf as string)
sub ipc_cleanup()
sub get_absolute_path(rel_path as string, byval abs_path as _offset, byval max_len as long)
end declare
dim r as long
r = ipc_init
print "ipc_init returned:"; r
if r = 1 then
print "We are the server! Waiting for a message..."
dim msg as string
msg = space$(100) + chr$(0)
dim start_time as double
start_time = timer
do while timer - start_time < 3
dim n as long
n = ipc_check_message(_offset(msg), 100)
if n > 0 then
print "Received: "; left$(msg, n)
exit do
end if
_limit 10
loop
ipc_cleanup
elseif r = 0 then
print "We are the client! Sending a message..."
ipc_send_message "Hello from client!"
else
print "Error initializing IPC"
end if
dim rel as string
rel = "readme.md"
dim abs_path as string
abs_path = space$(512) + chr$(0)
get_absolute_path rel, _offset(abs_path), 512
print "Absolute path of '"; rel; "' is: '"; _trim$(abs_path); "'"
system

12
todo.md Normal file
View file

@ -0,0 +1,12 @@
# todo
## ipc
add ipc functionality so it's possible while the program runs to:
- ~play next and previous track with `cimp --next` and `cimp --prev`~
- ~change volume with `cimp --volume xx`~
- ~add file to playlist with `cimp --add filename.ext`~
- if started without `--add` it should clear the playlist and load and play the given file or playlist.
- if given the flag `--playlist` it should output the song array as a playlist.
- start stop pause and quit would be good, repeat as well.
the c functions are ready in terminkey.h and an example of how it works is found in test_ipc.bas
## recurcive add all music in folder