LinuxHelm es una aplicación de escritorio para Linux construida con Kotlin y
Compose Multiplatform. Funciona como un panel de utilidades del sistema: muestra
información del equipo (gestor de paquetes, discos y su estado de salud), permite
añadir y ejecutar scripts desde una interfaz gráfica e incluye de serie dos
herramientas del sistema: ulxCheckDirtyDrives (repara particiones NTFS "dirty"
dejadas por Windows) y ulxFixDiskOwnership (corrige discos montados con propietario
root:root).
- Multiidioma: English, Español, 中文, Português (inglés por defecto).
- Tema claro y oscuro, con persistencia entre sesiones.
- Los scripts y ajustes se guardan en SQLite (
~/.config/ulx/LinuxHelm/lh.db), así que sobreviven a reinicios. - Escala de interfaz ajustable (100–200%) para pantallas HiDPI, aplicada en vivo y persistida.
- Características
- Requisitos
- Compilar y ejecutar
- Cómo usar la aplicación
- Herramientas incluidas
- Apartado técnico
- Estructura del proyecto
- Tests
| Pantalla | Qué ofrece |
|---|---|
| Principal | Menú de acceso con el logo de la app. |
| Información | Gestor de paquetes detectado (con enlace a su wiki) y lista de discos con tamaño, uso y estado de salud (comprobación rápida y profunda). |
| Herramientas | Lanzador de scripts: añade .sh, .jar, .py, .AppImage, binarios… y ejecútalos en una terminal del sistema. |
| Configuración | Selector de idioma, tema (claro/oscuro) y escala de interfaz (100–200%), guardados al instante. |
Detalles útiles:
- Los scripts no se mueven ni se copian: solo se guarda la ruta que elegiste.
- Al ejecutar se abre una terminal real del escritorio: puedes responder prompts,
escribir la contraseña de
sudoy ver la salida en vivo. - La terminal se queda abierta al terminar ("Pulsa una tecla para cerrar") para que puedas leer el resultado, y avisa si el script salió con error.
- La app detecta si un script ya no existe o no tiene permiso de ejecución (+x) y te avisa antes de que falle.
- Las herramientas incluidas de serie no se pueden borrar desde la interfaz.
Para usar la app (Linux):
- Cualquier escritorio con un emulador de terminal instalado
(
gnome-terminal,konsole,xfce4-terminal,kitty,xterm…; se prueba primerox-terminal-emulator, el estándar de Debian/Ubuntu). lsblk(deutil-linux, ya viene en casi todas las distros) para la pantalla de Información.- Opcional, para la comprobación profunda de discos:
pkexec(policykit) y las herramientas del sistema de archivos correspondientes (e2fsprogs,ntfs-3g,dosfstools,btrfs-progs). - Para la herramienta incluida
ulxCheckDirtyDrives: paquetentfs-3g(sudo apt install ntfs-3g). - Opcional, para el diálogo nativo de selección de archivo:
kdialog(KDE) ozenity(GNOME y otros). Si no hay ninguno, se usa el diálogo incluido en la app.
Para compilar desde el código:
- JDK 17 o superior (el plugin de toolchains de Gradle lo resuelve automáticamente si falta).
- No hace falta instalar Kotlin ni Compose: todo viene por Gradle.
# Ejecución normal
./gradlew :desktopApp:run
# Hot reload (recarga al guardar, ideal para desarrollo)
./gradlew :desktopApp:hotRun --auto
# Tests de escritorio
./gradlew :shared:jvmTest
# Empaquetar para el sistema actual (en Linux genera un .deb)
./gradlew :desktopApp:packageDistributionForCurrentOS
# Empaquetar todos los formatos de Linux de una vez (ver empaquetar.sh)
./empaquetar.shLa script empaquetar.sh compila en un solo paso todos los formatos —uber-jar,
.deb, .rpm, .tar.gz y .AppImage—, verifica que cada fichero se generó de verdad
y los deja en la carpeta dist/. Si falta alguna herramienta opcional del sistema
(rpmbuild, appimagetool), ese formato se omite con un aviso y el resto se compila
igual.
Los formatos de empaquetado configurados son .deb y .rpm
(desktopApp/build.gradle.kts), más las tareas propias packageTarGz y
packageAppImage. El paquete es autocontenido: trae su propio JRE y las herramientas
incluidas viajan dentro del JAR (ver Apartado técnico).
El nombre del paquete es Linux Helm: Debian y rpm no admiten mayúsculas ni
espacios en el nombre del paquete, así que jpackage lo sanea a linux-helm
(fichero linux-helm_1.0.0_amd64.deb, instalación en /opt/linux-helm, lanzador
/opt/linux-helm/bin/Linux Helm), mientras que el menú de aplicaciones sí muestra
"Linux Helm".
Al abrir la app ves el menú con tres botones: Información, Herramientas y Configuración. Todas las pantallas secundarias tienen un botón Volver arriba a la izquierda.
- Arriba se muestra el gestor de paquetes detectado (apt, dnf, pacman, zypper o emerge). El botón Ver sitio abre su documentación en el navegador.
- Debajo, el expander "Unidades de almacenamiento detectadas" lista las particiones
reales del equipo (estilo Dolphin: oculta la partición EFI, swap, RAID y las
particiones padre, mostrando solo las hijas). Para cada una verás:
- Nombre (la etiqueta del volumen, o "Unidad interna/extraíble de X GiB" si no tiene).
- Punto de montaje, tamaño total y espacio usado.
- Un indicador de estado con semáforo: verde (sin errores), amarillo (aviso, p. ej. montada en solo lectura), gris (sin comprobar) o rojo (errores).
- Comprobar a fondo: lanza una comprobación de solo lectura con privilegios
(aparecerá el diálogo gráfico de contraseña de
pkexec). Según el sistema de archivos:ext4/ext3/ext2: lee el estado del journal (dumpe2fs -h).ntfs:ntfs-3g.probe --readonly(detecta dirty e hibernación de Windows).vfat:fsck.fat -n(detecta el dirty bit).btrfs:btrfs device stats(contadores de errores de E/S).
- La comprobación rápida (sin root) ya se hace al abrir la pantalla: mira los
contadores de errores del kernel en
/sys/fs/...para ext4/btrfs y si alguna unidad está montada en solo lectura. Si no puede verificar nada, lo pone como "Sin comprobar" (nunca marca como correcto algo que no pudo comprobar).
Esta es la pantalla central de la app: tu lanzador de scripts.
- Pulsa Añadir script y elige el archivo con el diálogo nativo del escritorio
(el Qt de KDE si tienes
kdialog, el GTK de GNOME si tieneszenity, o el incluido en la app en su defecto). Se admiten.sh,.jar,.py,.AppImage,.run,.biny binarios sin extensión: el diálogo nativo no filtra por tipo, así que todos se ven. - En el diálogo escribe el Nombre (viene rellenado con el del archivo) y, si quieres, un Sitio web opcional (p. ej. la página del proyecto).
- En la lista pulsas Ejecutar: se abre una ventana de terminal con el script funcionando. Cuando termine, la terminal se queda abierta para que leas la salida; si el script falló, muestra el código de salida.
- Ver sitio (solo si pusiste web) abre la página en el navegador.
- La papelera quita el script de la lista. Ojo: borra el registro, no el archivo en disco; puedes volver a añadirlo cuando quieras.
Cómo se lanza cada tipo de archivo:
| Archivo | Cómo se ejecuta | ¿Necesita +x? |
|---|---|---|
.sh |
bash archivo.sh |
No |
.jar |
java -jar archivo.jar |
No |
.py |
python3 archivo.py |
No |
Otros (binarios, .AppImage…) |
Se lanzan directamente | Sí |
La app trabaja con el directorio de trabajo del propio script (su carpeta), así que los scripts que usan rutas relativas funcionan igual que si los lanzaras a mano.
Si un archivo desaparece de disco o le falta el permiso +x, la fila muestra un aviso en rojo y el botón Ejecutar queda deshabilitado.
Tres controles dentro de una tarjeta:
- Tema: Claro / Oscuro. El cambio es en vivo (la UI se repinta al instante) y persiste.
- Idioma: English / Español / 中文 / Português. Se aplica al instante en todos los textos de la interfaz y también persiste.
- Escala: 100% / 125% / 150% / 175% / 200%. Multiplica toda la interfaz (y el tamaño de la ventana) por el factor elegido; útil en pantallas HiDPI. También cambia en vivo.
Los tres ajustes se guardan en la base de datos; la próxima vez que abras la app ya saldrán aplicados (sin parpadeo del tema/idioma por defecto).
La pestaña Herramientas trae dos herramientas de serie, sin papelera (no se pueden borrar):
ulxCheckDirtyDrives: comprueba y repara particiones NTFS marcadas como dirty o hibernadas por Windows (hiberfil.sys/ arranque rápido). Pidesudoel propio script. Documentación:README.md(English).ulxFixDiskOwnership: detecta discos/particiones secundarios montados con propietarioroot:root(típico en/mnt/...) y, si lo confirmas, se lo asigna a tu usuario. Se ejecuta con tu usuario normal (el script rechaza correr como root). Documentación:README.md(English).
Ambas viven en ulxTools/ y la app las despliega y registra al arrancar (ver
Apartado técnico). Para incluir una herramienta nueva basta con
crear su subcarpeta con ejecutable y metadata.json: instrucciones en
ulxTools/README.md.
| Componente | Versión / elección |
|---|---|
| Kotlin | 2.4.20 |
| Compose Multiplatform | 1.12.1 |
| Material 3 | 1.12.0-alpha03 |
| kotlinx-coroutines | 1.11.0 (con coroutines-swing en desktop) |
| SQLite | org.xerial:sqlite-jdbc 3.47.1.0 (JDBC puro) |
| Arquitectura | Módulo shared (KMP, solo target JVM) + desktopApp (JVM) |
Aunque el proyecto es Kotlin Multiplatform, hoy solo existe el target JVM/Desktop:
shared contiene el código común y desktopApp el punto de entrada.
LinuxHelm/
├── desktopApp/ # Punto de entrada JVM (main.kt) y empaquetado nativo
├── shared/
│ └── src/commonMain/kotlin/com/example/linuxhelm/
│ ├── App.kt # Raíz: tema + navegación animada
│ ├── ui/
│ │ ├── compose/
│ │ │ ├── Screen.kt # enum Pantalla + BotonVolver
│ │ │ ├── ScreenPrincipal.kt # Menú principal
│ │ │ ├── ScreenInfo.kt # Discos, paquetes, chequeos
│ │ │ ├── ScreenTools.kt # Lanzador de scripts
│ │ │ └── ScreenConfiguracion.kt # Idioma, tema y escala
│ │ ├── ulxComponentes/ # MenuSelector (desplegable genérico de ajustes)
│ │ └── ulxLib/ # Infraestructura (ver abajo)
│ └── composeResources/ # logo, etc.
├── ulxTools/ # Herramientas del sistema (ver ulxTools/README.md)
└── gradle/ # Version catalog (libs.versions.toml)
El estado de navegación vive en la raíz (App.kt) como un mutableStateOf del enum
Pantalla { Principal, Info, Tools, Configuracion }. AnimatedContent transiciona
entre pantallas (entra desde la derecha con fade, sale hacia la izquierda). La pantalla
principal recibe onNavigate, las secundarias solo onVolver: así ninguna pantalla
secundaria sabe a qué pantalla se devuelve, el flujo se controla todo desde la raíz.
Todo el estado persistente usa una única BD SQLite en
~/.config/ulx/LinuxHelm/lh.db (estándar XDG para configuración), gestionada por el
objeto ulxDB (JDBC puro, todas las llamadas bloqueantes → siempre desde Dispatchers.IO).
Se eligió SQLite en lugar de JSON por la escritura atómica (no puede quedarse un fichero corrupto a mitad si la app se cierra en mal momento) y por tener una única fuente de verdad.
Tablas:
scripts (id, name, description, website, path UNIQUE, deletable)
settings (key PRIMARY KEY, value)scripts: los scripts del usuario (deletable = 1) y las herramientas de serie (deletable = 0, la UI no muestra papelera y elDELETEpropio también lo comprueba, doble protección).pathconUNIQUE+ON CONFLICT DO UPDATEhace que "re-añadir" el mismo archivo sirva para editar nombre/web sin pantalla de edición aparte.settings: clave-valor con ajustes (lang,theme,escala). Se guardan claves estables ("dark","Español"…), nunca etiquetas traducidas, para que cambiar de idioma no rompa la interpretación del valor guardado.- Hay migración manual de columnas (
PRAGMA table_info+ALTER TABLE) porqueCREATE TABLE IF NOT EXISTSno añade columnas a tablas existentes.
Cada herramienta vive en ulxTools/<nombre>/ con su ejecutable y un metadata.json
(obligatorio: name, description, website). shared/build.gradle.kts copia cada
subcarpeta dentro del JAR como recurso bajo el prefijo ULXTools/
(jvmProcessResources). En runtime, ulxDB.seedSystemTools():
- Extrae el árbol de recursos a una carpeta de preparación
(
~/.config/ulx/LinuxHelm/.ulx-staging). - Descubre las herramientas válidas (ejecutable +
metadata.json; la principal es el fichero con el nombre de la carpeta). - Copia solo los ejecutables a
~/.config/ulx/LinuxHelm/tools/(plano, con+x). - Las registra en la BD con
deletable = 0y sus metadatos (upsert, idempotente). - Poda las herramientas que ya no existan y limpia la antigua carpeta
ULX Tools/de versiones previas.
Se redespliega en cada arranque, así que al actualizar la app las herramientas
quedan siempre en su última versión. Al viajar en el classpath, funciona igual con
run, .deb, AppImage o tar.gz, sin necesitar la carpeta ulxTools/ en disco. Cómo
añadir una herramienta nueva: ulxTools/README.md.
ulxScripts.kt construye el comando según la extensión (bash / java -jar /
python3 / ejecución directa) y lo lanza en una terminal del sistema con
ProcessBuilder. ¿Por qué terminal y no un proceso en segundo plano? Para que el
usuario pueda interactuar: prompts, stdin y la contraseña de sudo (que pide la propia
terminal).
- Lista de terminales conocidas de mayor a menor probabilidad, con dos estilos de
invocación: quienes reciben el comando como cadena tras
-e(gnome-terminal, xfce4-terminal…) y quienes reciben argv separado (konsole, xterm, kitty…). - Se busca en el
PATHsin lanzar subprocess; si no hay ninguna, la UI avisa. - El comando interior encadena: lanzar → capturar
$?→ mostrar código de salida si hubo error →read -n1para mantener la terminal abierta. Toda la línea va conshellQuote(comillas simples) para que rutas con espacios o$no se rompan. - Directorio de trabajo = carpeta del archivo.
pickScriptFile() (ulxScripts.kt) prueba, en orden:
kdialog --getopenfilename(KDE): el diálogo Qt nativo, con el mismo look que Dolphin.zenity --file-selection(GNOME y otros): el diálogo GTK nativo.JFileChoosercon aspecto GTK y tamaño relativo a la pantalla (último recurso).
kdialog y zenity son procesos aparte: la respuesta vuelve por su stdout (la ruta elegida) y su código de salida (0 = aceptado, 1 = cancelado), sin D-Bus ni señales. Cancelar nunca abre el siguiente diálogo.
Por qué no el XDG Desktop Portal (
gdbus+dbus-monitor): comprobado en el bus real que la señalRequest.Responsese entrega en unicast al procesogdbusque hizo la llamada —y que muere justo al devolver la ruta del objeto—, de modo que ningún oyente externo la recibe jamás y la app quedaba esperando eternamente. Con herramientas CLI, esa vía es insalvable.
La escala (100–200%) se guarda en settings (escala) como porcentaje; la UI lo
convierte a factor ("150%" → 1.5). main.kt crea la ventana fija de 1150×760 dp
multiplicada por la escala, y App.kt provee LocalDensity con
Density(density * escala, fontScale). En Compose, sp→px se calcula como
sp × fontScale × density, así que un factor uniforme sobre density escala toda la
interfaz sin doble escala. Es observable, así que el cambio es en vivo.
ulxColor: todos los colores de la app como variables respaldadas pormutableStateOf, de modo que son observables en composición: al cambiar de tema la UI se repinta sola, sin reiniciar la ventana. Las pantallas nunca crean unColor(...)a mano.ulxColorManager: la única fuente de verdad de los valores de cada paleta (claro/oscuro) y traductor aColorSchemede Material 3, para que botones, campos y diálogos usen la paleta de la app en vez del morado por defecto de M3.- Los colores semánticos (
exito,aviso,error,desconocido) alimentan los semáforos de salud de los discos.
ulxLang contiene todas las cadenas como variables con defecto en inglés;
ulxLangManager las reasigna con setSpanish(), setChinese(), setPortuguese()…
Las claves de guardado son los literales fijos del selector
("English", "Español", "中文 (zhōngwén)", "Português"), que nunca se traducen.
main.kt llama a ulxSettings.loadAndApply() antes de crear la ventana: la BD se
lee una vez al arrancar y queda en caché en memoria (ninguna query SQLite dentro de una
composición). Al cambiar ajustes en Configuración, saveLang/saveTheme/saveEscala
actualizan caché + aplican + persisten en IO.
getDiskInfo()ejecutalsblk -b -Py parsea la salida (incluyendo los escapes\x20de lsblk), filtra partition tables (EFI por GUID, swap, RAID, LVM,/boot…) y calcula el uso real conFile.totalSpace/usableSpacecuando la unidad está montada.quickCheck(sin root): contadores de errores del kernel vía/sys/fs/ext4/<dev>/errors_county/sys/fs/btrfs/<uuid>/devinfo/*/error_stats, más detección de montaje en solo lectura (/proc/self/mounts).deepCheck(conpkexec, solo lectura): comando según el FS, interpretando códigos de salida y texto (p. ej. los códigos 3/4/5 dentfs-3g.probe= dañado / hibernado / dirty).
./gradlew :shared:jvmTestHay tests en shared/src/commonTest y shared/src/jvmTest: SharedCommonTest,
SharedLogicDesktopTest y UlxSystemToolsTest (metadatos y despliegue de las
herramientas del sistema). En total, 12 tests.
