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 **c**onsole **i**nterface **m**usic **p**layer
A lightweight, keyboard-driven music player for the terminal, written in A keyboard-driven music player for the terminal, written in QuickBASIC (QB64).
QuickBASIC (QB64). It plays audio files and M3U playlists directly from the It plays audio files and M3U playlists directly from the command line, featuring
command line, with a minimal two-line display showing playback state. a minimal two-line display showing playback state and an IPC remote control
interface.
## Features ## Features
- Plays individual audio files or M3U playlists - Plays individual audio files or M3U playlists.
- Real-time progress bar and time display (elapsed or remaining) - Real-time progress bar and time display (elapsed or remaining).
- Volume control and seeking via arrow keys - Volume control and seeking via arrow keys.
- Shuffle and repeat modes - Shuffle and repeat modes.
- Cross-platform: works on Linux, macOS, and Windows - **Client/Server IPC Architecture**: Control a running instance of `cimp` from
- Unicode-aware display 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 ## Building
@ -36,9 +39,12 @@ qb64 -x cimp.bas
cimp [options] <file|playlist> [file2 ...] 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 | | 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`) | | `-r [1]` | `--repeat [1]` | Repeat all (`-r`) or repeat one (`-r 1`) |
| `-n` | `--nooutput` | Run silently with no display | | `-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 ### Examples
#### Play a single file normally
```shell ```shell
# Play a single file
cimp song.mp3 cimp song.mp3
```
# Play a playlist shuffled at 80% volume ### Remote control examples (Run from a separate terminal)
cimp -s -v 80 playlist.m3u
# Play multiple files with repeat #### Pause the music
cimp -r track1.ogg track2.ogg track3.ogg
```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 ## Keybindings
When interacting directly with the player interface terminal, use these keys:
| Key | Action | | Key | Action |
| ------------- | ----------------------------------------- | | ------------- | ----------------------------------------- |
| `↑` / `↓` | Volume up / down | | `↑` / `↓` | Volume up / down |
@ -85,13 +135,19 @@ both compilers:
## Files ## Files
| File | Description | | File | Description |
| ------------- | ---------------------------------------------------------------------------- | | ------------- | --------------------------------------------------------------------------------------- |
| `cimp.bas` | Main player — QB64-PE source | | `cimp.bas` | Main player — QB64-PE source handling client/server states and UI |
| `terminkey.h` | C library for cross-platform raw keyboard input and terminal width detection | | `terminkey.h` | C library for cross-platform raw keyboard input, IPC pipes/sockets, and terminal sizing |
## Notes ## 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. - 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 - Volume adjustment is non-linear for a more natural response and better control
at lower ranges. at lower ranges.
```
```

View file

@ -3,11 +3,23 @@
#ifdef _WIN32 #ifdef _WIN32
#include <windows.h> #include <windows.h>
#include <conio.h> #include <conio.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#else #else
#include <termios.h> #include <termios.h>
#include <unistd.h> #include <unistd.h>
#include <fcntl.h> #include <fcntl.h>
#include <sys/ioctl.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 #endif
// Special key codes (unified across platforms) // Special key codes (unified across platforms)
@ -21,8 +33,17 @@ void echooff();
void echoon(); void echoon();
int termwidth(); 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 #ifdef _WIN32
static HANDLE hServerPipe = INVALID_HANDLE_VALUE;
static OVERLAPPED oOverlap;
static BOOL fPendingIO = FALSE;
int terminkey() { int terminkey() {
if (_kbhit()) { if (_kbhit()) {
int ch = _getch(); int ch = _getch();
@ -65,8 +86,126 @@ int termwidth() {
return -1; 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 #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() { int terminkey() {
struct termios oldt, newt; struct termios oldt, newt;
int ch; int ch;
@ -126,4 +265,102 @@ int termwidth() {
return -1; 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 #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