Skip to content

arca-gui migrates from egui to GPUI and redesigns the window with GPUI Kit - #1

Merged
Villoh merged 100 commits into
mainfrom
gpui-kit-redesign
Sep 14, 2026
Merged

Villoh merged 100 commits into
mainfrom
gpui-kit-redesign

Conversation

@Villoh

@Villoh Villoh commented Sep 8, 2026 •

Copy link
Copy Markdown
Collaborator

Migrate arca-gui from egui/eframe to GPUI and redesign the GPUI surface on top of
GPUI Kit. This covers phases 1 through 7 of the plan
added in docs/plans/migration-to-gpui.md.

Intentionally a draft: the GPUI window is already standing and looks good,
but there are still things to polish and platform validation to complete. The
list is below, under “What remains”.

cargo run -p arca-gui still launches the egui window. The GPUI window is
opt-in: cargo run -p arca-gui --features gpui. None of this changes what
users get yet.


Why

arca-gui was a ~4,600-line window in src/main.rs with business logic and
egui rendering interleaved. The goal is not just to change toolkits, but to
keep state out of the type that the toolkit renders.

What changes, layer by layer

1. State is separated from the toolkit

An AppController now owns the inputs, current folder, history, selection,
filter, sort order, jobs, and pending dialogs. It is driven through
AppAction instead of egui::Context.

The workers and message channel are unchanged: no disk or compression
operation touches the UI thread. That is precisely what allows two possible
windows to share the same logic, and what makes this PR incremental rather
than a big bang.

2. GPUI comes from GPUI Kit, not a Zed revision

The original plan pinned zed-industries/zed@3384317. That has been removed:

  • gpui-component is built against gpui-pre ^0.3.
  • Keeping the git revision left two copies of GPUI in the dependency graph,
    which do not link together.

The solution is to rename the packages in Cargo.toml, so not a single
use in the code changes
:

gpui = { package = "gpui-pre", version = "0.3", default-features = false, optional = true }
gpui-component = { version = "0.6", optional = true }
gpui-kit-assets = { version = "0.6", optional = true }

Resulting unified graph: gpui-pre 0.3.4 + gpui-base 0.6.0 +
gpui-component 0.6.0. Reproducibility comes from Cargo.lock, which is no
longer in .gitignore — a pin that is not versioned is not a pin.

spikes/gpui keeps the original revision: it records what was validated in
G1 and is not rewritten retroactively.

3. Monochrome light and dark theme (arca-gui/src/gpui_theme.rs)

Six greys per mode, neutral: no hue at all. Dark is painted on true black,
the way Geist paints a dark surface. Light is not that palette inverted by
formula — a light window needs its steps closer together or the chrome starts
to stripe.

Dark Light
background #000000 #FFFFFF
surface #0A0A0A #FAFAFA
raised #171717 #F2F2F2
border #2E2E2E #E0E0E0
text #EDEDED #171717
muted #A1A1A1 #666666

There is no accent color. Everything that would use one is the text color
at a fraction of its opacity: selected row 12%, hover 5%, alternating stripe
2%, focus ring 70%, text selection 18%. Contrast is guaranteed by construction
rather than by a table someone has to maintain: if the text is readable, the
selection is visible.

The only exceptions are danger and warning. They are not decoration — they
distinguish “extracted” from “not extracted”, and anyone scanning before
reading needs to be able to see that distinction. Dark uses #E5484D and
#F5A623; light darkens them to #C50E1F and #A15C00, because the red that
carries on black does not carry on white. A test holds danger at 4:1 against
its own background.

The tokens are the ones from gpui-component; Arca does not add its own
token layer
, it only specifies the value of each token.

Radius 4/6 px, base font size 16 px, monospace one step down at 13 px.

4. Layout redesign

The reference is Nohrs (the same problem:
a file explorer) with the density of DBFlux.

Before: a stack of floating strips in p_3 + gap_3. Now: full-bleed regions
separated by 1 px lines, with padding inside each bar so every divider spans
the full width.

┌─ actions (40 px) ────────────────────────── [filter] ──┐
├─ ← → ↑ │ path (34 px) ─────────────────────────────────┤
│ folders  │  full-bleed table                           │
│ (224 px) │                                              │
├──────────┴──────────────────────────────────────────────┤
│ file summary                 N visible │ M selected      │
└─────────────────────────────────────────────────────────┘
  • Sidebar with the folder tree (gpui_component::sidebar). It is content,
    not chrome: until now, reaching a deeply nested file meant double-clicking
    down and navigating back up. The current folder branch opens automatically;
    the rest stays closed. The tree is cached by (file path, entry count) —
    rebuilding it in render meant walking all names 60 times per second during
    extraction.
  • Status bar attached to the bottom (gpui_component::status_bar), with
    the summary on the left and counters on the right. The counters used to live
    in the middle of the navigation row, pushing the breadcrumbs against the
    right edge.
  • Borderless buttons, with the background appearing only under the pointer.
    Seven outlined boxes read as seven competing things.
  • Icon arrows (IconName, via gpui-kit-assets) instead of ‹ › ↑.
  • Floating menus: the overflow menu used to be rendered in flow and pushed
    half the window down.
  • Full-bleed table, with no border or radius of its own, a fixed header,
    and alternating stripes.

5. Accessibility

What was already there is preserved and not downgraded: AccessKit roles,
visible focus, keyboard navigation, focus trapping in modals, and the
background subtree without roles while an overlay has focus. Icon buttons keep
their accessible names as words.


Verification

  • cargo test --workspace — green (default build, egui).
  • cargo test -p arca-gui --features gpui — 38/38.
  • cargo fmt -p arca-gui -- --check — clean.
  • Visual inspection on Windows with a nested test ZIP: folder tree, navigation,
    table, status bar, dark theme.

Two new tests in gpui_theme.rs:

  • WCAG contrast for every ink color on each of the three backgrounds, in
    both modes. With no accent, the palette is the accessibility, so this is
    the test that matters.
  • Distinct, opposite palettes: two grays that are the same gray, or a mode
    that stops being the inverse of the other. These are copy-and-paste slips that
    compile and do not show up in a diff of thirty hex literals.

I wrote a third test that failed (it assumed the three backgrounds are ordered
in the same direction in both modes). That is false: in light mode a panel is
raised toward white and a hover is tinted away from it. Rather than tuning
the threshold until it passed, I replaced it with the thing that can actually
break.


What remains

That is why this is a draft.

  1. Internal widgets are still hand-rolled. They need to be replaced with
    the ones from gpui-component:
    • Input — removes the roughly 400 lines of FilterInput and its UTF-16
      IME contract. This is the largest pending deletion.
    • Modal/Root, Popover, Table, Notification.
  2. Menus float with absolute and fixed offsets (top(38.) right(232.)),
    rather than being anchored to their trigger. They break if the filter width
    or font size changes. The correct solution is Popover.
  3. There is no settings dialog in the GPUI surface, which would allow the
    theme and language to be changed without editing gui.conf. For now the
    preference is read at startup and System follows the desktop.
  4. Incomplete platform matrix. Only Windows GNU has been validated. Linux
    and macOS are still unmarked in docs/plans/gpui-spike-baseline.md, and
    they should not be marked without a real test.
  5. Screen reader not yet validated with Narrator/NVDA on the GPUI window.
    The plan says any accessibility limitation blocks removing egui, so this is
    required before phase 8.
  6. egui is still included. Phase 8 (removing eframe, egui,
    egui_extras) is not done, and should not be done until 4 and 5 are closed.

Notes for reviewers

  • Three button labels were reconstructed. During the work I corrupted 15
    multiline dialog_button calls with a bad regex. The on_click bodies
    survived intact, and modal_focus_targets/modal_enter pin the exact id and
    focus handle of each button, so the reconstruction is deterministic except
    for the visible text of three: "Set Password"/"Unlock",
    "Keep Both"/"Keep Both Always", and "Delete". They deserve a look.
  • Commit 1 contains the spike and the plan; commit 2 contains the code. They
    are not split further because any intermediate cut inside gpui_shell.rs
    produces a commit that does not compile.
  • Settings::load already reads the columns key, which was a parity defect
    noted in G1. The gui.conf format does not change.

Villoh and others added 3 commits September 8, 2026 21:17
La rama anterior se hizo sobre c4c0354 y main se movio 18 commits por
debajo: renombrar dentro del archivo, el visor, la vista plana, los
grupos por mascara, las columnas y el modo oscuro en blanco y negro. Los
dos lados reescribieron main.rs desde la misma base, asi que el merge
salieron 46 hunks que son el mismo conflicto repetido: la rama renombro
todos los accesos al estado y main escribio mil seiscientas lineas
nuevas contra la forma vieja.

Resolver eso no es elegir un lado, es rehacer la extraccion encima del
main de hoy, y se hace asi a proposito: el compilador comprueba la
transformacion. Un hunk mal resuelto compila; un self.archive que se
escape de la reescritura, no.

Este commit trae solo lo que no toca main.rs y por tanto no choca: el
spike, los planes, Cargo.lock fuera de .gitignore y las dependencias de
GPUI Kit. gpui_shell.rs y gpui_theme.rs vienen en el arbol pero main.rs
todavia no los declara, asi que no se compilan: la ventana GPUI se
enciende en el commit siguiente, cuando exista AppController.

El analisis de esa extraccion --que campo va donde, cuales de los 57
metodos se mueven, y el unico sitio que no es mecanico-- esta en
docs/plans/portar-controlador-a-main.md para que no viva en la cabeza de
nadie.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Faltaba lo que una sesion nueva no puede deducir del codigo: en que rama
esta cada cosa, y sobre todo que la rama vieja no se borra porque su
main.rs es la implementacion de referencia de AppState, AppController y
AppAction. Rehacerlos desde cero seria trabajo tirado.

Tambien la lista de comprobacion manual. cargo test no toca la UI de
egui, asi que si la extraccion se come el visor o los grupos por
mascara, la suite pasa verde igual y nadie se entera.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Villoh and others added 26 commits September 8, 2026 21:59
# Conflicts:
#	arca-gui/Cargo.toml
#	arca-gui/src/main.rs
El shell GPUI llevaba 149 de los 161 textos en ingles fijo dentro del codigo: en espanol se veia media ventana sin traducir y un lector de pantalla leia los nombres de las regiones en ingles. Ahora sale todo de i18n.rs.

Diecisiete cadenas nuevas, las que GPUI necesita y egui no tenia: los nombres de las regiones que solo existen aqui -- barra de acciones, estado, progreso, contenido -- y los estados vacios, que en egui eran una sola frase y aqui distinguen archivo sin entradas, carpeta vacia y filtro sin coincidencias.
Idioma, tema, y el formato, compresor, nivel y subcarpeta con que se crea un archivo nuevo. Hasta ahora la superficie GPUI leia gui.conf al arrancar y no habia forma de tocarlo sin editar el fichero a mano.

Cada preferencia es una fila de botones con el actual relleno, no un desplegable: hay tres como mucho de cada, y una lista asi de corta cuesta mas abrirla que leerla. El teclado y el raton pasan por settings_activate, que es una sola funcion, para que Enter y el clic no puedan acabar haciendo cosas distintas.
Solo habia Ctrl+F, C, X y V. Ahora tambien Ctrl+O, Ctrl+N, Ctrl+E, Ctrl+T, Ctrl+I, Ctrl+Shift+C, Alt+W, F5, F1 y Escape, y una ventana F1 que los lista.

Un solo manejador en la raiz en vez de una docena de acciones GPUI con sus bindings: cada atajo es la misma forma -- una tecla, una guarda y una accion ya escrita -- y una tabla de ellos se lee de una vez. La lectura de la tecla sale a shortcut_for, que no toca estado, para poder comprobar sin ventana que Ctrl+O sigue valiendo con el cursor dentro del filtro y que F5 no.
La superficie GPUI no tenia menu contextual: abrir, extraer la seleccion, extraer aqui, probar la seleccion, quitar, copiar, cortar, pegar, copiar nombres y seleccionar todo solo estaban en la barra, en el menu de overflow o en una tecla que habia que saberse.

Cada entrada nombra su atajo, para que el menu se deje de usar solo. Copiar, cortar y pegar se quedan fuera donde el escritorio no tiene portapapeles de ficheros, en vez de aparecer en gris: una entrada que no puede hacer nada nunca es peor que no tenerla. Pulsar donde no hay menu lo cierra; el menu esta occluded, asi que sus propios clics no llegan al fondo.
El ancho estaba clavado en el codigo, uno por columna, y no se guardaba. Ahora sale de Settings::widths, el mismo campo de gui.conf que llevaba usando la ventana egui, asi que una anchura puesta en una superficie aparece en la otra.

El tirador va absoluto dentro de la celda de cabecera, no como elemento de la fila: un separador con ancho propio correria cada cabecera unos pixeles respecto a la columna que nombra. El raton se sigue en la ventana y no en la tira, porque el puntero se sale de seis pixeles enseguida. Doble clic ajusta la columna a lo que lleva dentro.

El nombre sigue siendo elastico en vez de tener ancho fijo: es lo que hace que la lista llegue al canto derecho.
Todo el archivo de una vez, sin carpetas, que es como se busca algo cuando no se sabe donde esta. El controlador ya lo tenia en visible_rows leyendo settings.flat; lo que faltaba era la forma de encenderlo. Al encenderla se enciende la columna Ruta si estaba apagada, porque una lista sin carpetas encima tiene que decir de donde sale cada nombre.
Faltaba todo lo que escribe: anadir ficheros, nueva carpeta, renombrar, deshacer, guardar una copia, contrasena por defecto y seleccionar por mascara. La superficie GPUI solo sabia abrir, extraer y borrar.

undo_last, drop_undo y reread_names se van de la vista egui al AppController. No tienen nada de un toolkit dentro y las dos superficies las ofrecen; tenerlas colgando de ArcaApp era la razon por la que GPUI no podia deshacer.

Los cuatro dialogos que piden un texto comparten un solo campo con el nombre que le pone el dialogo abierto, porque son modales y excluyentes entre si: un campo con tres nombres es un campo, no tres que estan vacios casi siempre. El mismo motivo por el que el volcado de lo tecleado sale a push_to_owner: habia dos copias identicas y un campo nuevo se olvidaba en una de ellas.
Los recientes cuelgan del menu, diez como mucho, por ruta y no por nombre para que dos que se llaman igual se distingan, con su escoba al final.

La pagina de codigo se va a los ajustes en vez de a un submenu: es una preferencia guardada en gui.conf, y ese es el sitio de las preferencias guardadas. Cambiarla relee los nombres del archivo abierto en el sitio, sin volver a abrirlo.
F3, o el menu de la fila, abre lo que hay dentro del archivo sin sacarlo: como texto, como hex o como la imagen que es. El controlador ya sabia leerlo -- view_entry estaba en AppController -- y GPUI no tenia donde ponerlo.

El texto y el hex van por uniform_list, asi que solo se maquetan las lineas en pantalla y un log de un millon de lineas se abre igual de rapido que una nota de tres. La imagen se decodifica una vez y se guarda con el nombre del que salio: darle a GPUI una Image nueva por fotograma seria decodificar una foto de treinta megas sesenta veces por segundo.

La pestana de imagen solo aparece cuando hay imagen, y la fila de una carpeta no ofrece visor: una pestana que dice imagen sobre un fichero de texto es una pestana que miente.
El merge f10f5b4 se quedo con el main.rs de la rama tal cual y con el i18n.rs de main tal cual, asi que la ventana egui perdio features que si estaban en 0.6.6. moving_word, que nadie usaba en ninguna de las dos superficies, era la senal.

Vuelven Job::Move, moved_name con su prueba de las dos escrituras de barra, y move_into, ahora en AppController y no colgando de la vista egui. Con ellos vuelve tambien el apunte de deshacer en run_job: sin el, state.undo no se escribia nunca y Ctrl+Z y su entrada de menu estaban apagados para siempre.

En GPUI el gesto es el mismo que en egui: mientras la seleccion esta en el aire no se decide nada. Si cae sobre una carpeta del archivo, se mueve ahi; si sale de la lista, la coge arca-drag. El arrastre nativo no puede empezar antes, porque en cuanto empieza el sistema se queda el puntero y ya no hay vuelta a la lista.
Lo otro que se llevo por delante el merge f10f5b4. arca-net seguia en Cargo.toml sin que nadie lo llamase, y update_ready, check_updates, update_tampered y update_installing estaban en i18n.rs sin usar: esa era la senal.

Vuelve el aviso entero -- preguntar una vez al arrancar, en un hilo y sin decir nada; la entrada arriba del menu con un punto en el boton para que se vea desde fuera; bajar el instalador comprobando el SHA-256 contra las sumas publicadas al lado; y dejar que Inno cierre y reabra Arca. Una copia sacada del .zip no se actualiza sola, porque son ficheros sueltos en una carpeta que eligio alguien: a esa se le ofrece la pagina. Solo se aceptan direcciones https de nuestro propio repositorio.

Y con ello Settings::save, que construia las lineas de flat, tree, page, window y recent en un String que luego tiraba. Seis ajustes se leian al arrancar y no se escribian nunca: por eso la lista de recientes salia siempre vacia. El texto del fichero sale ahora a Settings::text y la prueba lo lee de vuelta sin tocar disco, que es la unica forma de que no se vuelva a romper en silencio.
La tercera que se llevo el merge, y con ella lo que nadie usaba en i18n.rs: elapsed_word, time_left, pause_word, resume_word, paused_word y stopping.

Con ella vuelve tambien Cancelar, que estaba muerto: AppAction::CancelJob era un brazo vacio y el notify del worker devolvia true siempre, asi que el boton no paraba nada. Ahora hay dos banderas -- parar y esperar -- que el worker lee al acabar cada entrada, que es el unico momento en que no esta en mitad de algo. Pausar suelta al final de una entrada, no de un byte: un fichero empezado tiene que terminar. Parar pasa aunque este en pausa, para no tener que reanudar algo solo para poder abandonarlo.

Las banderas se renuevan por trabajo en vez de bajarse: un hilo al que se le dijo que parase puede seguir de salida, y no debe leer la bandera que mira el trabajo siguiente.

El trabajo vuelve a distinguir si se lanzo desde la lista -- panel encima, con el fondo atenuado -- o desde el Explorador, que es la ventana entera porque no hay lista detras a la que volver. En GPUI la tira de progreso gana el reloj, la estimacion y el boton de pausa.
Un trabajo abandonado decia en rojo lo que dijese el error de dentro. No hay nada mal en el archivo: la reescritura se rindio antes de cambiar nada, y eso es lo que dice ahora, en el color de lo que solo es una noticia. Era la ultima cadena de i18n.rs que no usaba nadie.
Los dos gestos de puntero que faltaban. La lista de GPUI es virtualizada, asi que sobre que fila esta el raton es aritmetica con el desplazamiento y no un rectangulo que alguien haya guardado: eso sale a row_under, que se comprueba sin ventana porque una fila de diferencia ahi seleccionaria todo corrido un sitio.

La goma sigue la regla del Explorador, que es la unica que deja los dos gestos en el mismo boton: pulsar sobre algo ya marcado y tirar lo lleva a otro sitio, pulsar en cualquier otro lado y tirar dibuja una seleccion nueva. Ctrl suma a lo que habia. Pasado un borde la lista corre detras del puntero, porque si no una seleccion nunca podria ser mas larga que la ventana.

La rueda pulsada deja un ancla y la lista corre hacia el puntero, mas rapido cuanto mas lejos, con wheel_speed, que es la misma funcion y las mismas pruebas que usa egui. La suelta cualquier otro boton, la propia rueda girando o Escape.

Dos diferencias con egui, a proposito: el paso lo da el tick de 100 ms que ya tenia el shell en vez de un fotograma, y el ancla es un anillo sin las flechas que Windows dibuja sobre el puntero, porque eso pedia esconder el cursor del sistema.
Se veia OpenEnter, ViewF3, RemoveSupr. Las entradas se construian con una tabulacion dentro del texto, que es como egui dibuja una columna a la derecha; GPUI maqueta texto y ahi una tabulacion no es nada, asi que el atajo quedaba soldado a la palabra.

El atajo pasa a ser un hijo aparte, alineado a la derecha y en la tinta apagada, porque es una forma de llegar y no una segunda cosa que leer. Como ya no hay que meterlo en la cadena, lo tienen todas las entradas que lo tengan y no solo las tres del portapapeles: probar, seleccionar todo, invertir, soltar, deshacer y contrasena por defecto tambien lo dicen ahora.

Y vuelven las rayas que agrupaban ese menu en egui: mirar, cambiar, mover por el portapapeles y trabajar sobre la seleccion son cuatro cosas distintas, y doce entradas seguidas son un muro.
La leyenda nativa se queda transparente y la dibuja TitleBar de GPUI Kit, con los mismos tokens que todo lo que hay debajo: el arrastre, el doble clic para maximizar y los tres botones de la ventana los lleva ella. Dentro va el nombre de lo que esta abierto, que es para lo que sirve una barra de titulo.

Nada pulsable ahi dentro a proposito: la barra es la zona de arrastre, y un control dentro de ella movería la ventana en cuanto la mano temblase de camino a pulsarlo.

Dos cosas que el alto nuevo movia. El menu de overflow flota en coordenadas de ventana, asi que ahora cuenta la barra o se dibujaba encima de la fila de botones en vez de debajo. Y los dos velos que tapan el fondo -- los dialogos y el selector de ficheros del sistema -- empiezan bajo la barra en vez de cubrirla: los tres botones de la ventana no son fondo, y un dialogo que los tapase seria un dialogo que no deja cerrar la ventana que hay detras.
… ventana

Dos fallos distintos con la misma cara.

La goma leia el alto de fila de last_item_size.item, que pese al nombre es el tamano del viewport y no el de una fila: dividia por cuatrocientos en vez de por veintiocho, asi que todo punto caia en la primera fila y no se marcaba nada. El alto sale ahora del contenido entre el numero de filas, que es de donde GPUI lo saco, y la division va a row_height con su prueba para que el nombre no vuelva a enganar a nadie.

Y arrastrar fuera no hacia nada porque AppController::drag_out era un cuerpo vacio: la implementacion de Windows con arca-drag tambien se la comio el merge f10f5b4, y arca-drag llevaba desde entonces en Cargo.toml sin que la llamase nadie. Vuelve entera, sin extraer nada al empezar: al shell se le da una lista de nombres y tamanos y va pidiendo un fichero cada vez mientras suelta, asi que un arrastre del que uno se arrepiente no cuesta nada.

El disparo pasa de la fila a la ventana. on_drag_move se dispara con cualquier movimiento, y comparando contra el rectangulo de la fila el arrastre nativo empezaba en cuanto el puntero llegaba a la fila de al lado -- que no es salir de la lista, y ademas hacia imposible soltar sobre una carpeta. Ahora se mira una sola vez, contra la lista entera.
Dos costes, los dos por movimiento del raton, que es a cien hercios.

visible_rows ordenaba llamando a to_lowercase() dentro del comparador: dos String por comparacion, o sea unas treinta mil asignaciones por lista de mil quinientas filas. Ahora se pliega caracter a caracter con Iterator::cmp, sin asignar nada. Eso lo paga todo el que ordene, tambien el repintado normal.

Y la lista se construia entera dos o tres veces por evento. La goma se la queda congelada al empezar el gesto -- lo que marca no puede cambiar de donde lo marca, y con un boton bajado no hay nada mas que la cambie -- y lo unico que las demas necesitaban de ella era cuantas filas hay, que ahora lo apunta el repintado, que es el que ya lo sabia.
No habia perfil dev, asi que cargo run compilaba todo a opt-level 0: GPUI entero, su motor de layout, el modelador de texto y el rasterizador, que rehacen su trabajo en cada fotograma. Eso no es un poco mas lento, es la diferencia entre una lista que sigue al puntero y una que va a tirones al arrastrar y al marcar.

Las dependencias pasan a 3 tambien en depuracion: no son codigo de Arca y nadie las depura. El codigo de Arca a 1, que es lo que necesitan ordenar la lista y recorrer las entradas para no notarse, y a ese nivel el depurador sigue parando donde se le dice.

La primera compilacion despues de esto rehace el arbol entero -- seis minutos y medio aqui -- y a partir de ahi un cambio en arca-gui vuelve a ser ocho segundos.
The row div carried our own click listener and the kit chained its own on
top of it, so a double click ran both: select_row navigated into the
folder and the kit's DoubleClickedRow then resolved the old index against
the already navigated list, opening the first entry inside it.
The folder pane was a fixed 224 pixels, so a deep branch could only be
reached by scrolling a pane that had room to spare next to it. It now sits
in a resizable split with the file list, between 160 and 520 pixels, and
the width is kept with the column widths so it survives the window.

The row width follows the pane instead of a constant, which also fixes the
selected row's outline being clipped on its right edge: the sidebar's right
border eats into the content box, so rows sized to the padding alone
overflowed the scroll area by one pixel.
Crossing onto the sidebar counted as leaving the list, so the native
drag started before a folder there could ever be dropped on, and the
tree took no drop at all. Only the window's own edge counts on that
side now, and the tree's folders take the same move a folder row takes.

The preview was painted at the pointer less where inside the row the
press landed -- a row is as wide as the list, so a press on a far
column drew it back at the file name or half off the window. It is
carried at the pointer now: centred over it, lifted above the hand,
and opaque rather than a tint, which was unreadable over file names.
The em dash looked wrong in the title bar; a hyphen matches what every other window on the desktop uses.
Button renders its label in an inner element that applies
button_text_size(self.size), so the .text_xs() set on the button itself
was overridden by the default Medium size and every word button drew at
16px while the surrounding bars drew at 12px. The mismatch was visible in
the breadcrumbs, where the current folder is a plain div. Setting the
button size to XSmall resolves button_text_size to text_xs instead.
…a hit is

The filter matched an entry's whole path across the whole archive and drew
the result as that path, so searching "redist" inside "_CommonRedist"
answered with every file in the folder, each one labelled with the way back
to the archive root.

A search now starts at the current folder -- the whole archive from the
root, that folder's subtree from anywhere else -- and matches a name against
its own leaf, never the folders above it. It answers with folders as well as
files, and names a hit by the path from the searched folder down, so a
result says which subfolder it came out of without repeating the path to the
root. The optional path column is read from the same folder.

A rename takes its starting name from the entry rather than from the row
label, which now carries that folder while a search is showing.
…the tree

Right clicking anywhere but a row did nothing: the kit only marks a row
when the press lands on one, so the menu was built for a stale row or for
none at all, and the sidebar had no menu at all. Both are places a file
manager is expected to answer from.

Under the last row the menu is the folder's: make a folder, paste, select
all. On a branch of the tree it is the row menu, run against that folder
-- opening, pasting and making a folder go inside it, and everything else
picks it and works on the selection, so the list is never dragged
somewhere to explain an action. The name is edited in the panel it was
pointed at, which is why a rename started from the tree is typed there.
The tree was a drop target but never a source, so a folder the list is not showing could only be moved or extracted by navigating to its parent first. Pulling a branch now picks everything under it, the same carry a folder row of the list starts.

The sidebar's rows begin above the list's, so a pull begun in the tree crossed the list's top edge on its first move and handed itself to the system as a native drag. Over the sidebar column only the window's own edge now counts as leaving.
Two ghost words at the end of a strip that already ends in grey status text read as the rest of the sentence, not as something to press. Pause, resume and close are marks nobody needs a tooltip for, and the kit's icon set already carries them.
The toolbar password button built its job with no new password in it and
ran it, and nothing ever filled that blank, so the only thing the button
could do was take a password off: on a plain archive it rewrote the file
unencrypted without asking anything at all.

The window now holds a second question. An archive that is already locked
is asked for the password it has, and then, like every other archive, for
the one it is about to get; an empty answer to that second question is
what takes the password off, and the Remove password button is the same
answer by name. Answering no longer closes the dialog unconditionally
either: it closes only once nothing is left to ask, so an empty box or the
first of the two questions leaves the window where it is instead of
stranding it waiting on a dialog that is gone.
The password boxes carried a separate Show/Hide button underneath, driven
by a flag on the application state that every frame pushed back into the
field. The kit's own input has the eye for this, drawn inside the box on
the right, so the button, the flag, the action that flipped it and the two
words it needed are gone.

Masking is the field's business now, which also means it can be reset:
every password box opens hidden, where before revealing one left the next
dialog opening in the clear.
The password kept for the window was written down and then read by nobody:
Ctrl+P took one, said it would be used until the window closed, and every
dialog went on asking from scratch.

Both dialogs that ask for a password now offer it by name, next to the box
and only when there is one to offer: the one that asks to open, unlock or
lock an archive, and the one that compresses. It fills the box rather than
answering for you, so the password can still be read, changed or cleared
before the dialog is answered -- a password put on an archive by accident
costs a full rewrite to take off again.

Its Spanish name was also missing its tilde.
Compressing from the toolbar closed the whole program the moment the job
finished, which reads as a crash. close_when_done was set for every
Job::Compress and the shell answers that by removing the window. That is
right for a window the Explorer opened to do one job and wrong for one
being driven by hand, so it is now gated on a one_shot flag taken from the
startup arguments.

The add box could only ever be given files. Windows has one native dialog
for files and another for folders and none for both, so the picker is now
a single Add... button with an entry for each, and every pick adds to the
list instead of replacing it. The box lists what it is about to compress,
with a cross per row and a Remove all, because a selection built over
several passes is only trustworthy if it can be read back before the work
starts.

Dropping files in from the Explorer never worked anywhere in the window.
GPUI translates FileDropEvent::Entered into a MouseMove and Submit into a
MouseUp before dispatch, so only Exited ever reaches a FileDropEvent
listener and the window was watching for something that never comes.
External files arrive as an ordinary drag carrying ExternalPaths, which is
what the root and the add list now listen for -- and a drag is the one
gesture on Windows that carries files and folders together, so a drop onto
the add box goes straight into the list.
The archive was written to a path anchored at /tmp. Git Bash puts /tmp
under AppData while the native python3 reads the same string as C:\tmp, so
the fixture was never created, the extraction that followed failed for
want of a file rather than for want of permission, no escaped file
appeared, and the control reported a pass having run nothing at all. A
security check that cannot fail is worse than no check, because it is
counted.

The archive is now built and checked relative to the working directory the
script already moved into, and the entry escapes one level up so it lands
somewhere both platforms can name. Building it is no longer assumed: if
the fixture is missing the script says so instead of reading its absence
as a refusal. The extraction is asserted to fail as well, so the pass
needs the extractor to have actually refused and not merely to have left
the filesystem alone.

The protection itself was never in doubt and is unchanged: arca-core's
safe_name rejects the entry, and covers this in its own tests.
The window sat on "Created ..." and an empty list, and the only way to see
what had been built was to go and open it by hand. reread_target answered
None for Job::Compress because compressing writes somewhere else and
nothing that was open had changed, which was true and beside the point:
what it wrote is an archive, and a window left sitting on its own success
has nothing else to show.

This only surfaced once the window survived the job. Until then it closed
the moment compressing finished, so there was never anything to show it
in.

The two halves have to agree. A window the Explorer opened for one job
still leaves when that job ends, and what decides it leaves is still being
on View::Running -- which rereading is exactly what replaces with
View::Browse. Sending such a window to the new archive would cancel its
own departure, so the target is only set when the window is staying.
It was a quarter of a feature, and the quarter that pays least. The button
never unlocked anything: it wrote the remembered text into the box that was
already asking, in all four places one is asked for. That caution is right
for setting a password, where one put on by accident costs rewriting the
whole archive to take off, and pointless for opening one, where a failed
attempt costs nothing.

What was left saved typing a long password in exchange for two clicks,
only within a session, and only after arming it with Ctrl+P first. It
began to pay on a folder of archives sharing one password, and that is the
case it served worst, still asking for two clicks per archive.

The comment describing it promised behaviour the code did not have -- "one
password to try before asking", when nothing ever tried it -- and pointed
at a default_password_window that exists nowhere in the tree.

Removing rather than finishing, because finishing it properly is the four
pieces WinRAR already has: per-archive masks, an opt-in "accept without
confirmation", a master password to earn the right to persist, and labels.
That is its own piece of work, and it is written down in
docs/todos/gestor-de-contrasenas.md along with what was taken out, so it
can be rebuilt without archaeology.

Migration: Ctrl+P and the Default password entry in the overflow menu are
gone, as are the two "Use the default password" buttons. Passwords are
still asked for and still work exactly as before, one archive at a time.
Nothing was persisted, so nothing is left behind to clean up.
The box that asks for a password answers three different questions, and
its button carried one label for all of them but the first: "Start". An
archive that says "This archive is encrypted / Type the password to open
it" then offered Empezar, which starts nothing.

Now each branch says what pressing it does. Setting a new password keeps
"Set password". Opening or extracting says "Open", reusing the word the
toolbar already uses. The third case is the one the single label hid
worst: when the password being asked for is the one the archive already
has, pressing this does not open anything either, it brings up the second
half of the question, so it says "Continue".
The old PKWARE cipher was rejected outright, so an archive from any tool
that still writes it was a dead end: the listing opened and no entry came
out. It is read now, with the same streaming shape as the AES path, and
still never written -- anything Arca produces is AES-256. Changing the
password of one of these archives therefore moves it to AES-256, which is
the way out of the legacy scheme rather than a way to stay in it.

The scheme carries a one byte password check, so one wrong password in 256
gets past it and fails on the checksum instead.
Opening an encrypted archive asked for the password and then believed
whatever was typed: the listing is in the clear, so nothing contradicted it
until an entry was extracted, and by then the box was long gone. The
password is checked against the archive as it is typed, and a wrong one
leaves the box open saying so.

It reads only the few bytes at the front of the first encrypted entry, so it
costs nothing and blocks nothing: two verifier bytes for AES, one for
ZipCrypto. Extraction still has the last word.
The Windows build said `--target x86_64-pc-windows-msvc`, which is the host
triple of the runner and bought nothing, but it moved the artifacts to
`target/<triple>/release`, where the cache does not keep them. The job
restored 613 MB of cache and then rebuilt all 381 crates anyway: seven and a
half minutes on every run, against eight crates and one minute on Linux with
the same cache hit. Dropping the flag leaves one build step for the three
platforms.

`windows/arca-shell` is outside the workspace and has a target directory of
its own, which the cache was not told about, so the `windows` crate was
rebuilt every run too. Its lockfile goes in with it: without one the
resolution -- and with it the cache key -- changes on its own.
@Villoh
Villoh merged commit 1816bbb into main Sep 14, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant