The officially official Devuan Forum!

You are not logged in.

#101 Re: DIY » New Project, a simple music player. And now a video player!! » 2026-02-11 17:02:49

Okay,
1. It does not use as much resources as mpv, mpv has a clickable progress/seek bar, and that means constant progress checks going on in the background. But there's really no comparing the two, they're very different in philosophy, and mpv is some 60 times larger than vsvp and has a lot more depends.

2. I promise you I could install it on your machine if you have the right libraries, it's not debian-specific, it's just yad, bash, ffmpeg and gtk.

3. I'm not opposed to making an appimage of it, just busy with other things, I have a lot of apps to maintain now in addition to multiple Vuu-do and Devuan isos, and to be honest I don't see it getting enough traction to warrant a lot of extra work on it, only a few people will like it, most want the extended functionality of larger players whether they use those features or not.

Myself I think it's a beautiful script, elegant in it's size and brevity of code, leveraging ffplay to do the heavy lifting, the simple workarounds for various issues are actually quite groovy, I think a lot of people when faced with those challenges would have simply said screw it i'll just use mpv for the backend, and then what you would have is just another set of window dressings for mpv of which there are plenty enough already.

I made it for me.
I made it for the challenge.
I made it for the learning experience.
It works and I love it, mission accomplished, level-up complete. I just share things I make on the odd chance someone might like 'em.

#102 Re: Freedom Hacks » About installers » 2026-02-11 16:03:28

FYI I didn't start this thread, I made a post that got moved. But I do appreciate the replies and insight.

#103 Freedom Hacks » About installers » 2026-02-11 01:32:04

greenjeans
Replies: 44

My life is too short and my time too valuable to spend on the debian installer, i'd rather beat my junk with a ball-peen hammer, it's like trying to install win xp back in the day. Has anybody tried the Calamares installer? Is it any better?

Thank the gawds and fsmithred for Refracta-installer, without it I just wouldn't bother.

#104 Re: Off-topic » XFCE is building a new WM and compositor for Wayland » 2026-02-10 21:40:37

Thunar also takes centuries to load up thumbnails in a directory (such as Downloads). Older versions did not have this issue, and it didn't matter if you only had five files or five hundred -- everything loaded blinking fast.

Interesting, must not be caching thumbnails I guess? I have a script for that..... wink

#105 Re: DIY » New Project, a simple music player. And now a video player!! » 2026-02-10 21:05:02

@greenjeans I actually wonder how much cpu usage your video player uses compared to like mplayer or mpv.

Oh I can pretty much guarantee it uses less, just playing a video itself is likely the same, but those players (even mpv which is very basic) have a lot of features that require constant system calls and checks, the progress bar is one.

#106 Re: Installation » [SOLVED] Devuan 6.1.0 desktop installer has no options for modern filesystems » 2026-02-10 17:57:09

@Mercury, because Devuan is extremely short-handed and the devs are over-worked and underpaid, sometimes you have to focus on bigger stuff....

Perhaps you could write out a note about it since you seem familiar with the issue, then submit it for inclusion? I believe Xenguy is who you'd need to contact to add documentation to the website.

#107 Re: Devuan Derivatives » Vuu-do Linux! *New Openbox-64 iso's (1.0.7) up 2-02-18*!! » 2026-02-10 17:18:32

Hey I got a "Community Choice" badge on Sourceforge for hitting 10k downloads! A big heartfelt thank you to all who have taken the time to try out one or more of my little experiments! big_smile

#108 Re: Devuan Derivatives » Devuan 6 (excalibur) mate testing iso uploaded » 2026-02-10 02:24:19

So for those unfamiliar, this is NOT an official Devuan iso, nor is it an official Vuu-do iso. It's just a snapshot of vanilla Devuan with the Mate DE, rolled up from the same daedalus iso. Just a simple way to get up and running with Devuan Mate. FYI I forgot to revert sources.list to the default Devuan list, so gnlug is the current default and I don't feel like running another iso just to deal with that! You can change 'em in Synaptic.
https://sourceforge.net/projects/vuu-do … te-mini-6/

ige5ju.png

g99n2n.png

From the readme:
This is not an official Devuan release, nor a Vuu-do release. This is a user re-spin
of Devuan 6 (excalibur), updated completely as of 2/09/2026 with the Mate desktop environment.
It's purpose is to provide a quick small iso for those interested in testing the
Mate version of Devuan.

2-09-2026 Updated fully, some 155 packages including a new kernel. Also added a lot of
firmware packages, mostly for wi-fi and ethernet but also bluetooth and some
graphics stuff including the nvidia graphics package (for nouveau). All this
is to try and insure a user can get full functionality right out of the box.
It does bloat up initrd some, a smart user can get rid of what they don't need
post-install easily enough. I also added addititonal support for viewing and
thumbnailing newer image formats like heif and avif and jxl.

Fixed the alsa-restore issue using fsmithred's fix.

Fixed the dbus spamming issue from rotating the machine-ID using the init script
I posted in this thread: https://dev1galaxy.org/viewtopic.php?id=7414

Fixed the NM-lease spamming by adding to the post-install Refracta script.

Added updated excludes files for Refracta-tools, added some and commented one
which was the one deleting the gtk icon caches, and rebuilt all the icon caches.

Put a halt to the watchdog spam during boot.

Fixed a couple of useless systemd warnings.

Pulled picom out, it was spamming logs when not in use. Mate has a compositor
built-in which it uses by default. If you really want picom it's in the repo
but you'll need mate-tweak to apply it.

Pulled the gnome policy kit auth package, Mate has it's own, also got rid of
gnome keyring and it's depends.

Fixed and expanded sources.list, the default Devuan repo urls are there, and
also (commented out) are the gnlug listings for an alternate repo if you have
issues with DNS.

Changed the theme, the green-blue was just too clash, switched to BlueMenta
for a nice simple interface, and switched to the deepsea icons which are a
nice blue that compliments the Devuan wallpaper. Only issue with those was
the default menu icon was the gnome foot, so that had to go. I found a large
Devuan logo in pixmaps, so I made new icons from it in gimp, looks really
nice I think!

This is a liveCD and includes the Refracta-installer for fast simple install.
This is not uefi-enabled. The os-prober has been enabled when you install grub, if you
need to disable it before install, while still in live-session, just edit
/etc/default/grub and /usr/share/grub/default/grub to disable the prober.
---------------------------------------------------------------------------
In all respects it is identical to current Devuan, I have not "vuu-do-fied" it,
it contains all original files, docs, translations, help files, original configs,
artwork etc. with the following exceptions:

This is a "mini", it contains no browser, office or multimedia programs whatsoever,
just the infrastructure for you to build on and configure as you see fit. There are no
release notes or manual as this is stock Devuan and ample documentation is available
on the site and forum.

Had to add firmware for AMD before lightdm would work properly for me, this may be
just my machine, or you may need it for Nvidia and Intel too, just don't know yet,
again this is all from the testing section, and hasn't been declared stable yet.

Some packages have been added for convenience, these are:
Mozo - simple mate menu editor
Mate-tweak - handy utility, gets rid of pesky desktop icons if you so desire
Parcellite - clipboard manager
NTPsec - keeps time correct
Caja file manger extensions - resize-rotate images, open terminal here, edit/open as root etc.
Bleachbit - but it has been configured to NOT delete any translations/localizations.

Slight mods to config:
File-manager configged to open files with single-click and include delete command that
bypasses trash - this made my work go much faster. Altered sources.list as main repo has
been having some issues, changed to what's been working for me and what I used to build this iso.
Set lightdm.conf to autologin the user on the liveDVD for convenience.

Username/passwords for LiveDVD:

devuan = devuan
root = root

#109 Re: Desktop and Multimedia » [SOLVED] Devuan Excalibur install and Pipewire. » 2026-02-08 19:40:48

If you're a musician you might want to check out user @rations jack-bridge, it's a JACK+ALSA solution that doesn't require Pipe or Pulse to work. I don't know much about Jack or Midi myself, I just deal with low-level alsa stuff, but perhaps his app might help.

#110 Re: Other Issues » Refracta Snapshot (Excalibur) is Broken » 2026-02-08 19:32:55

Again, have used Snapshot many times on excalibur over the last 6 months, worked fine every time and still does, I have excalibur-based isos on my sourceforge right now that were made with Snapshot.

This seems like a case of user-error.

#111 Re: Off-topic » A blast from the past... » 2026-02-08 16:15:51

Cool story ComputerBob! I love tales from back in the day. The picture above is especially meaningful to me, as my father worked for NASA back in the 60's, and would have likely worked at some point on the very machine that memory stick was used in.

#112 Re: Installation » [SOLVED] excalibur netinstall iso does not boot from USB stick » 2026-02-08 16:13:19

@grebulos, if you'd like a reliable GUI method for writing to USB sticks, try Mintstick, it's in the repo and has been flawless for me over hundreds of uses.

#113 Re: DIY » New Project, a simple music player. And now a video player!! » 2026-02-08 02:22:46

@greenjeans I don't see those ones in Hyperbola. Although something else might exist, not sure.

Its also possible they might have used to exist and I have them still, but I never deleted them. sorry I can't be much more helpful.

No worries my friend, I am so thankful that you tried to test it in the first place! FYI it works swimmingly in Vuu-do, maybe download it and give it a try. smile

#114 Re: DIY » New Project, a simple music player. And now a video player!! » 2026-02-07 21:20:40

@zapper, no that's the gstreamer plugin for ffmpeg, what I was referring to in general are the ffmpeg libraries, libavformat, libavcodec and such. In daedalus we're using 59 i.e. libavformat59, in excalibur it's libavformat61.

#115 Re: Devuan » Kde and Systemd - In the News » 2026-02-06 15:09:24

The usage of GTK4 by HB is certainly unfortunate,

Is that in excalibur/trixie? Because it's still gtk3 in daedalus.

#116 Re: DIY » New Project, a simple music player. And now a video player!! » 2026-02-06 15:00:48

Okay, so I had a quick look at Hyperbola, and it seems kinda unusual the way they do things so I don't even know where to start, I build everything with Devuan packages on a Devuan machine. I don't know if it will work at all on Hyperbola, the libraries they use may be completely different. What versions of libav do they use?

In general to do a manual install, extract the .deb, drop the 3 scripts in /usr/local/bin (assuming that's in your $PATH), drop the .desktop in /usr/share/applications and that's it, works great on Devuan (daedalus).

The .deb will draw in the depends, but doing it manually make sure you have gtk3, ffmpeg, yad, and bash.

#117 Off-topic » A blast from the past... » 2026-02-06 14:44:05

greenjeans
Replies: 4

A single 32k core plane from the IBM 2361 large capacity storage. This one came from the IBM 7094 system at NASA that was used during the later Apollo missions. This is what 1 mb of memory looked like in the mid '60's.

storage-jpg.3097540

#118 Re: Desktop and Multimedia » [SOLVED] Devuan Excalibur install and Pipewire. » 2026-02-05 23:46:41

The issue is, the Devuan community isn’t really focused on MIDI and audio production.

The issue is you posted a detailed how-to about another distro, forum faux pas bro.

Maybe midi not so much, but there's a number of Devuan users who are actively working on Devuan sound, speaking of which, Ralph was nice enough to do a complete set-up of fftrate on Devuan's git, thus handing it to you on a silver platter after you posted a good 100,000 words on the subject over the course of a few months, are you going to take him up on his offer to let you maintain it after all that?

#119 Re: Off-topic » XFCE is building a new WM and compositor for Wayland » 2026-02-05 22:59:18

CAFE (fork of MATE) is doing, particularly CTK

Oh yeah, I have the strangest b.......never mind, i'm just excited.

They need to roll back some Mate goofiness, particularly in Caja. And dump the damn mixer.

#120 Re: Off-topic » XFCE is building a new WM and compositor for Wayland » 2026-02-05 22:56:07

Yeah, from recent experience it seems to go like this:

1. Hey we're adding support for wayland but we're still shipping X11.
2. Hey we're still shipping X11 but wayland is now the default.
3. Hey we're all in on wayland, but you can still use X11 if you jump through these 30 hoops and re-compile some stuff.
4. Hey we are discontinuing support for X11 for (your own) good.

#121 Re: DIY » New Project, a simple music player. And now a video player!! » 2026-02-05 22:51:04

Hold on, did you download the .deb and extract it and install the other scripts? I doubt it or you'd have that file since it's the wrapper script that creates it.

Srsly bro, just install the .deb, it will put everything where it needs to be and set proper permissions.

#122 Re: DIY » New Project, a simple music player. And now a video player!! » 2026-02-05 18:09:04

I have no idea what would work in Hyperbola as i've never used it, and keep in mind this is built on Devuan 5 (daedalus) libraries, I have not tried compiling it or running on the newer excalibur based libraries. The app is already compressed with everything in the .deb but the source code for the single small binary, the other two scripts are tiny shellscripts, and a .desktop. If you want to try compiling the binary yourself:

1. You'll need GCC and pkg-config and their deps, common build stuff.
2. You'll need to install libgtk-3-dev, in Devuan that pulls in the other dev packages you'll need (libx11-dev, libxext-dev etc.), in hyperbola I don't know if it does.
3. Compile command:
gcc -Wall -o vsvp vsvp.c `pkg-config --cflags --libs gtk+-3.0 x11 xext`

Source for the binary, the source script is 30.9 kb, compiled 43.5 kb, vsvp.c:

#include <gtk/gtk.h>
#include <gtk/gtkx.h>
#include <gdk/gdkx.h>              // For GDK X11 functions
#include <X11/Xlib.h>              // For XSelectInput
#include <X11/extensions/shape.h>  // Secret shape sauce
#include <X11/keysym.h>            // For keysyms like XK_Up
#include <stdlib.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include <glib.h>

#define SCRAPER_CMD "/usr/local/bin/vsvps" // For re-scan button

typedef struct {
    pid_t ffplay_pid;
    GtkWidget *play_button, *pause_button, *stop_button, *rebuild_button;
    GtkWidget *socket;            // For embedding ffplay window
    GtkWidget *placeholder_label; // Placeholder when no video is playing
    GtkWidget *socket_container;  // Overlay container
    GtkWidget *paused_overlay;    // Semi-transparent paused cover
    GtkWidget *stopped_overlay;   // Opaque stopped/select screen
    GtkWidget *placeholder_pane;  // Initial right pane
    GtkWidget *paned;             // Paned for swapping
    GtkTreeStore *video_store;
    GtkTreeIter current_video_iter;
    gboolean is_playing;
    gboolean is_paused;
    GtkWidget *video_tree;
    GtkWidget *control_box;
    gboolean socket_active;
    GtkWidget *external_play_button; // Launch external ffplay window
    unsigned long ffplay_wid;        // For sending key events
} AppData;

typedef struct {
    GList *path_components;
    gchar *title;
    gchar *fullpath;
    gboolean is_folder; // set later
} Entry;

// Forward declarations
void on_stop_clicked(GtkButton *button, AppData *app_data);
void populate_treeview(AppData *app_data);

// Send simulated key to ffplay
static void send_key_to_ffplay(AppData *app_data, KeySym keysym) {
    if (app_data->ffplay_wid == 0) return;

    Display *display = gdk_x11_get_default_xdisplay();
    if (!display) return;

    // Optional: Temp focus if needed (uncomment the below if seeks don't work),
	// so far it's working without it, but keeping this here pending further testing.

    // Window original_focus = XGetInputFocus(display, &original_focus, &revert_to);
    // XSetInputFocus(display, app_data->ffplay_wid, RevertToParent, CurrentTime);
    // XFlush(display);

    XEvent event = {0};
    event.xkey.display = display;
    event.xkey.window = app_data->ffplay_wid;
    event.xkey.root = RootWindow(display, DefaultScreen(display));
    event.xkey.subwindow = None;
    event.xkey.time = CurrentTime;
    event.xkey.x = 1; event.xkey.y = 1; // Dummy pos
    event.xkey.x_root = 1; event.xkey.y_root = 1;
    event.xkey.state = 0;
    event.xkey.keycode = XKeysymToKeycode(display, keysym);
    event.xkey.same_screen = True;

    // Press
    event.type = KeyPress;
    XSendEvent(display, app_data->ffplay_wid, True, KeyPressMask, &event);
    XFlush(display);

    // Release
    event.type = KeyRelease;
    XSendEvent(display, app_data->ffplay_wid, True, KeyReleaseMask, &event);
    XFlush(display);

    // Optional: Revert focus, uncomment this too if uncommenting the other focus lines above.

    // XSetInputFocus(display, original_focus, RevertToParent, CurrentTime);
    // XFlush(display);
}

// Seek button clicked
static void on_seek_clicked(GtkButton *button, AppData *app_data) {
    const char *label = gtk_button_get_label(button);
    KeySym keysym = 0;

    if (g_strcmp0(label, "Previous chapter") == 0) keysym = XK_Page_Down;
    else if (g_strcmp0(label, "<<1m") == 0) keysym = XK_Down;
    else if (g_strcmp0(label, "1m>>") == 0) keysym = XK_Up;
    else if (g_strcmp0(label, "Next chapter") == 0) keysym = XK_Page_Up;

    if (keysym != 0) {
        send_key_to_ffplay(app_data, keysym);
    }
}

// Helper to free Entry
static void free_entry(Entry *e) {
    g_list_free_full(e->path_components, g_free);
    g_free(e->title);
    g_free(e->fullpath);
    g_free(e);
}

// Sort function (folders before files, alpha within level)
static gint compare_entries(gconstpointer a_ptr, gconstpointer b_ptr) {
    const Entry *a = a_ptr;
    const Entry *b = b_ptr;
    GString *key_a = g_string_new("");
    GList *p;
    for (p = a->path_components; p; p = p->next) {
        if (key_a->len) g_string_append_c(key_a, '/');
        g_string_append(key_a, p->data);
    }
    if (key_a->len) g_string_append_c(key_a, '/');
    g_string_append(key_a, a->title);
    GString *key_b = g_string_new("");
    for (p = b->path_components; p; p = p->next) {
        if (key_b->len) g_string_append_c(key_b, '/');
        g_string_append(key_b, p->data);
    }
    if (key_b->len) g_string_append_c(key_b, '/');
    g_string_append(key_b, b->title);
    gint result = g_strcmp0(key_a->str, key_b->str);
    g_string_free(key_a, TRUE);
    g_string_free(key_b, TRUE);
    return result;
}

void recreate_socket(AppData *app_data) {
    if (app_data->socket && GTK_IS_WIDGET(app_data->socket)) {
        gtk_widget_destroy(app_data->socket);
    }
    app_data->socket = gtk_socket_new();
    gtk_widget_set_size_request(app_data->socket, 650, 400);
    gtk_container_add(GTK_CONTAINER(app_data->socket_container), app_data->socket);
    if (GTK_IS_WIDGET(app_data->socket)) gtk_widget_hide(app_data->socket);
}

void play_video(AppData *app_data, const char *file_path, GtkTreeIter *iter) {
    // First, ensure overlays are hidden BEFORE anything else
    if (GTK_IS_WIDGET(app_data->paused_overlay)) gtk_widget_hide(app_data->paused_overlay);
    if (GTK_IS_WIDGET(app_data->stopped_overlay)) gtk_widget_hide(app_data->stopped_overlay);
    if (!app_data->socket_active) {
        gtk_container_remove(GTK_CONTAINER(app_data->paned), app_data->placeholder_pane);
        gtk_paned_pack2(GTK_PANED(app_data->paned), app_data->socket_container, TRUE, FALSE);
        gtk_widget_show_all(app_data->socket_container);
        // Force overlays hidden AGAIN after container is shown
        if (GTK_IS_WIDGET(app_data->paused_overlay)) gtk_widget_hide(app_data->paused_overlay);
        if (GTK_IS_WIDGET(app_data->stopped_overlay)) gtk_widget_hide(app_data->stopped_overlay);
        app_data->socket_active = TRUE;
        gtk_widget_queue_draw(app_data->paned);
        gtk_widget_queue_resize(app_data->paned);
    }
    if (app_data->ffplay_pid > 0) {
        if (app_data->is_paused) {
            kill(app_data->ffplay_pid, SIGCONT);
            app_data->is_paused = FALSE;
        }
        kill(app_data->ffplay_pid, SIGTERM);
        waitpid(app_data->ffplay_pid, NULL, 0);
        app_data->ffplay_pid = 0;
        app_data->is_playing = FALSE;
        if (GTK_IS_WIDGET(app_data->socket)) gtk_widget_hide(app_data->socket);
        recreate_socket(app_data);
    }
    pid_t pid = fork();
    if (pid == 0) {
        execlp("ffplay", "ffplay", "-autoexit", "-x", "650", "-y", "400", "-noborder",
               "-loglevel", "quiet", "-hide_banner",  // Prevent log spam, ffplay is extremely verbose
               "-bufsize", "128k", "-af", "atempo=1.0",
               file_path, NULL);
        exit(1);
    } else if (pid > 0) {
        app_data->ffplay_pid = pid;
        app_data->is_playing = TRUE;
        app_data->is_paused = FALSE;
        if (iter) {
            app_data->current_video_iter = *iter;
        }
        unsigned long wid = 0;
        int attempts = 80;
        for (int i = 0; i < attempts; i++) {
            usleep(100000);
            char command[512];
            FILE *fp;
            snprintf(command, sizeof(command),
                     "xwininfo -name 'ffplay' -int 2>/dev/null | grep 'Window id' | awk '{print $4}'");
            fp = popen(command, "r");
            if (fp) {
                char buf[32];
                if (fgets(buf, sizeof(buf), fp)) wid = strtoul(buf, NULL, 10);
                pclose(fp);
            }
            if (wid == 0) {
                snprintf(command, sizeof(command),
                         "xwininfo -root -tree 2>/dev/null | grep -i ffplay | head -1 | awk '{print $1}'");
                fp = popen(command, "r");
                if (fp) {
                    char buf[32];
                    if (fgets(buf, sizeof(buf), fp)) wid = strtoul(buf, NULL, 16);
                    pclose(fp);
                }
            }
            if (wid > 0) {
                usleep(50000);
                gtk_socket_add_id(GTK_SOCKET(app_data->socket), wid);
                if (GTK_IS_WIDGET(app_data->socket)) gtk_widget_show(app_data->socket);
                // Secret sauce X11 hack, set empty input shape to block mouse clicks in embedded window
                Display *display = gdk_x11_get_default_xdisplay();
                if (display) {
                    Pixmap empty_shape = XCreatePixmap(display, wid, 1, 1, 1);
                    XShapeCombineMask(display, wid, ShapeInput, 0, 0, empty_shape, ShapeSet);
                    XFreePixmap(display, empty_shape);
                    XSync(display, False);
                } else {
                    g_print("Failed to get X display for shape hack\n");
                }
                app_data->ffplay_wid = wid;  // Store for key sends
                break;
            }
        }
        if (wid == 0) g_print("Error: Could not find ffplay window ID after %d attempts\n", attempts);
    }
}

void play_video_external(AppData *app_data, const char *file_path) {
    pid_t pid = fork();
    if (pid == 0) {
        // Launch ffplay standalone, no size constraints, autoexit when done, keyboard hints in title, no log spam
        execlp("ffplay", "ffplay", "-autoexit", "-loglevel", "quiet", "-hide_banner",
	           "-window_title", "Keyboard controls:  f=fullscreen,  p=pause/play,  q=quit",
               file_path, NULL);
        exit(1);
    } else if (pid > 0) {
    } else {
        g_print("Failed to fork for external ffplay\n");
    }
}

gboolean check_video_end(gpointer user_data) {
    AppData *app_data = (AppData *)user_data;
    if (app_data->ffplay_pid <= 0 || !app_data->is_playing || app_data->is_paused) {
        return TRUE;
    }
    int status;
    pid_t result = waitpid(app_data->ffplay_pid, &status, WNOHANG);
    if (result == app_data->ffplay_pid) {
        app_data->ffplay_pid = 0;
        app_data->is_playing = FALSE;
        app_data->is_paused = FALSE;
        if (GTK_IS_WIDGET(app_data->socket)) gtk_widget_hide(app_data->socket);
        if (GTK_IS_WIDGET(app_data->paused_overlay)) gtk_widget_hide(app_data->paused_overlay);
        if (GTK_IS_WIDGET(app_data->stopped_overlay)) gtk_widget_show(app_data->stopped_overlay);
        recreate_socket(app_data);
    }
    return TRUE;
}

void on_play_clicked(GtkButton *button, AppData *app_data) {
    GtkTreeView *video_tree = g_object_get_data(G_OBJECT(button), "video_tree");
    GtkTreeSelection *selection = gtk_tree_view_get_selection(video_tree);
    GtkTreeIter iter;
    GtkTreeModel *model;
    if (!gtk_tree_selection_get_selected(selection, &model, &iter)) {
        return; // nothing selected
    }
    gchar *file_path = NULL;
    gtk_tree_model_get(model, &iter, 1, &file_path, -1);
    if (!file_path || file_path[0] == '\0') {
        g_free(file_path);
        return; // clicked a folder - do nothing
    }
    // Visually indicate selection
    gtk_tree_selection_select_iter(selection, &iter);
    play_video(app_data, file_path, &iter);
    g_free(file_path);
}

void on_external_play_clicked(GtkButton *button, AppData *app_data) {
    GtkTreeView *video_tree = g_object_get_data(G_OBJECT(button), "video_tree");
    GtkTreeSelection *selection = gtk_tree_view_get_selection(video_tree);
    GtkTreeIter iter;
    GtkTreeModel *model;
    if (!gtk_tree_selection_get_selected(selection, &model, &iter)) {
        return; // nothing selected
    }
    gchar *file_path = NULL;
    gtk_tree_model_get(model, &iter, 1, &file_path, -1);
    // Guard: ignore if it's a folder (no path)
    if (!file_path || file_path[0] == '\0') {
        g_free(file_path);
        return; // clicked a folder - do nothing, no need to stop anything
    }
    // If something is currently playing in the embedded player - stop it first
    if (app_data->ffplay_pid > 0) {
        on_stop_clicked(NULL, app_data); // reuse existing stop logic
    }
    // Now it's safe to launch external
    play_video_external(app_data, file_path);
    g_free(file_path);
}

void on_pause_clicked(GtkButton *button, AppData *app_data) {
    if (app_data->ffplay_pid > 0) {
        if (app_data->is_playing && !app_data->is_paused) {
            kill(app_data->ffplay_pid, SIGSTOP);
            app_data->is_playing = FALSE;
            app_data->is_paused = TRUE;
            if (GTK_IS_WIDGET(app_data->paused_overlay)) {
                gtk_widget_show(app_data->paused_overlay);
            }
            if (GTK_IS_WIDGET(app_data->stopped_overlay)) {
                gtk_widget_hide(app_data->stopped_overlay);
            }
        } else if (app_data->is_paused) {
            kill(app_data->ffplay_pid, SIGCONT);
            app_data->is_playing = TRUE;
            app_data->is_paused = FALSE;
            if (GTK_IS_WIDGET(app_data->paused_overlay)) gtk_widget_hide(app_data->paused_overlay);
        }
    }
}

void on_stop_clicked(GtkButton *button, AppData *app_data) {
    if (app_data->ffplay_pid > 0) {
        if (app_data->is_paused) {
            kill(app_data->ffplay_pid, SIGCONT);
            app_data->is_paused = FALSE;
        }
        kill(app_data->ffplay_pid, SIGTERM);
        waitpid(app_data->ffplay_pid, NULL, 0);
        app_data->ffplay_pid = 0;
        app_data->is_playing = FALSE;
        if (GTK_IS_WIDGET(app_data->socket)) gtk_widget_hide(app_data->socket);
        if (GTK_IS_WIDGET(app_data->paused_overlay)) gtk_widget_hide(app_data->paused_overlay);
        if (GTK_IS_WIDGET(app_data->stopped_overlay)) gtk_widget_show(app_data->stopped_overlay);
        recreate_socket(app_data);
    }
}

void on_rebuild_clicked(GtkButton *button, AppData *app_data) {
    if (system(SCRAPER_CMD) != 0) {
        g_print("Warning: Failed to run scraper %s\n", SCRAPER_CMD);
    }
    gtk_tree_store_clear(app_data->video_store);
    populate_treeview(app_data);
    app_data->is_playing = FALSE;
    app_data->is_paused = FALSE;
    if (GTK_IS_WIDGET(app_data->socket)) gtk_widget_hide(app_data->socket);
    if (GTK_IS_WIDGET(app_data->paused_overlay)) gtk_widget_hide(app_data->paused_overlay);
    if (GTK_IS_WIDGET(app_data->stopped_overlay)) gtk_widget_show(app_data->stopped_overlay);
    recreate_socket(app_data);
}

void populate_treeview(AppData *app_data) {
    const char *home = getenv("HOME");
    if (!home) {
        g_print("Error: Could not determine home directory\n");
        return;
    }
    gchar *db_path = g_strdup_printf("%s/.local/share/vsvp/vsvp_db", home);

    FILE *db = fopen(db_path, "r");
    if (!db) {
        g_print("Error: Could not open database %s\n", db_path);
        g_free(db_path);
        return;
    }

    gtk_tree_store_clear(app_data->video_store);
    char line[1024];
    if (!fgets(line, sizeof(line), db)) { // skip header
        fclose(db);
        g_free(db_path);
        return;
    }
    GList *entries = NULL;
    while (fgets(line, sizeof(line), db)) {
        char path_str[512] = {0};
        char title[256] = {0};
        char fullpath[768] = {0};
        if (sscanf(line, "%511[^|]|%255[^|]|%767[^\n]", path_str, title, fullpath) != 3)
            continue;
        GList *components = NULL;
        if (strlen(path_str) > 0) {
            char *saveptr;
            char *token = strtok_r(path_str, "/", &saveptr);
            while (token) {
                components = g_list_append(components, g_strdup(token));
                token = strtok_r(NULL, "/", &saveptr);
            }
        }
        Entry *e = g_new0(Entry, 1);
        e->path_components = components;
        e->title = g_strdup(title);
        e->fullpath = g_strdup(fullpath);
        entries = g_list_prepend(entries, e);
    }
    fclose(db);
    // Sort once - folders before files + alpha within same level
    entries = g_list_sort(entries, compare_entries);
    // Build the actual tree
    GHashTable *folder_iters = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
    GList *l;
    for (l = entries; l != NULL; l = l->next) {
        Entry *e = l->data;
        GtkTreeIter *parent = NULL;
        GtkTreeIter iter;
        GString *current_path = g_string_new("");
        GList *comp;
        for (comp = e->path_components; comp != NULL; comp = comp->next) {
            gchar *folder_name = comp->data;
            if (current_path->len > 0)
                g_string_append_c(current_path, '/');
            g_string_append(current_path, folder_name);
            GtkTreeIter *existing = g_hash_table_lookup(folder_iters, current_path->str);
            if (existing) {
                parent = existing;
                continue;
            }
            // New folder node
            if (parent) {
                gtk_tree_store_append(app_data->video_store, &iter, parent);
            } else {
                gtk_tree_store_append(app_data->video_store, &iter, NULL);
            }
            gtk_tree_store_set(app_data->video_store, &iter,
                               0, folder_name,
                               1, NULL,
                               -1);
            // Store a copy of the iter
            GtkTreeIter *stored = g_new(GtkTreeIter, 1);
            *stored = iter;
            g_hash_table_insert(folder_iters, g_strdup(current_path->str), stored);
            parent = stored;
        }
        // Finally the file leaf
        gtk_tree_store_append(app_data->video_store, &iter, parent);
        gtk_tree_store_set(app_data->video_store, &iter,
                           0, e->title,
                           1, e->fullpath,
                           -1);
    }
    // Cleanup
    g_list_free_full(entries, (GDestroyNotify)free_entry);
    g_hash_table_destroy(folder_iters);
    g_free(db_path);
}

// Hide treeview and borders when maximized for quasi-theater-mode
gboolean on_window_state_changed(GtkWidget *widget, GdkEventWindowState *event, gpointer user_data) {
    AppData *app_data = (AppData *)user_data;
    if (event->changed_mask & GDK_WINDOW_STATE_MAXIMIZED) {
        gboolean is_maximized = (event->new_window_state & GDK_WINDOW_STATE_MAXIMIZED);
        // Playlist hide/show
        if (is_maximized) {
            gtk_widget_hide(app_data->video_tree);
        } else {
            gtk_widget_show(app_data->video_tree);
        }
        // Window border padding
        gtk_container_set_border_width(GTK_CONTAINER(widget), is_maximized ? 0 : 10);
        // Get vbox from window
        GtkWidget *vbox = gtk_bin_get_child(GTK_BIN(widget));
        if (GTK_IS_BOX(vbox)) {
            // Zero spacing (covers the tiny 1px if any)
            gtk_box_set_spacing(GTK_BOX(vbox), is_maximized ? 0 : 1);
            // KEY: Dynamically change paned's packing padding
            gtk_box_set_child_packing(GTK_BOX(vbox),
                                      app_data->paned,
                                      TRUE, // expand (keep as is)
                                      TRUE, // fill (keep as is)
                                      is_maximized ? 0 : 10, // padding
                                      GTK_PACK_START); // pack_type (keep as is)
        } else {
            g_warning("Main vbox not found or not a GtkBox!");
        }
    }
    return FALSE;
}

void on_window_destroy(GtkWidget *widget, AppData *app_data) {
    if (app_data->ffplay_pid > 0) {
        if (app_data->is_paused) {
            kill(app_data->ffplay_pid, SIGCONT);
            app_data->is_paused = FALSE;
        }
        kill(app_data->ffplay_pid, SIGTERM);
        waitpid(app_data->ffplay_pid, NULL, 0);
        app_data->ffplay_pid = 0;
        app_data->is_playing = FALSE;
    }
    gtk_main_quit();
}

int main(int argc, char *argv[]) {
    gtk_init(&argc, &argv);
    if (!getenv("HOME")) {
        g_print("Error: Could not determine home directory\n");
        return 1;
    }
    AppData app_data = {0};
    app_data.video_store = gtk_tree_store_new(2, G_TYPE_STRING, G_TYPE_STRING); // 0 = Title, 1 = Fullpath (NULL for folders)
    app_data.is_playing = FALSE;
    app_data.is_paused = FALSE;
    app_data.socket_active = FALSE;

    g_timeout_add(500, check_video_end, &app_data);
    
    GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(window), "Vuu-do Simple Video Player");
    gtk_window_set_default_size(GTK_WINDOW(window), 800, 600);
    gtk_window_set_hide_titlebar_when_maximized(GTK_WINDOW(window), FALSE);
    gtk_window_set_icon_name(GTK_WINDOW(window), "applications-multimedia");
    g_signal_connect(window, "destroy", G_CALLBACK(on_window_destroy), &app_data);
    g_signal_connect(window, "window-state-event", G_CALLBACK(on_window_state_changed), &app_data);
    gtk_container_set_border_width(GTK_CONTAINER(window), 10);
    
    GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 1);
    gtk_container_add(GTK_CONTAINER(window), vbox);
    
    app_data.paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
    gtk_box_pack_start(GTK_BOX(vbox), app_data.paned, TRUE, TRUE, 10);
    
    app_data.video_tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(app_data.video_store));
    gtk_tree_view_set_model(GTK_TREE_VIEW(app_data.video_tree), GTK_TREE_MODEL(app_data.video_store));
    GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
    gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(app_data.video_tree), -1, "Videos", renderer, "text", 0, NULL);
    gtk_paned_pack1(GTK_PANED(app_data.paned), app_data.video_tree, FALSE, FALSE);
    gtk_widget_set_size_request(app_data.video_tree, 200, -1);
    
    // Placeholder pane (initial state)
    app_data.placeholder_pane = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    gtk_widget_set_name(app_data.placeholder_pane, "placeholder-pane");
    app_data.placeholder_label = gtk_label_new("Select a video");
    gtk_widget_set_margin_start(app_data.placeholder_label, 10);
    gtk_widget_set_margin_end(app_data.placeholder_label, 10);
    gtk_widget_set_margin_top(app_data.placeholder_label, 10);
    gtk_widget_set_margin_bottom(app_data.placeholder_label, 10);
    gtk_box_pack_start(GTK_BOX(app_data.placeholder_pane), app_data.placeholder_label, TRUE, TRUE, 0);    
    GtkCssProvider *provider = gtk_css_provider_new();
    gtk_css_provider_load_from_data(provider,
        "#placeholder-pane {"
        " background-color: @theme_base_color;"
        " background-image: none;"
        "}"
        "#placeholder-pane label {"
        " color: @theme_text_color;"
        "}",
        -1, NULL);
    GtkStyleContext *pane_ctx = gtk_widget_get_style_context(app_data.placeholder_pane);
    gtk_style_context_add_provider(pane_ctx, GTK_STYLE_PROVIDER(provider),
                                   GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
    GtkStyleContext *label_ctx = gtk_widget_get_style_context(app_data.placeholder_label);
    gtk_style_context_add_provider(label_ctx, GTK_STYLE_PROVIDER(provider),
                                   GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
    gtk_paned_pack2(GTK_PANED(app_data.paned), app_data.placeholder_pane, TRUE, FALSE);
    gtk_widget_show_all(app_data.placeholder_pane);
    
    // Socket container as overlay host
    app_data.socket_container = gtk_overlay_new();
    gtk_widget_set_name(app_data.socket_container, "socket-container");
    
    // Main child: socket
    app_data.socket = gtk_socket_new();
    gtk_widget_set_size_request(app_data.socket, 650, 400);
    gtk_container_add(GTK_CONTAINER(app_data.socket_container), app_data.socket);
    gtk_widget_hide(app_data.socket);
    
    // Paused overlay (semi-transparent "Paused")
    app_data.paused_overlay = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    gtk_widget_set_halign(app_data.paused_overlay, GTK_ALIGN_FILL);
    gtk_widget_set_valign(app_data.paused_overlay, GTK_ALIGN_FILL);
    GtkWidget *paused_label = gtk_label_new("Paused");
    gtk_box_pack_start(GTK_BOX(app_data.paused_overlay), paused_label, TRUE, TRUE, 0);
    gtk_widget_set_name(app_data.paused_overlay, "paused-overlay");
    gtk_overlay_add_overlay(GTK_OVERLAY(app_data.socket_container), app_data.paused_overlay);
    gtk_widget_hide(app_data.paused_overlay);
    
    // Stopped overlay (opaque, "Select a video")
    app_data.stopped_overlay = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    gtk_widget_set_halign(app_data.stopped_overlay, GTK_ALIGN_FILL);
    gtk_widget_set_valign(app_data.stopped_overlay, GTK_ALIGN_FILL);
    GtkWidget *stopped_label = gtk_label_new("Select a video");
    gtk_box_pack_start(GTK_BOX(app_data.stopped_overlay), stopped_label, TRUE, TRUE, 0);
    gtk_widget_set_name(app_data.stopped_overlay, "stopped-overlay");
    gtk_overlay_add_overlay(GTK_OVERLAY(app_data.socket_container), app_data.stopped_overlay);
    gtk_widget_hide(app_data.stopped_overlay); // Start hidden
    
    // CSS for overlays and container
    GtkCssProvider *overlay_provider = gtk_css_provider_new();
    gtk_css_provider_load_from_data(overlay_provider,
        "#paused-overlay {"
        " background-color: alpha(@theme_base_color, 0.50);"
        " background-image: none;"
        "}"
        "#paused-overlay label {"
        " color: @theme_fg_color;"
        " font-weight: bold;"
        "}"
        "#stopped-overlay {"
        " background-color: @theme_base_color;"
        " background-image: none;"
        "}"
        "#stopped-overlay label {"
        " color: @theme_text_color;"
        " font-weight: normal;"
        "}"
        "#socket-container {"
        " background-color: @theme_base_color;"
        " background-image: none;"
        "}",
        -1, NULL);
    gtk_style_context_add_provider(gtk_widget_get_style_context(app_data.paused_overlay),
                                   GTK_STYLE_PROVIDER(overlay_provider),
                                   GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
    gtk_style_context_add_provider(gtk_widget_get_style_context(app_data.stopped_overlay),
                                   GTK_STYLE_PROVIDER(overlay_provider),
                                   GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
    gtk_style_context_add_provider(gtk_widget_get_style_context(app_data.socket_container),
                                   GTK_STYLE_PROVIDER(overlay_provider),
                                   GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
    g_object_unref(overlay_provider);
    g_object_unref(provider);

	//CSS for bottom buttons
	GtkCssProvider *button_provider = gtk_css_provider_new();
	gtk_css_provider_load_from_data(button_provider,
    "#control-bar button {"
    "  border-width: 1px;"
    "  border-style: solid;"
    "  border-color: #333333;"
    "  box-shadow: none;"
    "  border-radius: 4px;"
    "}",
    -1, NULL);

	gtk_style_context_add_provider_for_screen(gdk_screen_get_default(),
                                         GTK_STYLE_PROVIDER(button_provider),
                                         GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);    

    // Bottom controls 
    app_data.control_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_box_pack_start(GTK_BOX(vbox), app_data.control_box, FALSE, FALSE, 0);
    gtk_widget_set_margin_top(app_data.control_box, 5);
    gtk_widget_set_margin_bottom(app_data.control_box, 5);
    gtk_widget_set_name(app_data.control_box, "control-bar");
    app_data.play_button = gtk_button_new_with_label("Play");
    app_data.pause_button = gtk_button_new_with_label("Pause");
    app_data.stop_button = gtk_button_new_with_label("Stop");
    GtkWidget *seek_back10 = gtk_button_new_with_label("Previous chapter");
    GtkWidget *seek_back1 = gtk_button_new_with_label("<<1m");
    GtkWidget *seek_fwd1 = gtk_button_new_with_label("1m>>");
    GtkWidget *seek_fwd10 = gtk_button_new_with_label("Next chapter");
    gtk_widget_set_tooltip_text(seek_back10, "Go back 1 chapter");
    gtk_widget_set_tooltip_text(seek_back1, "Go back 1 minute");
    gtk_widget_set_tooltip_text(seek_fwd1, "Go forward 1 minute");
    gtk_widget_set_tooltip_text(seek_fwd10, "Go forward 1 chapter");
    app_data.rebuild_button = gtk_button_new_with_label("Re-scan Videos");
    gtk_widget_set_tooltip_text(app_data.rebuild_button, "Re-scan the ~/Videos folder after adding new media");
    app_data.external_play_button = gtk_button_new_with_label("Fullscreen window");
    gtk_widget_set_tooltip_text(app_data.external_play_button,
        "Play video in a separate fullscreen-capable window.\n"
        "Keyboard shortcuts only for that window :\n\n"
        " f  -  toggle fullscreen\n"
        " p  -  toggle pause/play\n"
        " m  -  toggle mute\n\n"
        " left/right  -  seek ±10s\n"
        " up/down  -  seek ±1min\n"
        " pg up/pg dn  -  seek ±1 chapter\n\n"
        " q or ESC  -  quit and close window");
       
    gtk_box_pack_start(GTK_BOX(app_data.control_box), app_data.play_button, FALSE, FALSE, 5);
    gtk_box_pack_start(GTK_BOX(app_data.control_box), app_data.pause_button, FALSE, FALSE, 5);
    gtk_box_pack_start(GTK_BOX(app_data.control_box), app_data.stop_button, FALSE, FALSE, 5);
    gtk_box_pack_start(GTK_BOX(app_data.control_box), seek_back10, FALSE, FALSE, 5);
    gtk_box_pack_start(GTK_BOX(app_data.control_box), seek_back1, FALSE, FALSE, 5);
    gtk_box_pack_start(GTK_BOX(app_data.control_box), seek_fwd1, FALSE, FALSE, 5);
    gtk_box_pack_start(GTK_BOX(app_data.control_box), seek_fwd10, FALSE, FALSE, 5);
    gtk_box_pack_start(GTK_BOX(app_data.control_box), app_data.rebuild_button, FALSE, FALSE, 5);
    gtk_box_pack_start(GTK_BOX(app_data.control_box), app_data.external_play_button, FALSE, FALSE, 5);
   
    g_object_set_data(G_OBJECT(app_data.play_button), "video_tree", app_data.video_tree);
    g_object_set_data(G_OBJECT(app_data.external_play_button), "video_tree", app_data.video_tree);
   
    g_signal_connect(app_data.play_button, "clicked", G_CALLBACK(on_play_clicked), &app_data);
    g_signal_connect(app_data.pause_button, "clicked", G_CALLBACK(on_pause_clicked), &app_data);
    g_signal_connect(app_data.stop_button, "clicked", G_CALLBACK(on_stop_clicked), &app_data);
    g_signal_connect(seek_back10, "clicked", G_CALLBACK(on_seek_clicked), &app_data);
    g_signal_connect(seek_back1, "clicked", G_CALLBACK(on_seek_clicked), &app_data);
    g_signal_connect(seek_fwd1, "clicked", G_CALLBACK(on_seek_clicked), &app_data);
    g_signal_connect(seek_fwd10, "clicked", G_CALLBACK(on_seek_clicked), &app_data);
    g_signal_connect(app_data.rebuild_button, "clicked", G_CALLBACK(on_rebuild_clicked), &app_data);
    g_signal_connect(app_data.external_play_button, "clicked", G_CALLBACK(on_external_play_clicked), &app_data);
   
    populate_treeview(&app_data);
    gtk_widget_show_all(window);
    gtk_main();
    return 0;
}

#123 Off-topic » XFCE is building a new WM and compositor for Wayland » 2026-02-05 16:10:08

greenjeans
Replies: 17

Building from the ground up they say, using Rust. Not dumping X11 though...for now.
https://blog.xfce.org/

"This initiative will utilize a significant portion of the project’s donated funds"

Do tell. wink

What say ye mousers about this? I don't use it myself so I don't have a dog in this hunt.

#125 Re: Desktop and Multimedia » [SOLVED] Devuan Excalibur install and Pipewire. » 2026-02-04 22:14:17

Interesting issue, and made more difficult by being intermittent. Sorry I can't be more help, I have zero experience with KDE.

I can tell you this factoid: When using AlsaTune on a pure alsa system, I have every control available for my soundcard. Some 7-8 volume sliders and a couple of toggles. But if I install it on a Mate system (Mate has an onboard mixer too) and try to use it, EVERYTHING but the main volume slider is gone completely. The onboard mixers grab control right at boot-up, and they are NOT sophisticated and you lose a lot of control. That's why in most cases people throw Pulse or Pipe at it in hopes that it all works out. Mate's onboard mixer is the main reason i'm not doing any more work with Mate, that and relentless Caja bugs.

Board footer

Forum Software