<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Qarmin on Medium]]></title>
        <description><![CDATA[Stories by Qarmin on Medium]]></description>
        <link>https://medium.com/@qarmin?source=rss-9c8ac0ce6dcd------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*9vPDc-LiVZ53bh7jKZ7csQ.jpeg</url>
            <title>Stories by Qarmin on Medium</title>
            <link>https://medium.com/@qarmin?source=rss-9c8ac0ce6dcd------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 08:45:40 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@qarmin/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Krokiet/Czkawka 12.0]]></title>
            <link>https://medium.com/@qarmin/krokiet-czkawka-12-0-c5dad2116793?source=rss-9c8ac0ce6dcd------2</link>
            <guid isPermaLink="false">https://medium.com/p/c5dad2116793</guid>
            <category><![CDATA[rust]]></category>
            <dc:creator><![CDATA[Qarmin]]></dc:creator>
            <pubDate>Sun, 28 Jun 2026 11:26:00 GMT</pubDate>
            <atom:updated>2026-06-28T11:26:00.020Z</atom:updated>
            <content:encoded><![CDATA[<h3>Krokiet/Czkawka 12.0 - Nowa aplikacja na Androida, porównywarka obrazów, menu kontekstowe, ostatnia wersja Czkawki GTK i AI</h3><p>W życiu każdego projektu po pewnym czasie nadchodzi moment, w którym konieczne jest nie dodawanie nowych funkcji(albo nawet i całych programów), lecz ich usuwanie - i ten właśnie stan został dziś osiągnięty. Co ciekawe, mimo tego drastycznego kroku, ilość rozwijanych przeze mnie aplikacji do deduplikacji, nie zmieniła się.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jZnTFThBV7zUJE9c0pmYKw.png" /><figcaption>Mam nadzieję, że w końcu Krokiet 12.0 z nowymi funkcjami spowoduje masową migrację z wersji GTK</figcaption></figure><p>Miejsce nieco już wysłużonej 5-letniej Czkawki GTK na desktopie, zajmuje ciągle całkiem zjadliwy, 2-letni Krokiet, a przebojem na Androida wchodzi Cedinia, która pewnie wypełni dużą lukę w tym segmencie na urządzeniach przenośnych (nie żeby takich aplikacji nie było - po prostu w wersji darmowej są one bardzo ograniczone i często wymagają absurdalnych pieniędzy za minimum funkcjonalności).</p><h3>Ostatnia wersja Czkawki GTK</h3><p>Czkawka Gtk jest obecna w repo od samego początku — od wersji 1.0(a w zasadzie to nawet dłużej) stanowiła przez większość czasu jedyną wersję graficzną aplikacji.</p><p>Patrząc wstecz, uważam że dobrym wyborem, było użycie gtk zamiast np. qt, mimo problemów jakie z nim miałem(i z samym sobą, nieudolnie próbującym coś sensownego w nim zrobić).</p><p>Problemy, na jakie natykali się użytkownicy, w zdecydowanej większości udawało mi się reprodukować i naprawiać. Części z nich jednak, takich jak niemal w 100% przezroczyste GUI czy wysypywanie się aplikacji podczas wklejania tekstu ze schowka na niektórych wersjach Windowsa, przez wszystkie te lata nie udało mi się za żadne skarby odtworzyć u siebie. Gdybym był w stanie również u siebie zaobserwować te problemy, mógłbym dokładnie zweryfikować, co je powodowało, a następnie albo załatać je po swojej stronie, albo zgłosić bezpośrednio do GTK lub autorów felernych bibliotek.<br><br>Przy narastającej ilości podobnych problemów nie mogąc ich poprawnie obsłużyć, zrobiłem więc to, co każdy mężczyzna w mojej sytuacji by zrobił - skapitulowałem. Uznałem, że nie warto kopać się z koniem, zwłaszcza że na horyzoncie zaczęły pojawiać się czysto rustowe alternatywy, takie jak Iced, Egui czy Slint. Jednym z ich podstawowych założeń była szeroka multiplatformowość, w przeciwieństwie do GTK, gdzie systemy inne niż Linux przez długi czas były traktowane jak obywatele drugiej kategorii. Choć z tego, co widzę, powoli się to zmienia, to jednak wciąż zbyt wolno.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*e1R89iMxAZJY2cpWl5Leyg.png" /><figcaption>Po lewej wersja 1.0 Czkawki bazująca na gtk3 a po prawej bazująca na gtk4 wersja 11.0.1(około 5 lat różnicy)</figcaption></figure><p>W poprzedniej wersji zrobiłem przymiarki do przeportowania Czkawki na jeszcze nie istniejące GTK 5, mając nadzieję, że nowy framework naprawi część problemów, z którymi się borykałem. Jednak ilość rzeczy, które trzeba by przepisać - wraz z bazowymi elementami, tj. TreeView - była zbyt wielka i wymagała dużych nakładów pracy, przez co ograniczyłem się jedynie do usprawnień w części kodu.</p><p>W wersji 7.0 pojawił się Krokiet, używający Slinta, i choć początkowo brakowało mu wielu opcji obecnych w frontendzie GTK, przez co niemożliwe było, by z dnia na dzień zastąpił starsze GUI. Jednak z wersji na wersję ta różnica była coraz mniejsza i w obecnej chwili to Krokiet dysponuje większą liczbą narzędzi oraz bardziej rozbudowanymi opcjami. W wersji 11.0, dodałem popup, który wyświetla się po pierwszym uruchomieniu aplikacji a w tej wersji dodałem dodatkowo specjalny jednorożcowy przycisk.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LPuxNfGPszpvIKBxPaeOsw.png" /></figure><p>Z racji tego, że od momentu ogłoszenia Krokieta poświęcałem wersji GTK coraz mniej czasu, a jednocześnie nikt nie próbował przejąć na swe barki jej rozwoju, postanowiłem zakończyć rozwijanie tej wersji aplikacji, jednocześnie namawiając użytkowników (z wersji na wersję coraz agresywniej) do przejścia na Krokieta.</p><p>Dodatkowym impulsem do działania było to, że przeglądając różne fora i strony, widziałem, że raz za razem to właśnie Czkawka jest oferowana użytkownikom jako ten &quot;główny&quot; program, a gdy z różnych powodów nie działał, wylewana była na nią frustracja jak i też na mnie - mimo że istniała wtedy alternatywa, która większości ze zgłaszanych problemów nie miała. Brak nowszych wersji programu być może spowoduje, że użytkownicy w końcu(po dwóch latach indoktrynacji) dokonają migracji.<br><br>Wiem jednak, że nie wszyscy coś takiego zrobią. Nazwa Czkawka swą dziwnością wpisała się wielu ludziom w umysły, zatem dla opornych, którzy nie chcą spróbować pobrać innego programu, do jedynego pliku zip z Czkawką, jaki dystrybuuję - <em>windows_czkawka_gui_gtk_412.zip</em> - potajemnie dorzuciłem pliki binarne Krokieta i pewne wiadomości, które, mam nadzieję, będą skutecznie działały na podświadomość wielu użytkowników.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YSZUduSUQhUsim1H3zny0g.png" /></figure><h3>Cedinia - nowa aplikacja do czyszczenia na Androida</h3><p>W przeszłości miałem kilka rozmów z osobami, które chciały stworzyć aplikację androidową przy użyciu biblioteki czkawka_core i np. Fluttera. Od tego czasu śledziłem, czy na GitHubie, w Sklepie Play czy w F-Droid nie pojawią się aplikacje korzystające z mojego projektu, lecz mimo dość długiego czasu nie znalazłem żadnej z nich (co nie znaczy, że nie istnieją - po prostu możliwe, że źle szukałem albo te aplikacje nie chwalą się, że korzystają z tej biblioteki, co oczywiście nie jest wymagane).<br><br>Z racji tego, że taka aplikacja na Androida korzystająca z Czkawki zapewne jeszcze nie powstała i istnieje znaczna posucha na androidowym poletku, jeśli chodzi o funkcje, które Czkawka oferuje (jest obecnie wiele vibe-codowych aplikacji, które implementują pojedyncze funkcje), a do tego zawsze byłem ciekawy, czy da się stworzyć aplikację graficzną na Androida w czystym Ruście - postanowiłem więc spróbować taką aplikację napisać.</p><p>Próbowałem już tworzyć aplikacje na Androida, jednak zawsze odbijałem się od skomplikowanej konfiguracji i procesu budowania, konieczności korzystania z Android Studio i Android SDK, konfiguracji w plikach XML itd. (z podobnych względów nie przepadam za ekosystemami JS i C#, ale to osobny temat). Miałem z tym duży dysonans w porównaniu do C/C++, gdzie, przynajmniej na Linuksie, zazwyczaj wystarczy zainstalować kilka aplikacji lub bibliotek, a następnie skompilować całość jedną lub kilkoma komendami. Podobnie wygląda to w Rust, gdzie zwykle pojedyncza komenda wystarcza do zbudowania aplikacji przy minimalnej liczbie zewnętrznych zależności, co dodatkowo sprawia, że cross-kompilacja jest bardzo prosta.</p><p>Mając w pamięci te rzeczy, zacząłem eksperymentować z próbą stworzenia mostu Rust &lt;-&gt; Android przy użyciu JNI. Nawet mimo tego, że istnieje projekt jni-rs, który eliminuje konieczność ręcznego budowania takiego połączenia, to zarówno korzystanie z niego, jak i debugowanie, jest trudne, nawet przy użyciu Address Sanitizera.<br><br>Po wielu próbach, z których każda kończyła się niepowodzeniem, uznałem, że jest raczej mało prawdopodobne, abym przy ograniczonym czasie był w stanie ogarnąć i zaimplementować takie komponenty jak:</p><ul><li>zarządzanie uprawnieniami</li><li>zarządzanie przywracaniem stanu aplikacji i wymuszaniem jednoinstancyjności</li><li>obsługa wyboru plików i folderów</li><li>obsługa katalogów cache i konfiguracji</li><li>zarządzanie powiadomieniami</li></ul><p>Mając jasno określone cele, postanowiłem powierzyć zadanie pisania tej części kodu AI, samemu stając się jedynie recenzentem i testerem(wiem, że to niezbyt mądre - używać AI do tworzenia kodu w dziedzinie, o której mam małe pojęcie, lecz alternatywą byłoby spędzenie zapewne kilkudziesięciu godzin na poznawaniu ekosystemu Androida, pisaniu kodu i testach, a ta wiedza w przyszłości zapewne i tak by mi się więcej nie przydała). Zachowując zdrową zasadę, że tak napisanemu kodowi nie można ufać, spędziłem co najmniej kilka godzin na różnorakich testach, poprawkach i łamaniu sobie głowy nad tym, czemu aplikacja w losowych miejscach się wysypuje albo nie przywraca stanu.</p><p>O ile sam kod łączący aplikację z Androidem sprawiał problemy, o tyle stworzenie samego programu nie było już tak problematyczne. Pod spodem Cedinii znajduje się Slint, używany w Krokiecie, dzięki czemu w dużej mierze mogłem ponownie wykorzystać koncepcje i ogólny wygląd projektu do stworzenia tego. Jeśli chodzi o problemy, na które się natknąłem, było ich sporo, dotyczyły np. przewijania, klawiatury ekranowej czy ogólnej wydajności. Większość z nich zgłosiłem do odpowiednich bibliotek i część została już naprawiona (np. migające ikony), a na część znalazłem obejścia (np. używanie ListView zamiast ScrollView, co pozwalało na dowolne skalowanie modelu bez pogorszenia wydajności), jednak w dalszym ciągu obsługa klawiatury ekranowej sprawia dość duże problemy.<br><br>Samą aplikację najchętniej zrobiłbym, kopiując niemal 1:1 wygląd wersji desktopowej, jednak ze względu na dotykowy ekran i jego niewielkie rozmiary musiałem się nieco wysilić i metodą prób i błędów wymyślić GUI od nowa (co było dodatkowo problematyczne, bo nie znam i nigdy nie starałem się poznać dobrych praktyk tworzenia takich interfejsów). Podobnie jak więc w Krokiecie, wygląd aplikacji jest wysoce subiektywny i nie każdemu może się podobać.<br><br>Aplikacja jest podzielona na 4 części, które można przełączać przyciskami na dole, których kliknięcie otwiera:</p><ul><li>Ekran wyboru narzędzia (ekran główny)</li><li>Ekran wybranego narzędzia (domyślnie tryb wyszukiwania duplikatów, ale zmienia się, gdy użytkownik wybierze coś innego z ekranu głównego)</li><li>Ekran wyboru ścieżek do skanowania</li><li>Ekran ustawień, informacji o aplikacji itp.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*a6Uwgt1wPoiMnoyniW5Mvw.png" /><figcaption>Cedinia — różne ekrany</figcaption></figure><p>Co do narzędzi, to tutaj znajdziesz niemal wszystko co było w Krokiecie - wykrywanie duplikatów, pustych plików, folderów, obrazów, plików audio itd. Należy jednak pamiętać, że bardziej zasobożerne operacje mogą trwać zauważalnie dłużej niż na komputerze, gdzie lepsze chłodzenie, brak ograniczeń energetycznych i wyższa wydajność CPU sprawiają, że te same operacje wykonują się szybciej.<br><br>Większymi ograniczeniami, które wynikają ze środowiska, w którym uruchomiona jest aplikacja, są:</p><ul><li>brak możliwości wykrywania podobnych plików wideo - brakuje najzwyklejszego standardowego systemowego ffmpeg (przeglądając GitHuba znalazłem jednak czysto rustowe dekodery związane z obrazami/wideo, które w przyszłości będą mogły zastąpić ffmpeg - <a href="https://github.com/OxideAV">https://github.com/OxideAV</a> - zrobiłem już wstępne testy i niektóre formaty działają wystarczająco dobrze, by móc zacząć je używać już w następnej wersji)</li><li>ograniczenia w typie sprawdzanych obrazów - brak możliwości dynamicznego linkowania bibliotek takich jak libavif czy libraw na Androidzie powoduje, że pula wspieranych rozszerzeń jest ograniczona</li></ul><p>Planowałem wydać aplikację na F-Droid, ale obecnie polityka F-Droida nie pozwala na publikację aplikacji zawierających kod wygenerowany przez AI, więc aplikacja nie może zostać tam opublikowana bez przepisania tej części projektu. Spróbowałem też opublikować w Sklepie Play, założyłem konto deweloperskie i zacząłem coś przy tym grzebać, jednak okazało się, że wymagane jest zbieranie testerów, tworzenie paczek testowych i wiele innych mniej lub bardziej zajmujących rzeczy - problem w tym, że nie mam ochoty przy tym grzebać, bo wystarczająco wiele czasu zajmują mi inne zadania. Obecnie aplikacja jest dostępna jedynie w formie apk, które można pobrać z GitHuba i zainstalować ręcznie, jednak o ile możliwości pozwolą, chciałbym w przyszłości spróbować opublikować ją w zewnętrznych sklepach, umożliwiając tym samym łatwiejsze jej pobranie i aktualizację. Najprościej zapewne byłoby przekazanie dystrybucji aplikacji do jakiejś rzetelnej osoby/firmy (która ma już doświadczenie w publikacji takich aplikacji) i dodatkowo można by też poszukać jakiegoś innego sklepu, który nie wymagałby wykonywania aż tak dużej ilości rzeczy, przy dystrybucji programu.</p><p>Z racji tego, że posiadam tylko jedno urządzenie z Androidem, zwykły telefon, aplikacja została przetestowana wyłącznie na nim. Najprawdopodobniej nie będzie działać poprawnie na tabletach, a na telefonach o innych proporcjach ekranu mogą pojawić się drobne problemy z interfejsem. Zgłoszenia takich problemów wraz ze zrzutami ekranu aplikacji z pewnością pomogą w poprawie obsługi kolejnych urządzeń.<br><br>Na sam koniec zostawiłem bardzo ciekawą opowieść o etymologii nazwy. Cedynia to miejscowość, w której polskie wojska w czasach tworzenia państwa polskiego pokonały wojska możnowładców niemieckich. Z racji tego, że interesuję się czasami średniowiecznymi, uznałem to za dobrą nazwę (kontynuując tradycję nadawania nazw w żaden sposób niepomagających rozwikłać, do czego używa się danej aplikacji). W nazwie zmieniłem jednak y na i, ponieważ czasem szukam sobie w internecie materiałów o aplikacji, a w przypadku Czkawki czy Krokieta zamiast moich aplikacji wyskakiwały porady medyczne lub przepisy kulinarne.</p><p>Należy wspomnieć, że wybór tak nietypowych nazw jest celowym sygnałem dla użytkowników, że nie sprzedałem się żadnej korporacji - bo żadna z nich nie zdecydowałaby się na takie nazewnictwo dla swojego produktu. Zresztą wiem to z pracy, gdzie proponowałem bardziej niestandardowe nazwy, ale wszystkie zostały odrzucone na rzecz bezpiecznych, generycznych wariantów.</p><h3>Krokiet - porównywarka obrazów</h3><p>Porównywanie obrazów było jedną z ostatnich funkcji, których Krokietowi brakowało względem starej, GTK-owej Czkawki. Plany jej stworzenia miałem już dużo wcześniej, ale problemem były zarówno kwestie częściowo techniczne (tj. brak możliwości stworzenia zewnętrznego okna, podobnego jak w wersji GTK), jak i znaczenie trudniejsze zadanie polegające na wymyśleniu jak zrobić to lepiej niż było wcześniej. Lekko problematyczne było to, że sam z tej funkcjonalności wcześniej aż tak często nie korzystałem, więc nie miałem zbyt wielu przemyśleń, jak można by ją poprawić, a wiele osób wspominało, bym wzorował się na dupeGuru.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GBs9wqAPhkDYdojV2dzZxw.png" /><figcaption>Dupeguru i podgląd obrazów</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eBStAQZaeW1SZS38F7hi3w.png" /><figcaption>A tutaj Czkawka GTK</figcaption></figure><p>Ostatecznie wybrałem mieszane rozwiązanie - sam wygląd jest mocno wzorowany na tym, co oferuje Czkawka, dodałem tam również, zgodnie z tym jak robi to dupeguru, podstawowe informacje o obrazie. W przeciwieństwie do tych obu narzędzi ten tryb nie otwiera nowego okna, tylko działa jako nakładka na jego obecną treść, co pozwala na pełne wykorzystanie dostępnego miejsca i obejście problemów Slinta (choć to pewnie bardziej wina winit, którego używa pod spodem) z obsługą wielu okien.<br><br>Oczywiście moim głównym celem było zrobić to nie tak samo jak wcześniej, tylko lepiej, więc oprócz dodanych informacji o obrazach obecnie możliwe jest porównywanie obrazów w 4 różnych trybach:</p><ul><li><strong>normalnym</strong> - widoczne są dwa obrazy, jeden po lewej stronie, drugi po prawej</li><li><strong>różnicowym</strong> - widoczny jest jeden czarno-biały obraz, na którym na na czarno są rysowane piksele identyczne w obu obrazach a w skali szarości/na biało miejsca w którym się one różnią(obrazy o różnej rozdzielczości są skalowane tak, by miały identyczne rozmiary, i dla nich działa to nieco gorzej niż dla plików o identycznej rozdzielczości)</li><li><strong>overlay</strong> - nałożone są na siebie 2 obrazy i przy pomocy suwaka można regulować przezroczystość jednego z nich</li><li><strong>split</strong> - dwa obrazy są nałożone na siebie i pomiędzy nimi jest dodana pionowa linia, która oddziela pokazywane obrazy - po jej lewej jest jeden obraz, a po prawej drugi</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gEMocvGA4exZIZMNKopyhA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aMuH-g9pejY6hc3oCLcdLA.png" /><figcaption>Tryb split i diff</figcaption></figure><p>Według mnie to narzędzie jest już całkiem użyteczne, jednak wciąż widzę kilka potencjalnych ulepszeń, o których można by pomyśleć w przyszłości:</p><ul><li>zoom i przesuwanie obrazu, żeby dało się porównywać drobne detale na dużych zdjęciach</li><li>porównywanie więcej niż dwóch plików naraz</li><li>analogiczny tryb dla miniaturek w trybie wideo</li><li>wyświetlany pełniejszy zestaw metadanych/EXIF</li></ul><h3>AI w projekcie i poza nim</h3><p>Chyba nie trzeba nikomu tłumaczyć, czym jest AI ani jak ogromny postęp poczyniły modele językowe w ostatnich latach a nawet miesiącach. W Czkawce korzystałem z AI niemal od samego początku, choć przez długi czas ograniczałem się głównie do używania go jako alternatywy dla Google, DuckDuckGo czy Stack Overflow (do czego znakomicie się nadawało), podrzucania potencjalnych pomysłów na rozwiązanie problemów (co również robiło całkiem nieźle), a także autouzupełniania wpisywanych fragmentów kodu (z czym radziło sobie znacznie gorzej, ale i tak usprawniało mi pracę). Nie sądziłem jednak, że kiedykolwiek powierzę mu coś więcej niż drobne podpowiedzi.</p><p>Wszystko jednak zmieniło się jakieś trzy miesiące temu. Wtedy zacząłem na serio bawić się Copilotem i Claude. Ku mojemu zdziwieniu już po pierwszych próbach i testach tworzenia nieco bardziej skomplikowanych projektów, na odstrzał poszły moje wewnętrzne skrypty, które w pocie czoła rozwijałem na własny użytek przez ostatnie miesiące i lata. Byłem z nich dumny, ponieważ były zwięzłe i dobrze wykonywały swoje zadania, odpowiadając m.in. za zarządzanie i tworzenie obrazów dockerowych, fuzzing różnych projektów itd.<br><br>Przy minimalnym wysiłku stworzyłem ich ulepszone wersje, niwecząc w jednej chwili efekty pracy wielu lat. Wywołało to nawet krótkie epizody zwątpienia we własne umiejętności, a momentami także w życiowy wybór ścieżki zawodowej.<br><br>Zabawa dopiero się jednak zaczynała. Wraz z kolejnymi projekcikami dostrzegałem coraz więcej obszarów, w których AI mogło usprawnić pracę, ale też wiele takich, które nadal wymagały ode mnie sporego nakładu czasu i wysiłku. Ponieważ w pracy również zaczęliśmy eksperymentować z AI metodą prób i błędów, na podstawie zdobytych wspólnych doświadczeń, dobrych praktyk itd. udało mi się stworzyć zestaw reguł i skilli odpowiadających mojemu stylowi pracy. Dzięki temu modele zaczęły znacznie lepiej dostosowywać się do tego, co tworzę i z czym pracuję na co dzień.<br><br>Wracając jednak do Czkawki, to zacząłem wykorzystywać AI w programie na następujące sposoby:</p><ul><li><strong>Robienie wstępnych przeglądów pull requestów </strong>- AI jest naprawdę dobre w wykrywaniu literówek, błędów stylistycznych i innych podobnych problemów. Dzięki temu późniejsze ręczne sprawdzanie zmian jest znacznie sprawniejsze, ponieważ mogę skupić się na bardziej subtelnych błędach oraz kwestiach związanych ze stylem i architekturą, których AI nadal nie rozumie zbyt dobrze.</li><li><strong>Kategoryzacja i przegląd zgłoszeń</strong> - w ostatnim czasie liczba otwartych zgłoszeń spadła z 430 do 270. Częściowo jest to zasługa zamknięcia zgłoszeń związanych wyłącznie z GTK, jednak w większym stopniu wynikało to z tego, że przy użyciu AI przejrzałem każde zgłoszenie pod kątem tego, czy nadal jest aktualne, czy jego implementacja nie wymaga zbyt dużych zmian architektonicznych oraz w jaki sposób można by je potencjalnie zaimplementować lub naprawić.<br><br>Znacząco ułatwiło mi to weryfikację zgłoszeń, zamknięcie tych, które były już nieaktualne, a także podrzuciło kilka pomysłów na implementację funkcji, które w przeszłości uznałem za zbyt trudne do wykonania lub zbyt problematyczne do sensownego wpasowania w aplikację(Patrząc po ilości zgłoszeń, na pewno coś takiego by się przydało projektom takim jak Godot, które to z &gt;10000 zgłoszeniami błędów, utknęły w spirali nawarstwiającej się ich liczby, których jedynie mała część jest poprawnie weryfikowana i implementowana/zamykana, a tysiące z nich można by bez problemu zamknąć jako duplikaty/zaimplementowane już funkcje)</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*S-JbqtJEgKeqgCqRQZmNbA.png" /></figure><ul><li><strong>Unikanie powtarzalnych czynności -</strong> jak sami wiecie, programowanie nie zawsze sprowadza się do części kreatywnej. Czasem trzeba przenieść strukturę z jednego miejsca do drugiego, lekko zmieniając jej kształt, przygotować konwersję jednego modelu na inny albo rozbić zbyt duże pliki czy funkcje na mniejsze części. W wielu przypadkach jest to żmudna i monotonna praca polegająca na wykonywaniu prostych, powtarzalnych czynności, które nie wymagają głębokiej znajomości projektu. AI radzi sobie z takimi zadaniami całkiem dobrze, oszczędzając mi przy tym sporo czasu.</li><li><strong>Pełne przeglądy projektów -</strong> statyczne analizatory, takie jak Clippy czy Cppcheck, są bardzo dobrymi narzędziami i działają deterministycznie. Dla tego samego kodu przy każdym uruchomieniu zwracają identyczne wyniki. Ich raportom brakuje jednak zrozumienia kontekstu, w którym wykonywany jest analizowany kod. Tę lukę częściowo wypełniają modele językowe. <br><br>Claude potrafi wykonywać przeglądy kodu już w standardowej konfiguracji, jednak sposób prezentowania wyników oraz rodzaje wykrywanych problemów nie do końca odpowiadały moim potrzebom. Przygotowałem więc własny szablon raportu wraz z zestawem instrukcji określających, na jakich typach błędów model powinien się skupiać. Tak przygotowane raporty pozwoliły mi odnaleźć i naprawić w repozytorium dużą liczbę literówek, potencjalnych panik, problemów wydajnościowych, a przede wszystkim błędów logicznych, które przez długi czas pozostawały ukryte w kodzie i nie były widoczne na pierwszy rzut oka.<br><br>Co ciekawe, samo tworzenie takich raportów i ich zdolność do wykrywania interesujących problemów (mimo całej masy false positive&#39;ów) spodobały mi się na tyle, że uznałem je za wystarczająco użyteczne, by zacząć przygotowywać je również dla niektórych projektów open source. Część odnajdywanych w ten sposób problemów była już wcześniej zgłaszana przez użytkowników, jednak takie zgłoszenia zwykle opisywały jedynie objawy. Ustalenie, które miejsce w kodzie rzeczywiście odpowiada za dany problem, wymagało często sporego nakładu czasu i pracy, na co nie każdy miał możliwość lub ochotę.<br><br>Maintainerzy różnych projektów reagowali na takie raporty bardzo różnie. Projekty takie jak Tauri, CUPS, Boa czy OpenWrt zamykały zgłoszenia, wymagając rozbicia problemów na osobne tickety lub określając raporty mianem „low quality contribution”. Było to do pewnego stopnia zrozumiałe. Moim zamiarem nie było bowiem zgłaszanie każdego znalezionego problemu osobno, lecz przygotowanie raportu zawierającego najbardziej wiarygodne przykłady, aby zainteresować nim maintainerów. Liczyłem, że jako osoby najlepiej znające kod projektu będą w stanie szybko przejrzeć całość, odróżnić rzeczywiste błędy od halucynacji modelu i, jeśli uznają to za zasadne, oznaczyć je do poprawy lub od razu naprawić. Z drugiej strony były też projekty, które zaczęły poprawiać lub ulepszać elementy wskazane w raportach. W tej grupie znalazły się między innymi Lofty, Image-rs, Strawberry, FreeRDP oraz Hayro.<br><br>Przechodząc do przykładu błędów, jakie taki raport potrafi wykrywać, spójrzmy na ten kod:<br><br><em>x_min = min(x_min, points[i].x);<br>y_min = min(y_min, points[i].y);<br>x_max = max(x_max, points[i].x);<br>y_max = max(y_min, points[i].y);</em><br><br>Ani Clippy, ani cppcheck, ani żadne flagi GCC/Clang nie wykrywają tu problemu, a przy dokładniejszym spojrzeniu widać błąd. W ostatniej linii zamiast <em>max(y_min, ...)</em> powinno być max<em>(y_max, ...)</em>.<br><br>Ten błąd znajduje się w projekcie Wine od około dwóch lat, być może dlatego, że jest ukryty w 3000-liniowym pliku, a takich plików w repozytorium jest wiele. Skutki mogą być różne, od przesunięcia rysowania jakiejś linii o 1 px, po poważniejsze problemy, w skrajnym przypadku nawet nieprawidłowe renderowanie całego ekranu aplikacji.<br><br>Część takich błędów po wstępnej weryfikacji zgłosiłem, ale przez ograniczony czas nie wszystkie zostały sprawdzone, więc ktoś inny może je przejrzeć i ewentualnie zgłosić lub nawet naprawić, to raporty dla projektów takich jak Wine, Godot czy Systemd można znaleźć tutaj - <a href="https://github.com/qarmin/ClaudeReports">https://github.com/qarmin/ClaudeReports</a></li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4YBBlbu3l7tpX3IH_I_2bg.png" /><figcaption>Raport jaki sobie generuję, dla poszczególnych repo - w tym oczywiście też dla Czkawki</figcaption></figure><ul><li><strong>Tworzenie kodu w technologii lub języku, które są mi mniej znane - </strong>przy tym punkcie wszystkim powinna zapalić się lampka ostrzegawcza, ponieważ AI powinno przede wszystkim wspierać programistę w tworzeniu kodu, który byłby w stanie napisać samodzielnie, tylko znacznie większym nakładem czasu, a nie zastępować zrozumienie wykorzystywanej technologii. Tak jak wcześniej wspominałem, wykorzystałem takie podejście podczas tworzenia połączenia aplikacji z Androidem. Mimo że sama technologia nie była mi równie dobrze znana jak te, przy których coś grzebię na co dzień, to podstawowe zasady programowania pozostają w dużej mierze wspólne dla różnych języków i frameworków(przynajmniej większości). Z tego względu dużą uwagę poświęciłem weryfikacji wygenerowanego kodu, zarówno pod kątem jego jakości i zgodności ze stylem projektu, jak i poprzez szczegółowe testy, aby możliwie ograniczyć liczbę problemów, z jakimi mogliby spotkać się użytkownicy.</li></ul><p>Mój stosunek do kontrybucji tworzonych przy pomocy AI jest prosty i zgodny z duchem czasu. Nie mam problemu ze zmianami wygenerowanymi przez AI (sam zmergowałem już kilka takich zewnętrznych PR-ów), o ile kod jest poprawny, spójny stylistycznie z resztą projektu, odpowiednio przetestowany, a sam feature lub bugfix jest oczekiwany i zaakceptowany. Warto zauważyć, że to niemal nie różni się od tego, czego oczekuje od zwykłego ręcznie stworzonego PR.</p><p>Podsumowując, uważam, że z AI warto korzystać rozsądnie, ponieważ nie przekonują mnie skrajne podejścia do tego narzędzia. Z jednej strony są osoby skupiające się wyłącznie na jego wadach i nawołujące do całkowitego unikania AI, mimo że potrafi ono znacząco przyspieszyć proces tworzenia oprogramowania. Z drugiej strony są osoby bezkrytycznie akceptujące wszystko, co wygeneruje model, a następnie włączające taki kod do projektu bez jego odpowiedniej analizy i weryfikacji.<br><br>Ciekawym i dość kontrowersyjnym wydarzeniem w tej kwestii jest głośne ostatnio przepisanie w projekcie Bun niemal miliona linii kodu z Zig do Rust w ciągu kilku dni. Choć sam proces wzbudził sporo dyskusji, głównie ze względu dość luźne podejście do weryfikacji kodu wynikowego(tysiące unsafe), trzeba przyznać, że projekt dysponował wcześniej bardzo rozbudowanym zestawem testów(częściowo stworzonych też przy pomocy AI), które właśnie w dużej mierze umożliwiły sprawną weryfikację zgodności portu z oryginałem.</p><h3>Pozostałe zmiany</h3><p>Poza większymi nowościami opisanymi wyżej, w tej wersji wylądowała też masa drobniejszych poprawek i usprawnień - poniżej wybrane z nich:</p><ul><li><strong>Optymalizator wideo doczekał się opcji redukcji szumu robionej przez ffmpeg</strong>. Zaszumione filmy - np. nagrane po ciemku albo starym/słabszym sprzętem - kompresują się fatalnie, bo szum pojawia się w losowych fragmentach wideo i kodek ma problem by je skompresować. Wstępne odszumianie przed kodowaniem potrafi zbić rozmiar niektórych plików nawet kilkukrotnie, kosztem wygładzenia obrazu i utraty niektórych.</li><li><strong>Porównywanie podobnych wideo korzysta teraz z biblioteki similario_core zamiast vid_dup_lib_finder</strong>. vid_dup_lib_finder był w projekcie od bardzo dawna i z czasem doczekał się wielu poprawek, jednak ciągle miał trochę problemów, przestarzałe zależności oraz kłopoty z false positive&#39;ami, bo weryfikował jedynie maks. 30 s danego wideo. Nowa biblioteka powinna pozwolić ograniczyć liczbę niepoprawnie oznaczonych plików i zapewnić o wiele szerszą pulę konfiguracji do wyboru. Minusem jest to, że skanowanie plików trwa teraz nieco dłużej.</li><li><strong>Narzędzie podobnych obrazów zyskało możliwość wykrywania zdjęć odwróconych lub odbitych względem jednej ze swych osi</strong>. Co ciekawe, jest to zewnętrzna kontrybucja powstała przy pomocy AI, która - w przeciwieństwie do slopu, z którym czasem miałem do czynienia - działała całkiem dobrze i po bardziej szczegółowych testach (+ fuzzingu) trafiła do tej wersji.</li><li><strong>Popsute pliki można teraz sprawdzać kilkoma walidatorami naraz</strong>. Dodatkowo sprawdzanie wideo zostało rozbite na 2 tryby - szybki (ffprobe - czyta tylko nagłówki, np. po przywróceniu plików z uszkodzonego dysku można szybko i bardzo łatwo wykryć brak krytycznego moov) i wolny (ffmpeg - pełne dekodowanie, wykrywa uszkodzenia wewnątrz pliku, ale trwa znacznie dłużej i raczej w mniejszej liczbie przypadków jest to użyteczne). Dodano też wykrywanie uszkodzeń w kolejnych typach plików: fontach (TTF/OTF/TTC), formatach &quot;konfiguracyjnych&quot; + svg(JSON/XML/TOML/YAML/SVG), archiwach (7z/gz/tgz/tar/zst/bz2/xz) i kilku innych - tak, by jednym skanem dało się wyłapać jak najwięcej popsutych danych.</li><li><strong>Menu kontekstowe</strong> - jednym z chyba najwcześniej oczekiwanych usprawnień było wsparcie dla menu kontekstowego. Początkowo jego brak spowodowany był tym, że po prostu nie mogłem znaleźć żadnej funkcji, która potrafiłaby zrobić coś takiego. Grzebiąc jeszcze przy GTK 3, odkrywałem, że funkcja za funkcją, której chciałem do tego użyć, jest oznaczona jako przestarzała, a dodatkowo w międzyczasie dodałem kliknięcie pojedynczego prawego przycisku myszy jako otwieranie folderu nadrzędnego dla pliku. Nie powiem - akurat do tej opcji dość mocno się przywiązałem i często jej używałem, ale jednak konieczność dodania operacji na określonych rekordach spowodowała, że musiałem dodać to menu. Dostępne są w nim takie opcje jak np. wykluczenie danego elementu albo rezultatów z całego folderu.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-n9Sn-aP16U5XQZGCItYhQ.png" /></figure><ul><li><strong>W narzędziu do wykrywania podobnych wideo można teraz wyszukiwać podobne pliki przez porównanie ścieżki audio</strong>. Zazwyczaj jest to mniej zasobożerna operacja niż pobieranie i dekodowanie setek obrazów wyciągniętych z wnętrza pliku wideo i zwykle daje całkiem dobre rezultaty (choć częściej się myli, głównie ze względu na ciche sekcje wewnątrz plików wideo lub przez to, że dany fragment muzyczny może bez problemu występować w wielu plikach wideo). Dzięki temu, że jest to całkowicie niezależne od ffmpeg i działa w pełni w Rust, dodałem tę opcję również do Cedinii.</li><li><strong>Narzędzie do wykrywania pustych plików wykrywa teraz także pliki, które technicznie nie są puste, ale zawierają wyłącznie niedrukowalne znaki ASCII lub bajty zerowe (\0)</strong>. To pozwala usuwać niepuste pliki, które są często “śmieciowe”. Ta funkcjonalność w całości przejmuje rolę, jaką niegdyś pełniło (usunięte już dawno temu) narzędzie &quot;Zeroed Files&quot;.</li><li><strong>Przy wstępnej weryfikacji, czy plik jest duplikatem, czy też nie, obecnie oprócz odczytywania ~4 KB z początku pliku następuje również odczyt 4 KB z jego końca</strong>. Z tego, co zauważyłem, robi tak sporo narzędzi do deduplikacji, więc by nie być gorszym, i ja tak zrobiłem. Z moich testów wynika, że takie sprawdzenie zmniejsza liczbę plików przekazywanych później do pełnego odczytu zawartości o około 15-20% (ale to mocno zależy od danych wejściowych).</li><li><strong>Trochę zamieszania zrobiłem przy binarkach</strong>. Linuksowe paczki z dodatkowymi zewnętrznymi zależnościami mają teraz obsługę AVIF, przez co - ponad to, czego wymagały wcześniej - wymagają także libavif i libdav1d zainstalowanych w systemie. <br><br>Ze względu na bardzo długie buildy (2-3 razy wolniejsze niż mac arm, a nawet 4-6 razy wolniejsze niż linuksowe) przestałem budować i testować w CI binarki dla maców na Intelu (x86_64) - nie wiem, jaki odsetek użytkowników z tego korzystał, ale zalecam wszystkim przejście na Homebrew, bo tam, z tego co kojarzę, te paczki są dostępne. <br><br>Jak wcześniej wspominałem, do paczek z binarkami Czkawki na Windowsie podstępnie dorzuciłem też Krokieta - żeby osoby pobierające &quot;starą&quot; aplikację od razu miały pod ręką następcę i łatwiej było im się przesiąść. <br><br>W przypadku linuksowych/windowsowych buildów ze wszystkimi krokietowymi backendami dostępne są skrypty sh/bat ułatwiające uruchamianie poszczególnych backendów, jeśli któryś działałby niepoprawnie. <br><br>Na koniec doszły skrypty instalacyjne dla Linuksa (apt/dnf/pacman/zypper) i macOS (Homebrew) do instalacji zależności wymaganych przez buildy z zewnętrznymi zależnościami.</li><li><strong>Zaprzestanie aktualizowania wersji Flatpak</strong>. Próba zastąpienia we Flatpaku Czkawki Krokietem okazała się bardziej skomplikowana, niż zakładałem, więc ostatecznie z niej zrezygnowałem. Od początku poświęcałem zbyt dużo czasu na pakowanie i poprawki błędów z tym związanych, kosztem jej rozwoju. Mam nadzieję, że z czasem ktoś przejmie utrzymanie tej wersji, podobnie jak ma to miejsce w przypadku pakietów dla Homebrew, Winget czy MinGW, które są utrzymywane przez osoby niezwiązane z projektem.</li><li><strong>Główny kolor w aplikacji, jest pobierany z systemu. </strong>Po aktualizacji Slinta do wersji 1.17 zauważyłem, że część kontrolek i przycisków automatycznie zaczęła dostosowywać swoje kolory do motywu ustawionego przez użytkownika.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NCcG0Q7KHmAuJOE1ja1Oig.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9V_RWS8UfuVig8Q9gzd70g.png" /></figure><h3>Koniec</h3><p><strong>Repozytorium</strong> - <a href="http://github.com/qarmin/czkawka">github.com/qarmin/czkawka<br></a><strong>Pliki do pobrania wraz ze szczegółową listą zmian</strong>- <a href="http://github.com/qarmin/czkawka/releases">github.com/qarmin/czkawka/releases</a><br><strong>Licencja</strong> - MIT/GPL (w skrócie: za darmo, bez reklam, łączenia się z internetem czy zbierania statystyk o użytkowniku)</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c5dad2116793" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Krokiet/Czkawka 12.0]]></title>
            <link>https://medium.com/@qarmin/krokiet-czkawka-12-0-6fa09c43c3b9?source=rss-9c8ac0ce6dcd------2</link>
            <guid isPermaLink="false">https://medium.com/p/6fa09c43c3b9</guid>
            <category><![CDATA[rust]]></category>
            <dc:creator><![CDATA[Qarmin]]></dc:creator>
            <pubDate>Sun, 28 Jun 2026 11:25:58 GMT</pubDate>
            <atom:updated>2026-06-28T11:25:58.312Z</atom:updated>
            <content:encoded><![CDATA[<h3><strong>Krokiet/Czkawka 12.0 - New Android App, Image Comparison Tool, Context Menu, Final GTK Version of Czkawka, and AI</strong></h3><p>At some point in every project&#39;s life, there comes a moment when the right thing to do is not to add new features(or even apps), but to remove them. That moment has finally arrived. Interestingly, despite this rather drastic step, the number of deduplication applications I&#39;m developing has not changed.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*jZnTFThBV7zUJE9c0pmYKw.png" /><figcaption>I hope that Krokiet 12.0 and its new features will finally convince users to move away from the GTK version.</figcaption></figure><p>The aging 5-year-old Czkawka GTK desktop application is now being fully replaced by Krokiet, which is already 2 years old and still holding up very well. At the same time, Cedinia is making its way onto Android and will hopefully fill a significant gap in this area. It&#39;s not that Android lacks cleanup applications, but free versions are usually heavily restricted and often charge surprisingly high prices for very basic functionality.</p><h3>The Final Release of Czkawka GTK</h3><p>Czkawka GTK has been part of the repository since the very beginning. Starting with version 1.0, and even earlier if we&#39;re being precise, it was the only graphical version of the application for most of its existence.<br><br>Looking back, I still think choosing GTK instead of something like Qt was the right decision, despite all the problems I encountered, both with GTK itself and while trying to build something reasonable on top of it.<br><br>Most of the issues reported by users of the GTK version were eventually tracked down and fixed. Some problems, however, remained completely mysterious. Examples include the nearly transparent GUI reported by some users, or crashes caused by pasting text from the clipboard on certain Windows versions. No matter what I tried, I could never reproduce these bugs on my own machines. If I had been able to, I could have investigated them properly and either fixed them myself or reported them upstream to GTK or the maintainers of the problematic libraries.<br><br>Unable to keep up with the growing number of similar issues, I made the same decision any man in my position eventually would: I surrendered.<br><br>At that point it no longer seemed worth fighting an uphill battle, especially as Rust native alternatives such as Slint, Iced, and Egui started becoming more mature. One of their key goals was true cross platform support, unlike GTK, where non Linux systems often felt like second class citizens. Things seem to be improving now, but still much more slowly than I would like.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VfszjnBbdnh9nrrFdd9eMA.png" /></figure><p>In the previous version I experimented with porting Czkawka to non-existent yet GTK 5, hoping that a new framework might fix some of the problems I had been dealing with. However, the number of things that would have needed to be rewritten, including core components such as TreeView, was simply too large and would have required a significant amount of work, so I limited myself to improvements in parts of the existing code instead.</p><p>I introduced Krokiet, based on Slint some time ago. At first it lacked many features available in the GTK frontend, so replacing GTK immediately wasn&#39;t realistic. However, release after release the gap became smaller. Today Krokiet actually offers more tools and more advanced functionality than the GTK version ever did. In the previous release I added a popup shown on the first launch of the application, and in this version I additionally introduced a special, always visible unicorn button, that opens different popup with even more Krokiet propaganda.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*LPuxNfGPszpvIKBxPaeOsw.png" /></figure><p>Since announcing Krokiet, I had been spending less and less time on the GTK version, and with no one stepping in to take over its development, I eventually decided to stop working on that edition of the application altogether. At the same time, I increasingly encouraged users, in a progressively more aggressive manner, to move over to Krokiet.<br><br>A further factor that influenced this decision was observing various forums and websites, where I repeatedly saw Czkawka being presented as the default tool users would end up trying first instead of Krokiet. When it failed to work for various reasons, frustration was often directed both at the application and at me, even though an alternative already existed that avoided most of the reported issues. The lack of newer releases may eventually lead users to migrate, after what will hopefully be two years of gradual persuasion.</p><p>I am aware not everyone will do that. The name Czkawka, due to its unusual nature, has become embedded in many minds, so for those resistant to trying another program, I secretly added Krokiet binaries and certain messages into the only zip file I distribute for Czkawka, windows_czkawka_gui_gtk_412.zip, hoping they will subtly influence users.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*YSZUduSUQhUsim1H3zny0g.png" /><figcaption>Additional files, added to</figcaption></figure><h3>Cedinia, a new Android cleaning application</h3><p>In the past I had several conversations with people who wanted to build an Android application using the czkawka_core library and for example Flutter. Since then I monitored GitHub, the Play Store and F Droid to see whether any applications based on my project appeared, but despite a long period I did not find any. This does not necessarily mean they do not exist, it is possible I simply searched poorly or such applications do not advertise that they use the library, which is of course not required.<br><br>Given that such an Android application based on Czkawka probably still did not exist and there is a noticeable lack of Android tools offering the functionality Czkawka provides, currently there are many vibe coded applications implementing only single features, and also because I was always curious whether it is possible to build a graphical Android application in pure Rust, I decided to try writing one.<br><br>I had already attempted Android development before, but I always ran into the complexity of configuration and build processes, the need to use Android Studio and the Android SDK, XML configuration files, and so on. For similar reasons I dislike JS and C# ecosystems, but that is a separate topic. I experienced a strong contrast compared to C and C++, where at least on Linux it is usually enough to install a few libraries and compile everything with one or a few commands. Rust is similar in this regard, a single command is usually enough to build an application with minimal external dependencies, which also makes cross compilation very easy.<br><br>With that in mind I started experimenting with a Rust to Android bridge using JNI. Even though there is the jni-rs project which removes the need for manual bindings, both using it and debugging remain difficult, even with Address Sanitizer.<br><br>After many attempts, each ending in failure, I concluded that with limited time it was unlikely I would be able to properly implement components such as permission management, application state restoration and enforcing single instance behavior, file and folder picking, cache and configuration directory handling, and notification management.</p><p>With those goals clearly defined, I decided to delegate this part of the code to AI, becoming mainly a reviewer and tester. I am aware this is not an ideal approach, using AI to generate code in an area I do not fully understand, but the alternative would have been spending tens of hours learning the Android ecosystem, writing code and tests, knowledge that I would most likely never use again. Treating AI generated code as inherently untrustworthy, I still spent several hours on testing, fixes and debugging why the application randomly crashed or failed to restore state.<br><br>While the Android integration layer was problematic, building the actual application logic itself was not as difficult. Under the hood Cedinia uses Slint, the same UI framework used in Krokiet, which allowed me to reuse many concepts and the overall project structure. The issues I encountered were numerous, including scrolling, on screen keyboard behavior and general performance. I reported many of these problems to the relevant libraries and some were already fixed, for example flickering icons, while others were worked around, for example using ListView instead of ScrollView, which allowed arbitrary model scaling without performance degradation. However, keyboard handling still remains problematic.<br><br>I would have preferred to design the application by copying the Krokiet desktop layout almost one to one, but due to touch input and small screen size I had to redesign the interface through trial and error. I also do not know and never tried to learn proper mobile UI design principles. As in Krokiet, the final UI is subjective and may not be to everyone’s taste.<br><br>The application is divided into four sections, switched using buttons at the bottom:</p><ul><li>Tool selection screen(main screen)</li><li>Selected tool screen(by default duplicate search mode, changes depending on user selection)</li><li>Path selection screen for scanning</li><li>Settings and about screen</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*a6Uwgt1wPoiMnoyniW5Mvw.png" /><figcaption>Cedinia screens</figcaption></figure><p>Regarding tools, it includes almost everything from Krokiet, duplicate detection, finding empty files, empty folders, similar images, audio files and more. It should be noted that more resource intensive operations may take significantly longer than on a desktop, where better cooling, no power limits and higher CPU performance make the same operations faster.<br><br>The main limitations imposed by the runtime environment are:</p><ul><li>No support for detecting similar video files, due to the absence of a standard system ffmpeg. While researching I found Rust based decoders related to images and video, which in the future may replace ffmpeg, <a href="https://github.com/OxideAV">https://github.com/OxideAV</a>. I already performed initial tests and some formats work well enough to start using them in the next version.</li><li>Limitations in supported image formats, due to the inability to dynamically link libraries such as libavif or libraw on Android, which reduces the supported extension set.</li></ul><p>I initially planned to release the application on F-Droid, but current F-Droid policy does not allow publishing applications containing AI generated code, so the app cannot be published there without rewriting that part of the project. I also attempted to publish it on the Google Play Store, set up a developer account, and started going through the process, but it quickly turned out to require collecting testers, preparing testing builds, and handling a number of other relatively time consuming steps. The main issue is that I simply do not have the motivation to deal with all of this, as I already spend enough time on other tasks. Currently the application is available only as an APK that can be downloaded from GitHub and installed manually. If possible, I would like to publish it later in external stores to make installation and updates easier. The simplest option would likely be delegating distribution to a reliable person or company with experience in publishing such applications, or finding an alternative store with fewer requirements.</p><p>Due to having only a single Android device, a standard phone, the application was tested exclusively on it. It is likely that it will not function properly on tablets, and on phones with different aspect ratios, minor UI issues may occur. Bug reports including screenshots of the application will be helpful in improving support for additional devices.<br><br>At the end comes a short explanation of the name’s etymology. Cedynia is a place where Polish forces in the early days of the Polish state defeated German feudal forces. Since I am interested in medieval history, I considered it a good name, continuing my tradition of choosing names that do not make it any easier to understand what the application does. I changed the y to i because searching online for the name often returned unrelated medical or culinary results instead of my projects.</p><h3>Krokiet, image comparison tool</h3><p>Image comparison was one of the last missing features in Krokiet compared to the old GTK based Czkawka. I had planned it earlier, but there were both technical issues, such as the inability to create an external window similar to the GTK version, and the more difficult conceptual challenge of improving on the previous design. It was somewhat problematic because I did not use this feature frequently myself, so I had limited intuition on how to improve it, while many users suggested basing it on dupeGuru.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GBs9wqAPhkDYdojV2dzZxw.png" /><figcaption>Comparing images in Dupeguru</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eBStAQZaeW1SZS38F7hi3w.png" /><figcaption>…and in Czkawka Gtk</figcaption></figure><p>In the end I chose a hybrid approach. The layout is strongly inspired by Czkawka, while also adding basic image metadata similar to dupeGuru. Unlike both tools, this mode does not open a new window, instead it works as an overlay on top of the existing view, which allows full use of available space and avoids Slint limitations, although this is likely more an issue of winit underneath and its multi window handling.<br><br>The goal was also to improve on the previous approach, so beyond image metadata the tool now supports four comparison modes:</p><ul><li><strong>Normal</strong>, two images side by side, left and right</li><li><strong>Diff</strong>, a black and white image where identical pixels are black and differing areas are shown in grayscale or white, images with different resolution are scaled to match, which reduces accuracy in some cases</li><li><strong>Overlay</strong>, two images stacked with a transparency slider</li><li><strong>Split</strong>, two images overlaid with a vertical divider, left side shows one image, right side the other</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gEMocvGA4exZIZMNKopyhA.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*aMuH-g9pejY6hc3oCLcdLA.png" /><figcaption>Split and diff modes</figcaption></figure><p>In my opinion the tool is already quite useful, but there are still several potential improvements:</p><ul><li>zooming and panning for detailed inspection</li><li>comparing more than two files at once</li><li>similar mode for video thumbnails</li><li>more complete metadata and EXIF display</li></ul><h3>AI in the project and beyond</h3><p>It hardly needs explaining what AI is or how significant the progress in language models has been in recent years, and even in recent months. In Czkawka I have used AI almost from the very beginning, although for a long time I mostly limited myself to using it as an alternative to Google, DuckDuckGo or Stack Overflow (for which it worked remarkably well), suggesting potential approaches to problem solving (which it also handled quite well), as well as autocompleting code fragments being typed (where it performed noticeably worse, but still helped improve my workflow). I did not expect that I would ever entrust it with anything beyond minor hints.<br><br>Everything changed about three months ago. That was when I started seriously experimenting with Copilot and Claude. To my surprise, even after the first attempts and tests with slightly more complex projects, my internal scripts were effectively pushed aside - scripts I had carefully developed for my own use over months and years. I was proud of them, as they were concise and did their job well, handling tasks such as managing and building Docker images, fuzzing various projects, and more.</p><p>With minimal effort, I created improved versions of them, undoing in an instant the results of years of work. This even led to brief episodes of self doubt regarding my own skills, and at times also my career choices.</p><p>The real experimentation had only just begun. With each small project, I started noticing more areas where AI could improve my workflow, but also many where it still required a significant amount of time and effort on my side. Since we were also experimenting with AI at work through trial and error, I was able, based on shared experience and emerging best practices, to develop a set of rules and skills tailored to my working style. As a result, the models started adapting much better to what I build and work with on a daily basis.<br><br>Returning to Czkawka, I began using AI in the project in the following ways:</p><ul><li><strong>Initial pull request reviews -</strong> AI is very good at detecting typos, stylistic issues, and other similar problems. This makes later manual review of changes significantly more efficient, as I can focus on more subtle errors and on aspects related to style and architecture, which AI still does not understand very well.</li><li><strong>Issue categorization and triag</strong>e - recently, the number of open issues dropped from 430 to 270. Part of this was due to closing issues related exclusively to GTK, but a larger factor was that I used AI to go through every issue and assess whether it was still relevant, whether its implementation would require major architectural changes, and how it could potentially be implemented or fixed. <br><br>It significantly simplified issue triage, allowed me to close those that were already obsolete, and also surfaced several ideas for implementing features that I had previously considered too difficult or too problematic to integrate into the application in a sensible way. Looking at the number of issues, this kind of approach would likely be useful for projects such as Godot, which, with over 10,000 bug reports, have ended up stuck in a self reinforcing spiral of accumulating issues, where only a small fraction is properly verified and implemented or closed, while thousands could likely be dismissed as duplicates or already implemented features.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*S-JbqtJEgKeqgCqRQZmNbA.png" /></figure><ul><li><strong>Removing repetitive tasks -</strong> as you know, programming is not always about the creative part. At times, it comes down to moving a structure from one place to another with slight adjustments, converting one model into another, or breaking overly large files or functions into smaller components. In many cases, this is tedious and monotonous work consisting of simple, repetitive actions that do not require deep knowledge of the project. AI handles such tasks quite well, saving me a considerable amount of time in the process.</li><li><strong>Full project reviews -</strong> static analyzers such as Clippy or Cppcheck are very good tools and behave deterministically. For the same code, they always produce identical results on every run. However, their reports lack an understanding of the context in which the analyzed code is executed. This gap is partially filled by language models.<br><br>Claude can perform code reviews out of the box, but the way it presents findings and the types of issues it focuses on did not fully match my needs. I therefore created my own report template along with a set of instructions defining exactly which kinds of problems the model should look for. These customized reports helped me uncover and fix a large number of typos, potential panics, performance issues, and, most importantly, logical errors that had remained hidden in the codebase for a long time and were not obvious at first glance.<br><br>Maintainers of different projects reacted to these reports in very different ways. Projects such as Tauri, CUPS, Boa, and OpenWrt closed the issues, either asking for each finding to be split into a separate ticket or describing the reports as a &quot;low quality contribution&quot;. That was understandable to some extent. My intention was not to report every potential issue individually, but to prepare a report containing the most convincing examples in the hope of drawing the maintainers&#39; attention to it. Since they know their codebases inside out, I expected they would be able to quickly review the full report, distinguish genuine issues from model hallucinations and, if appropriate, mark them for fixing or fix them directly. On the other hand, some projects started fixing or improving the issues highlighted in these reports. Among them were Lofty, Image rs, Strawberry, FreeRDP, and Hayro.<br><br>To illustrate the kind of issues these reports can uncover, let&#39;s take a look at the following code:<br><br><em>x_min = min(x_min, points[i].x);<br>y_min = min(y_min, points[i].y);<br>x_max = max(x_max, points[i].x);<br>y_max = max(y_min, points[i].y);</em><br><br>Neither Clippy, Cppcheck nor GCC or Clang flags detect this issue. A closer look shows that the last line uses <em>y_min </em>instead<em> of y_max</em>.<br><br>This bug has been present in the Wine project for about two years, perhaps because it is buried inside a 3000 line source file, and there are many files of similar size throughout the repository. Its impact can range from something as minor as a line being rendered one pixel off, to much more serious issues, potentially even causing an application&#39;s entire window to render incorrectly.After performing an initial review, I reported some of the issues I found. Due to limited time, however, many findings remain unverified and may still be worth investigating. If anyone is interested, the reports for projects such as Wine, Godot, and Systemd are available here: <a href="https://github.com/qarmin/ClaudeReports">https://github.com/qarmin/ClaudeReports</a></li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4YBBlbu3l7tpX3IH_I_2bg.png" /><figcaption>Report generated for individual repositories, including Czkawka</figcaption></figure><ul><li><strong>Writing code in technologies or languages I am less familiar with - </strong>this is probably the point where alarm bells should start ringing. AI should primarily assist developers in writing code they could produce themselves, albeit with considerably more time and effort, rather than replace their understanding of the underlying technology. As I mentioned earlier, I applied this approach while implementing the Android integration for my application. Although I was less familiar with the Android ecosystem than with the technologies I work with on a daily basis, the fundamental principles of software development are largely shared across most programming languages and frameworks, at least in most cases. For that reason, I spent a significant amount of time reviewing the generated code, evaluating both its quality and its consistency with the project&#39;s coding style, as well as performing extensive testing to minimize the likelihood of issues affecting end users.</li></ul><p>To sum up, I believe AI should be used in a sensible way, as I am not convinced by extreme attitudes toward this tool. On one side, there are people who focus exclusively on its drawbacks and advocate avoiding AI altogether, even though it can significantly speed up software development. On the other side, there are people who uncritically accept everything generated by a model and then merge such code into a project without proper analysis and verification.<br><br>A recent and rather controversial example in this context is the widely discussed rewrite in the Bun project, where nearly a million lines of code were ported from Zig to Rust in a matter of days. The process itself sparked a lot of debate, mainly due to a relatively loose approach to validating the resulting code, including thousands of unsafe blocks. At the same time, it has to be acknowledged that the project had previously built a very extensive test suite, partly generated with the help of AI, which largely enabled efficient verification of the port’s correctness against the original implementation.<br><br>My stance on AI assisted contributions is straightforward and in line with the current direction of the industry. I have no issue with changes generated by AI, I have already merged several external pull requests of this kind, as long as the code is correct, stylistically consistent with the rest of the project, properly tested, and the feature or bugfix is genuinely desired and accepted. It is also worth noting that in practice this differs very little from a manually written pull request.</p><h3>Additional changes</h3><p>Apart from the major features described above, this version also includes a large number of smaller fixes and improvements, some of which are listed below:</p><ul><li><strong>The video optimizer now includes a noise reduction option powered by ffmpeg.</strong> Noisy videos, for example recordings made in low light or captured with older or lower quality hardware, tend to compress poorly because noise appears as random fluctuations across frames, which makes it difficult for video codecs to efficiently encode them. Applying denoising as a preprocessing step before encoding can in some cases reduce file size by several times, at the cost of a smoother image and some loss of fine detail.</li><li><strong>Video similarity comparison now uses the similario_core library instead of vid_dup_lib_finder</strong>. The previous library had been part of the project for a long time and accumulated various fixes over time, but it still suffered from several issues, including outdated dependencies and a tendency toward false positives, mainly because it only analyzed up to 30 seconds of each video. The new library should reduce the number of incorrectly flagged files and provide a much broader range of configuration options. The downside is that file scanning is now somewhat slower.</li><li><strong>The similar image tool now supports detection of images that have been flipped or mirrored along one of their axes.</strong> Interestingly, this was an external contribution created with the help of AI, which - unlike the low quality “slop” I have sometimes encountered - performed well and, after more extensive testing including fuzzing, was included in this release.</li><li><strong>Corrupted files can now be checked using multiple validators at once.</strong> In addition, video validation has been split into two modes. The fast mode uses ffprobe and only reads file headers, which makes it useful for quickly detecting critical issues such as a missing moov atom after recovering files from a damaged disk. The slow mode uses ffmpeg and performs full decoding, allowing it to detect internal file corruption, although it is significantly slower and less frequently necessary in practice.<br><br>Support for corruption detection has also been extended to additional file types, including fonts (TTF, OTF, TTC), configuration-like formats such as JSON, XML, TOML, YAML, and SVG, as well as archives (7z, gz, tgz, tar, zst, bz2, xz), among others. The goal is to enable a single scan to surface as many corrupted data cases as possible.</li><li><strong>Context menu support </strong>- one of the earliest requested improvements was the addition of a context menu. Initially, its absence was simply due to the fact that I could not find a suitable function that would implement this behavior. While working with GTK 3, I kept running into functions I intended to use for this purpose only to discover they had already been marked as deprecated.<br><br>In the meantime, I had introduced a feature where a single right mouse button click would open the parent folder of a given file. I grew quite attached to this behavior and used it frequently, but the need to perform actions on specific records eventually made it necessary to implement a proper context menu.<br><br>The menu now provides options such as excluding a selected item or excluding results from an entire folder</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-n9Sn-aP16U5XQZGCItYhQ.png" /></figure><ul><li><strong>The similar video detection tool now supports searching for similar files by comparing the audio track.</strong> This is typically a less resource-intensive operation than extracting and decoding hundreds of frames from inside a video file, and it usually produces fairly good results, although it is more prone to errors. These inaccuracies mainly come from silent sections within videos or from the fact that the same musical fragment can legitimately appear across multiple unrelated video files.<br><br>Because this approach is completely independent of ffmpeg and implemented entirely in Rust, I have also added the same option to Cedinia.</li><li>The empty file detection tool now also identifies files that are not technically empty, but contain only non-printable ASCII characters or zero bytes (\0). This makes it possible to remove files that are non empty in a strict sense, but still effectively represent junk data.<br><br>This functionality fully replaces the role once served by “Zeroed Files” tool, removed in past.</li><li><strong>During the initial duplicate check, instead of only reading approximately 4 KB from the beginning of a file, the tool now also reads 4 KB from its end.</strong> This approach is commonly used in many deduplication tools, so I adopted the same strategy for consistency and effectiveness.<br><br>Based on my tests, this additional check reduces the number of files that need to undergo full content comparison by roughly 15-20%, although the exact improvement depends heavily on the input dataset.</li><li><strong>I have introduced some changes to the prebuild binaries:</strong><br><br>Linux packages that include additional external dependencies now support AVIF, which means that beyond the dependencies they required previously, they now also require libavif and libdav1d to be installed on the system.<br><br>Due to very long build times, typically two to three times slower than on Apple Silicon Macs and even four to six times slower than on Linux, I have stopped building Intel based macOS (x86_64) binaries in CI. I don’t know how large the user base on this architecture is, but I recommend switching to Homebrew, where these packages should be available for this architecture.<br><br>As mentioned earlier, I also bundled Krokiet into the Windows binaries of Czkawka in a somewhat hidden way, so that users downloading the “old” application would immediately have access to its successor and find migration easier.<br><br>For Linux and Windows builds that include all Krokiet backends, there are now shell and batch scripts available to simplify launching individual backends in case any of them do not work correctly.<br><br>Finally, installation scripts have been added for Linux distributions (apt, dnf, pacman, zypper) and macOS (Homebrew) to handle installation of dependencies required by builds that rely on external libraries.</li><li><strong>Discontinuing Flatpak updates</strong>. Attempting to replace Czkawka with Krokiet on Flatpak turned out to be more complicated than I had expected, so I eventually abandoned the effort. From the beginning, I spent too much time packaging and distributing the application at the expense of actually developing it. I hope that, over time, someone from the community will take over maintaining the Flatpak package, just as is already the case for the Homebrew, Winget, and MinGW packages, which are maintained by people outside the project.</li><li><strong>The main color used in the application is now derived from the system theme.</strong> After updating Slint to version 1.17, I noticed that some controls and buttons started automatically adapting their colors to the user’s selected theme.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*NCcG0Q7KHmAuJOE1ja1Oig.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9V_RWS8UfuVig8Q9gzd70g.png" /></figure><h3>The End</h3><p><strong>Repository -</strong> <a href="http://github.com/qarmin/czkawka">github.com/qarmin/czkawka</a><br><strong>Releases and changelog -</strong> <a href="http://github.com/qarmin/czkawka/releases">github.com/qarmin/czkawka/releases</a><br><strong>License -</strong> MIT GPL, meaning free to use, no ads, no access to the internet, no telemetry or tracking</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6fa09c43c3b9" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Czkawka/Krokiet 11.0]]></title>
            <link>https://medium.com/@qarmin/czkawka-krokiet-11-0-c95ee35eccc2?source=rss-9c8ac0ce6dcd------2</link>
            <guid isPermaLink="false">https://medium.com/p/c95ee35eccc2</guid>
            <category><![CDATA[duplicate]]></category>
            <category><![CDATA[rust]]></category>
            <dc:creator><![CDATA[Qarmin]]></dc:creator>
            <pubDate>Sat, 14 Feb 2026 11:14:30 GMT</pubDate>
            <atom:updated>2026-02-14T11:14:30.096Z</atom:updated>
            <content:encoded><![CDATA[<h3>Czkawka/Krokiet 11.0 — blamaż GTK, czyszczenie EXIF, optymalizacja wideo, przycinanie czarnych pasów i nowe logo</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hbJZoHCeDa_YywTEeABuPw.png" /></figure><p>Czkawka i Krokiet już dawno przestały być prostymi aplikacjami do wyszukiwania duplikatów. Z czasem potrzebowałem coraz więcej funkcjonalności, więc je dodawałem bez większego przemyślenia, przez co projekt rozrastał się dość chaotycznie. Potrzebowałem chwili przerwy, żeby poukładać pewne rzeczy w głowie i w kodzie. Udało się to częściowo zrobić, więc w tej wersji zacząłem od nowa dodawać kolejne narzędzia. W efekcie obecne wydanie jest największe do tej pory i jednocześnie najciekawsze.</p><p>Zazwyczaj po wydaniu nowej wersji, pojawia się wiele komentarzy, że znowu popsułem działanie niektórych trybów, aplikacja przestała się włączać czy masę pytań, czemu ciągle nie ma dodanych bazowych funkcjonalności(tj. filtrowania wyników po skanowaniu czy nowego przycisku do zaznaczania plików, stworzonych po 23:00 w 11 niedzielę roku nieparzystego).</p><p>Po wydaniu poprzedniej wersji, nie było inaczej. Wśród morza regresji tj. niewyświetlanie wyników w CLI, moją uwagę przykuły freezy i mikroskopijny podgląd plików w aplikacji gtk, a działo się to w miejscach w kodzie, których nie tykałem ostatnio. Po dłuższej chwili poszukiwań i konsternacji z tym związanej, okazało się że błąd nie został stworzony przeze mnie, ale jest powiązany z jedną biblioteką którą używam. A tą biblioteką jest…</p><h3>GTK — stabilność i błędy</h3><p>Ponad 5 lat temu, gdy zaczynałem tworzyć ten program, jednym z głównych wyzwań, było wybranie frontendu, jaki Czkawka będzie używać. W tamtym czasie rustowe biblioteki graficzne dopiero raczkowały, próbując znaleźć swoją niszę. Zatem całkiem rozsądnym rozwiązaniem wydawał mi się wybór QT lub GTK. Ostatecznie wybór padł na GTK 3, głównie przez to że uważałem, że na linuxie(mojej głównej platformie do programowania, jak i na początku jedynym wspieranym systemie) będzie działało to najstabilniej.</p><p>Oprócz głównie moich błędów i błędów bindingów, na linuxie aplikacja działała całkiem stabilnie. W między czasie GTK 4 wyszło, co wymusiło przepisanie znacznej części aplikacji a również po wielu trudach udało mi się skompilować aplikację na windowsa i macos. I tutaj właśnie zaczęły się niejako problemy. Problemy z kompilacją, losowe crashe niewystępujące na linuxie czy problemy z ikonami i renderowaniem, powodowały że zacząłem kwestionować słuszność wyboru tej biblioteki.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TFm--EjoHLeGT4qaXdWrCw.png" /><figcaption>Mikroskopijny podgląd obrazu 12MP</figcaption></figure><p>Jednak wraz z merge requestem — <a href="https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/8627">https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/8627</a> — dotychczasowa sielanka minęła. Zmiana związana z GtkImage, którego używałem od czasów GTK 3, spowodowała, że w absolutnie każdej wersji programu używającej GTK 4, aż do poprzednio aktualnej10.0 — podgląd obrazów którego ich używał, zmniejszył się do rozmiarów pojedynczych pikseli. Zgodnie z opisem zmian, ta poprawka to breaking change i jej autorzy oczekiwali że niektóre z projektów będą mieli problemy przy aktualizacji. Na moje nieszczęście problem dotyczył, właśnie Czkawki. GTK 4.20 jest obecne we wszystkich nowszych systemach, przez co ich użytkownicy, niemal nie mogą korzystać z obecnej aplikacji.</p><p>Obecna wersja(11.0) zawiera poprawkę podmieniającą GtkImage na GtkPicture, naprawiająca prosto problem. Jednak ta zmiana oprócz pewnej niedogodności, spowodowała nieco większy problem — problem z moją wiarą w stabilność GTK. W przeciwieństwie do Krokieta, gdzie w plik binarny jest wbudowywana biblioteka Slint tworząca i zarządzająca całym interfejsem, Czkawka używa dynamicznie linkowanego GTK, który aż się prosi ażeby losowo z powodu błędu lub naprawy błędu(jak to miało miejsce w moim przypadku) zmienić swoje działanie, pozostawiając użytkowników jak i twórców z ręką w nocniku. Domyślam się, że ta zmiana dotknęła raczej nieduży procent aplikacji, jednak akurat dla mnie i tych co używaja Czawki, było to bolesne.</p><h3>Czyszczenie plików z danych EXIF — nowe narzędzie</h3><p>Pod wpływem chwili lub nie do końca uchwytnego impulsu postanowiłem dodać do programu nowe narzędzie do usuwania danych EXIF. Sam korzystałem z tej funkcjonalności raczej rzadko i do tej pory wyłącznie z <a href="https://github.com/szTheory/exifcleaner">https://github.com/szTheory/exifcleaner</a>, który nie jest już rozwijany i dodatkowo używa Electrona(straszne, czyż nie?). Przeszkadzało mi też to, że narzędzie nie pozwalało podejrzeć zmian przed konwersją.</p><p>Od kilku wersji liczba narzędzi w programie pozostawała stała, 11, a ja skupiałem się głównie na ich ulepszaniu oraz na zmianach upraszczających logikę późniejszego dodawania i zarządzania nimi. W końcu uznałem więc, że to dobry moment, aby rozszerzyć ich pulę.</p><p>Wiedziałem, że po stronie Rusta istnieją create do obsługi danych EXIF, jednak okazało się, że w porównaniu z popularnym exiftool wciąż sporo im brakuje. Po wstępnej analizie brałem pod uwagę jedynie trzy biblioteki, które były ostatnio aktualizowane:</p><ul><li>nom_exif</li><li>little_exif</li><li>guf_exif</li></ul><p>Guf_exif odrzuciłem, ponieważ z tego, co zauważyłem, brakowało mu generycznego interfejsu do zarządzania wszystkimi tagami. Nom_exif już obecnie wykorzystuję do odczytywania rotacji z obrazów, aby móc je poprawnie obracać do podglądu. W przeszłości, przy użyciu fuzzera, zgłosiłem w nim wiele błędów, dzięki czemu stał się bardzo rock-solid i nie powoduje crashy ani freezów(przynajmniej w moich zastosowaniach). Problemem okazał się jednak brak możliwości modyfikacji danych, a zgłoszenie dotyczące tej kwestii nie było aktualizowane od 2024 roku.</p><p>W efekcie na placu boju pozostała jedynie biblioteka little_exif. Równolegle z próbą zrozumienia, jak działa sam EXIF, uczyłem się korzystać z little_exif. Samo doprowadzenie biblioteki do działania nie było trudne. Potrzebowałem jedynie wyciągać z pliku informacje o obecnych tagach, ich nazwę, id oraz grupę, a następnie umożliwić ich usuwanie.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7RRpKRxJs7wgeWJky8rDqw.png" /><figcaption>Przykład wyszukiwanych tagów EXIF</figcaption></figure><p>Zgodnie ze swoim zwyczajem do testów podszedłem dość dokładnie, co zaowocowało kilkoma utworzonymi issue, dziesiątkami crashy oraz uświadomieniem sobie, że na EXIF świat się nie kończy, bo istnieją również inne formaty, takie jak XMP czy iTXt. Bez dogłębnej znajomości formatu EXIF byłem w stanie naprawiać crashe jedynie do momentu, aż fuzzer przestał je wykrywać podczas testów obejmujących około 1 miliona inputów, oraz zmodyfikować kilka API, które były dla mnie mało wygodne w użyciu.</p><p>W obecnym kształcie narzędzie pozwala na usuwanie danych EXIF z kilku popularnych formatów obrazów. Obsługuje mniejszą liczbę formatów plików oraz rozpoznawanych tagów niż exiftool, jednak do moich zastosowań na razie to wystarcza(bardzo chętnie widziałbym tutaj wsparcie dla innych formatów metadanych, więc wypatruję bibliotek oferujących takie funkcjonalności by rozszerzyć funkcjonalność narzędzia w przyszłości).</p><p>To narzędzie, jak i większość niżej opisanych funkcjonalności, jest dostępna jedynie w Krokiecie i CLI. Czkawka Gui, będąca w trybie maintenance, nie otrzyma tego trybu, ani żadnej innej większej funkcjonalności(no chyba, że ktoś to zaimplementuje samemu, do czego szczerze zachęcam).</p><h3>Gtk4-rs i nowe tajemnicze błędy</h3><p>Od pierwszych wersji Czkawka korzysta z GTK, do budowania swojego gui. GTK jak powszechnie wiadomo, jest napisane w C i przy używaniu go w Rust, trzeba albo samemu tworzyć kod łączący te dwa języki albo korzystać z gotowej biblioteki. Jako początkujący próba budowania od zera interoperacyjności C &lt;-&gt; Rust byłaby proszeniem się o problemy, więc jedynym sensownym rozwiązaniem było użycie wtedy raczkującego gtk-rs. Biblioteka ta dawała całkiem dobre możliwości wywoływania funkcji napisanych w C z poziomu Rusta, jednak połączenie moich ówczesnych umiejętności z wczesną wersją samej biblioteki sprawiało, że musiałem mierzyć się z wieloma dziwnymi błędami.</p><p>Z czasem kod, mimo niezbyt dobrego stylu w jakim był zapisany, to stawał się “rock-solid” i coraz trudniej było go popsuć(*na typowym linuxowym systemie). Ze względu na problemy występujące na innych systemach oraz trudności w dalszym rozwijaniu projektu przeniosłem większość wysiłków na Krokieta, a samą aplikację GTK uznałem za deprecated, co oznaczało, że nie zamierzam implementować w niej nowych funkcji, narzędzi ani trybów. Jednak obecnie większość użytkowników Czkawki nadal korzysta z frontendu GTK, więc nie mogłem ich pozostawić samych sobie i postanowiłem, na ile to możliwe, naprawiać pojawiające się błędy.</p><p>Zacząłem nieco poprawiać kod, by łatwiej byłoby naprawiać problemy, działało to wszystko do czasu gdy nie stworzyłem jednego testu, który podobnie jak inne testy, weryfikował wynik funkcji usuwania z listy, elementów, które nie były powiązane z innymi elementami. Zasada działania była prosta:</p><ul><li>Iteruj po modelu</li><li>Jeśli iter wskazuje na zwykły element to zapisz go do wektora</li><li>Jeśli iter wskazuje na nagłówek lub model się skończył, to jeśli wektor elementów ma tylko 1 element to usuń zarówno poprzedni nagłówek jak i ten element(np. gdy mamy dwa podobne obrazy, jeden usuniemy z dysku, to z listy trzeba usunąć ten drugi, bo to bezsensowne trzymać w gui element bez podczepionego do niego podobnego obiektu)</li></ul><p>Ten test po prostu się zawieszał. Zacząłem więc analizować, dlaczego niemal identyczne testy przechodzą poprawnie, a ten jeden nie. Najbardziej problematyczne było to, że test zazwyczaj działał bez zarzutu, gdy kompilowałem go osobno, natomiast przestawał działać, gdy był kompilowany w grupie z innymi. W międzyczasie, na starszej wersji Czkawki kompilowanej ręcznie, zacząłem zauważać sporadyczne zwisy po usuwaniu plików, jednak wtedy uznałem to za regresję spowodowaną innymi zmianami w kodzie.</p><p>Pierwszą hipotezą było niepoprawne użycie API, na przykład operowanie na zwolnionej pamięci lub błędnym iteratorze. Ani AddressSanitizer, ani Valgrind nie wykazały jednak żadnych nieprawidłowości. W trakcie przygotowywania minimalnego przykładu do zgłoszenia błędu w gtk4-rs zauważyłem, że problem występuje głównie w trybie release i przy większej liczbie testów. Po zgłoszeniu błędu w repozytorium gtk4-rs utworzyłem również zgłoszenie w repozytorium Rusta, ponieważ problem zaczął pojawiać się od wersji 1.86.0. Użytkownik yvt, a następnie ds84182, ustalili, że kod w gtk4-rs jest unsound, ponieważ w funkcji iter_next parametr iter jest niemutowalną referencją bez wewnętrznej zmienności, mimo że w praktyce jej stan ulega zmianie.</p><pre>#[doc(alias = &quot;gtk_tree_model_iter_next&quot;)]<br>fn iter_next(&amp;self, iter: &amp;TreeIter) -&gt; bool {<br>    unsafe {<br>        from_glib(ffi::gtk_tree_model_iter_next(<br>            self.as_ref().to_glib_none().0,<br>            mut_override(iter.to_glib_none().0),<br>        ))<br>    }<br>}</pre><p>Poprawka zmieniająca iter na typ &amp;mut TreeIter trafiła do nowej wersji, gtk4-rs 0.11.0, której wydanie było jednym z blokerów wydania nowej wersji aplikacji.</p><h3><strong>Optymalizator wideo — nowe narzędzie</strong></h3><h4>Zmiana kodeka</h4><p>Będąc pod ciągłą presją posiadania na dysku wystarczającej ilości wolnego miejsca, aby mieć możliwość zapisywania losowych głupich memów i filmików, postanowiłem skupić się na zmniejszeniu zajętości już istniejącej przestrzeni dyskowej. Mając kilka dużych plików wideo w formacie MJPEG, uznałem, że najprościej będzie je przekonwertować do bardziej efektywnego formatu, co pozwoliłoby kilkukrotnie zmniejszyć ich rozmiar i zaoszczędzić kilkanaście GB miejsca. Mógłbym używać do tego ffmpega ręcznie dla każdego pliku, ale wyszukiwanie takich filmów i konwersja byłyby żmudne i tak narodził się pomysł na nowe narzędzie.</p><p>Podobnie jak w przypadku pozostałych narzędzi, logika tego jest prosta:</p><ul><li>zbierana jest lista plików, wyglądających od strony rozszerzenia jak wideo</li><li>przy użyciu ffprobe, pobierane są metadane tegoż oto wideo</li><li>wyświetlana jest lista plików w gui</li><li>użytkownik może wybrać pliki i je “zoptymalizować” — zmieniając kodek, opcjonalnie ograniczając rozdzielczość wideo, przy pomocy ffmpeg i jego specyficznych opcji</li></ul><p>równie dobrze zapewne działałby zwykły prosty skrypt pythonowy, jednak ja lubię mieć wszystko w jednym miejscu, z możliwościa ręcznej selekcji wyników.</p><h4>Przycinanie czarnych pasów i statycznej zawartości</h4><p>Nieco bardziej zaawansowaną logikę ma inny tryb w tym narzędziu — przycinania wideo.</p><p>Przeglądając moją kolekcję zabawnych filmików, zauważyłem, że wiele z nich ma losowe czarne pasy po bokach lub kolorowe, nieruchome obramówki. Taka czarna ramka nie tylko zajmuje cenne kilobajty, ale też utrudnia komfortowe oglądanie materiału.</p><p>Jednym ze znienawidzonych przeze mnie rzeczy, jest oglądanie poziomego filmiku, który ma czarne pasy u góry i u dołu na zwykłym monitorze — taki filmik na pełnym ekranie zajmuje ułamek dostępnego miejsca — no tragedia.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0kPx7EW91hfwXp31TIr8Ug.png" /><figcaption>Przykład filmiku z czarnymi pasami u góry i na dole, uruchomionego na pełnym ekranie</figcaption></figure><p>Istnieją programy, w których wystarczy zaznaczyć obszar, do którego chcemy przyciąć wideo, jednak zazwyczaj dodają one do materiału swoje logo i/lub działają tylko dla jednego pliku naraz, przez co takie operacje są bardzo czasochłonne dla użytkownika. Dlatego postanowiłem na początek dodać prosty tryb, który automatycznie usuwa czarne pasy i to bez dodawania czegokolwiek do wideo. Starając się utrzymać wszystko w prostocie, logika wygląda następująco:</p><ul><li>Wideo są wyszukiwane, a z nich pobierane są metadane przy użyciu ffprobe, takie jak rozmiar, bitrate, kodek itp.</li><li>Następnie przy użyciu ffmpeg pobierana jest pierwsza klatka wideo, która nadpisuje wymiary obrazu z metadanych (ffprobe nie uwzględnia rotacji wideo, a czasem zwraca wymiary różniące się od rzeczywistych).</li><li>Dla każdego wideo pobierane jest maksymalnie 60 klatek w odstępach co minimum 0,25 s (dla 15-sekundowego wideo klatka co 0,25 s, dla 60-minutowego wideo co około 1 minutę).</li><li>Na każdej klatce tworzony jest obrys zawartości, tak aby wszystko poza zaznaczonym prostokątem było traktowane jako czarne pasy. Proces przebiega z czterech kierunków (góra, dół, lewo, prawo). Od przeciwnej krawędzi sprawdzane są linie, w których co najmniej określony procent pikseli (domyślnie 90%) ma wszystkie trzy składowe RGB poniżej zadanej wartości (domyślnie 20). Jeśli warunek jest spełniony, sprawdzana jest kolejna linia; jeśli nie, przerywa się analizę z danego kierunku i ustala miejsce krawędzi prostokąta.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/921/1*HqpsR56-hqwVXLqLIxAyjw.png" /><figcaption>Przykład w jaki sposób jest robione zwężanie</figcaption></figure><ul><li>Następnie kolejne kroki powtarzają tę samą procedurę dla kolejnych klatek, przy czym prostokąt z zawartością może tylko się rozszerzać, czyli zmniejszać powierzchnię przyciętą. Wynika to z zasady, że jeśli na jednej klatce pojawia się element w miejscu, które na innych klatkach jest ciemne, nie można go usunąć.</li><li>Jeżeli prostokąt ma rozmiar równy ramce wideo, oznacza to, że przycinanie nie jest potrzebne, bo nie ma czego przyciąć. W przeciwnym wypadku ffmpeg wykonuje operację przycięcia obszaru do określonego rozmiaru.</li></ul><p>Niektóre filmiki są jednak bardziej problematyczne — mają białe napisy lub tło w innych kolorach. Początkowo myślałem o rozszerzeniu algorytmu tak, aby wykrywał również kolory inne niż czarny, np. biały, ale problemem stało się to, że liczba analizowanych kolorów/możliwości szybko by rosła i rozwiązanie przestałoby być w miarę proste do implementacji i nie działałoby dla wszystkich oczekiwanych przez użytkowników sytuacji.</p><p>Początkowo myślałem o bardziej złożonym podejściu: pobieraniu 60 klatek i analizowaniu każdej kolumny oraz wiersza względem poprzedniej klatki, co jednak znacznie komplikowało porównania. Wpadłem więc na pomysł, by porównywać wszystkie wyciągnięte klatki z pierwszą klatką, stosując prostą funkcję obliczającą różnice między pikselami w dwóch klatkach — dla każdej składowej RGB danego piksela wyznaczana jest różnica bezwzględna względem piksela o tych samych współrzędnych w pierwszej ramce. Na przykład: RGB(50, 0, 50) — RGB(75, 50, 25) = RGB(25, 50, 25). W ten sposób powstaje nowy obraz, którego wartości pikseli odzwierciedlają zmiany względem pierwszej klatki.</p><p>Korzystając z tego, że obszary mało różniące się od pierwszej klatki, w takim obrazie były bardzo ciemne, nic nie stało na przeszkodzie by użyć poprzedniej techniki do przycinania obszarów. Zatem niezmienione fragmenty obrazu, bez względu na oryginalny kolor, ostatecznie stawały się czarne i zaczynały nadawać się do ich prostego przycinania</p><p>Początkowo niezbyt byłem do tej funkcjonalności przekonany, jednak efekt przerósł moje oczekiwania i działa to bardzo dobrze w mojej kolekcji głupich filmików.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*R-HNfmuoiVAtgoTXkXpgNA.png" /><figcaption>Widoczne filmiki i ich automatycznie przycięte wersje</figcaption></figure><p>Oczywiście nie jest narzędzie idealne i posiada trochę wad i ograniczeń:</p><ul><li>czasem pozostawia cienkie czarne linie — spowodowane zapewne tym, że utrata jakości spowodowała rozmycie obrazu, przez co te czarne linie, nie są już takie do końca czarne, bo nachodzi na nie czasem obraz, więc algorytm ich nie usuwa</li><li>tryb podobnej zawartości może przycinać również zawartość wideo — algorytm nie rozróżnia czy nieruchoma część wideo jest ważna czy też nie (zauważyłem, że np. ruchome wideo plaży, gdzie górna część ciągle wskazuje na 0niemal nieruchome niebo, często jest przycinane do obszaru samej plaży, gdzie algorytm wykrywa jakiś ruch)</li><li>konieczne jest transkodowanie — to powoduje utratę jakości i czasem nawet zwiększenie rozmiaru — można przy tym skorzystać z konwersji do h265, by ten rozmiar zmniejszyć, skoro i tak trzeba video transkodować do jakiegoś formatu</li><li>jeśli zmiany obrazu w określonych miejscach są widoczne jedynie bardzo rzadko, to 60 wyciąganych w równych odstępach klatek, może je ominąć i algorytm może niepoprawnie uznać że jest to dobre miejsce do odcięcia — niestety ale to kompromis pomiędzy prędkością przetwarzania a w miarę dobrymi wynikami — testowanie każdej klatki wideo, dawałoby niemal pewność, że nic ważnego nie zostanie przycięte, ale wraz z dłuższym filmem liniowo zwiększał by się czas jego przetwarzania(obstawiam, że film 1h, by się przetwarzał ~5h w programie, zamiast kilku/kilkunastu sekund obecnie).</li></ul><h3>Złe nazwy — nowy tryb</h3><p>Kolejnym nowym trybem o który nikt nie prosił i który pewnie mało kto będzie używał, jest wyszukiwanie potencjalnie niepożądanych nazw.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*N3-r2bAFHmHfFA8jsVOHbw.png" /></figure><p>Rozszerzenia z dużych liter, emoji w nazwie, spacje na początku końcu, znaki non ascii, limitowana ilość znaków, czy ich zduplikowane sekwencje, to wszystkie dostępne możliwe tryby wyszukiwania.</p><p>By zapewnić w miarę sensowne nazwy, kroki są wykonywane w ścisłej i niemożliwej do zmiany kolejności, dzięki czemu “ 🚀 świstak .txt” przy usuwaniu emoji, znaków non-ascii i pustych miejsc, nie stanie się nagle “rocket swistak.txt” tylko “swistak.txt”.</p><p>Te tryby przyszły mi do głowy jako pierwsze, patrząc na dość dziwne nazewnictwo plików, jakie to posiadam na komputerze, więc nie wykluczam że w kolejnych wersjach rozszerzę listę wyszukiwanych/zmienianych elementów/zmodyfikuję obecne.</p><h3>Podgląd parametrów wideo i jego klatek</h3><p>Ciekawszą rzeczą w tej wersji, jest dodana możliwość wyświetlania parametrów wyszukanych plików wideo. Oprócz samej ścieżki do wideo, jak i jego rozmiaru i czasu , teraz w gui(dodałem to zarówno do krokieta, jak i wersji GTK, która jak przypominam jest w trybie maintenance) wyświetlają się również takie informacje jak długość wideo, bitrate, kodek, framerate oraz jego rozmiar w pikselach.</p><p>Przy użyciu zewnętrznego crate, wszystkie te informacje są sczytywane przez ffprobe — więc by mieć dostęp do nich, musi to być zainstalowane obok ffmpeg. Najstarsze issue dotyczące tej funkcjonalności były sprzed 3 lat, a przez ten czas nikt się tym zgłoszeniem nie zajął ani ja nie czułem jakiejś specjalnej chęci by tym się zajmować.</p><p>Problem się zaczął, gdy moja kolekcja głupich filmików się rozrosła i przestałem nadążać z usuwaniem duplikatów. Bez parametrów widocznych bezpośrednio przy filmiku, konieczne było ciągła ciągła ręczna weryfikacja, przez ich odpalanie, tak by usunąć te gorszej jakości/mniejszego rozmiaru.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UTcbQ8aVV8HlfkDqlPOKBQ.png" /><figcaption>Przykład dodanych parametrów wideo, wraz z klatką z obecnie zaznaczonego video(Lobster 2015)</figcaption></figure><p>Kolejnym usprawnieniem w widoku wideo, było dodanie podglądu do rezultatów. Mimo że wydaje się to proste, to takie nie było. Do tworzenia podglądu zaprzęgnąłem ffmpeg, podobnie jak to obecnie działa przy hashowaniu i porównywaniu plików wideo. Jest to operacja czasochłonna(nawet setki ms), więc przy tworzeniu ich ad-hock widoczne byłoby znaczne opóźnienie, co opóźniałoby klikanie. Dodatkowo trzeba by albo albo gubić te obrazy i przy każdym klikaniu je na nowo generować albo wrzucać to do jakiegoś cache, który komplikowałby logikę.Problematyczne byłoby też to, że przy klikaniu albo całe gui by freezowało i dopiero wtedy by obrazek po całkowitym przetworzeniu się wyświetlał(tak obecnie dzieje się dla zwykłych obrazów, wyłączając operacje przy użyciu ffmpeg) albo trzeba byłoby to ładować w osobnym wątku, co komplikowałoby logikę.</p><p>Uznałem, że obsługa tego w całości po stronie gui w taki sposób jest niepotrzebne trudne, więc postanowiłem przerzucić to do core. Obecnie został dodany nowy krok do skanowania, po którym(jeśli tylko użytkownik zaznaczył, że chce generować podgląd), dla każdego pliku który jest podobny do innych plików, jest generowany podgląd. Nazwa pliku, czas modyfikacji, jego rozmiar jak i miejsce z którego pobierana jest ramka, są hashowane, tak by dla tych samych parametrów tworzyć zawsze tą samą ścieżkę, by w przy kolejnych skanach nie trzeba było ponownie ich generować — alternatywnie możnaby zapisywać to info do cache, ale wtedy stracilibyśmy możliwość wyboru miejsca wyciąganej klatki. Alternatywnie jest możliwość podglądu wideo, jako obraz składający się z 9 obrazów podglądowych wyciąganych z wnętrza filmu — z racji na konieczność generacji aż 9 zamiast 1 części, jest to dużo wolniejsze.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4eNxJvnGD5ApOXRYHiR_mw.png" /><figcaption>Alternatywny podgląd wideo w formie siatki 3x3 (1670 2023)</figcaption></figure><p>Podgląd obrazów jest dostępny jako opcja tylko z poziomu Krokieta. Wersja CLI ma zahardkodowane nie tworzenie podglądu(bo tak szczerze mówiąc, to tam nie ma sensu) a wersja GTK, to już sami wiecie.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pzGcupZYok_EDRcZP57Mzw.png" /><figcaption>W trybie przycinania, widoczne są prostokąty wskazujące miejsce przycięcia wideo</figcaption></figure><p>By uniknąć nadmiernego zużycia pamięci i procesora — wyciąganie podglądu z pliku przy użyciu ffmpeg, ma udostępniony tylko 1 wątek na wideo(domyślnie ffmpeg pozwala na stworzenie tylu wątków ile jest dostępne w systemie), co skutecznie zapobiega błędom braku pamięci(podczas testów kilkukrotnie program mi się wysypywał, im tego nie zaimplementowałem),</p><h3><strong>Inne zmiany</strong></h3><h4>Okno dialogowe w Czkawce GTK, z informacją o Krokiecie</h4><p>Zdziwiło mnie, że tak mało osób o tym wie, że Krokiet jest nową i ulepszoną wersją Czkawki — mimo, że próbowałem to na wiele różnych sposobów przekazać. Zatem kierując się zasadą “bo ja wiem lepiej”, postanowiłem dodać nowe okno dialogowe, wprost mówiące o tym wszystkim użytkownikom. Jego wyświetlanie jest zależne od tego czy istnieje magiczny plik mówiący że już to okno zostało wyświetlone(powinno się wyświetlić tylko raz) lub czy użytkownik ustawił magiczną zmienną CZKAWKA_DONT_ANNOY_ME</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FnKzl2b_P8J5q9SUvl1xbA.png" /></figure><h4>Nowe Logo Krokieta</h4><p>Poprzednie logo, było na szybko i ręcznie narysowanym jednorożcem, w formie zwykłego obrazka rastrowego. Taki obrazek pasował gdy określał aplikację, która była eksperymentalna. Dziś jak powszechnie Krokiet to aplikacja niemal idealna i takiego niemal idealnego loga mi trzeba było. Obecnie przedstawia jednorożca z tarczą, hełmem i sztandarem z krokietem — czemu akurat takie? — a ja odpowiem, a dlaczego nie(choć zapewne ma to związek z audiobookiem Trylogii Husyckiej , którą słuchałem w między czasie)? Dodatkowo jest w formacie svg, no i ładnie się teraz skaluje.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/863/1*9gOZ4HFDvvX_8GnUe79mUA.png" /><figcaption>Nowa maskotka programu</figcaption></figure><h4>Czyszczenie przestarzałych rekordów z opóźnieniem w cache</h4><p>Zwykły flow aplikacyjny to:</p><ul><li>zbieranie listy plików</li><li>ładowanie cache</li><li>przetwarzanie plików</li><li>zapisywanie cache</li><li>wyświetlanie rezultatów</li></ul><p>zazwyczaj najdłużej trwającym zadaniem jest przetwarzanie plików, a następnie zbieranie listy plików, ponieważ operują na dysku, weryfikując wiele małych plików. Ładowanie i zapisywanie cache operuje na pojedynczych plikach, co powinno być szybkie, prawda? No nie do końca — istnieje domyślnie włączona opcja usuwania przestarzałych rekordów, tak by trzymać rozmiar cache w ryzach, minimalizując czasy ładowania i zapisywania. Przy każdej operacji ładowania, wszystkie rekordy są weryfikowane czy istnieją i czy czas modyfikacji i rozmiar się zgadzają. Zwykle trwa to w miarę szybko, jednak w przypadku np. zewnętrznych dysków łączonych przez sambę lub hdd, operacje na wielu małych plikach, mogą trwać niepotrzebnie długo. Z tego też powodu te operacje postanowiłem opóźnić, tak by wykonywały się tak rzadko jak tylko to możliwe. Zatem postanowiłem przy ładowaniu cache, jedynie raz na tydzień, wykonywać te operacje. Dodatkowo oddałem do użytku użytkownikom, możliwość ręcznego czyszczenia cache, po to by w razie konieczności można było w dowolnej chwili wyczyścić.</p><h4>Dodanie możliwości skanowania pojedynczych plików</h4><p>Od zarania dziejów, jedyną możliwością skanowania, było wybranie wpierw folderu jaki będzie skanowany. Takie coś oczywiście ma sens, bo obejmuje 100% przypadków użycia i pozawala na przeskanowania plików w każdej kombinacji plików. W niektórych sytuacjach wymaga to tworzenia nowych folderów i przenoszenia tam plików, by móc je przeskanować. Niektórzy z użytkowników, chcieli mieć możliwość porównywania tylko jednego rezultatu obrazu/pliku/wideo czy nie ma on podobnych plików na dysku. Obecnie by do tego poziomu przejść trzeba stworzyć nowy folder, wrzucić tam ręcznie plik, dodać w aplikacji i ustawić go jako referencyjny folder. Obecnie dodałem możliwość dodawania pojedynczych plików, dzięki czemu po dodaniu ich do aplikacji i ustawieniu jako ścieżki referencyjnej, możnasprawdzać teraz pojedyncze pliki bez konieczności ich przenoszenia. Muszę powiedzieć, że akurat mi to niezbyt przeszkadzało i przez te wszystkie lata po prostu nauczyłem się testować pliki, bawiąc się nimi po dysku.</p><h4>Dźwięki</h4><p>Cichą nowością w tej wersji jest dodanie dźwięków do Krokieta po zakończeniu skanowania. W przypadku dłuższych skanowań, łatwo było się rozproszyć innymi działaniami i nie zauważyć jego końca. Zrobiłem to na próbę dodając jedynie jeden dźwięk by zobaczyć czy warto i być może w przyszłości zostanie to rozszerzone o kolejne. Dźwięk końca skanowania to stworzony przez moją szczękę dźwięk, który zapewne nie będzie zbyt przyjemny, więc istnieje możliwość ustawienia dowolnego innego dźwięku przez odpowiednią zmienną środowiskową</p><h4>Tłumaczenia SI</h4><p>Niemal cały kod rustowy w projekcie, systemy budowania czy skrypty są rozwijane głównie przeze mnie, bo sam większość tego kodu napisałem i ją całkiem nieźle rozumiem. Jedynym wyjątkiem są tłumaczenia, w których to odpowiadam za większość teksów, jedynie dla języka angielskiego i polskiego. Inne języki takie jak włoski czy francuski, w przeszłości były aktywnie uzupełniane, część obecnie jest aktualizowana np. portugalsko-brazylijski a część była uzupełniana od zera przy pomocy tłumacza internetowego, z powodu zapytań o akurat ten język. Obecnie wspieranych języków jest kilkadziesiąt języków do daje dziesiątki tysięcy słów do przetłumaczenia, i co wersję kilkadziesiąt/kilkaset nowych fraz wymaga aktualizacji. Skala tego może nie jest jakaś ogromna dla niektórych projektów, ale dla mnie jest i przy kolejnych zmienionych stanąłem więc przed wyborem:</p><ul><li>zostawić tłumaczenia na wpół przetłumaczone</li><li>usunąć/ukryć tłumaczenia niekompletne</li><li>ręcznie tłumaczyć brakujące teksty</li></ul><p>By zapobiec kolejnym zapytaniom o dodanie już wcześniej istniejącego języka do aplikacji, oraz by nie straszyć użytkowników niepełnymi tekstami, postanowiłem(nie posiadając w zanadrzu kilku setek lub tysięcy dolarów na zlecenie tłumaczenia, ani własnej ekipy tłumaczy) najpierw ręcznie tłumaczyć określone zwroty przy pomocy ogólnodostępnych stron do tłumaczenia a następnie używać do tego skryptu, który pobiera model zongwei/gemma3-translator:4b, który to działa niezbyt szybko(max kilka słów na sekundę na moim sprzęcie) jednak wygląda, że daje lepsze rezultaty niż inne metody.</p><h4>Refaktory kodu w Czkawce Gtk</h4><p>Powiedzieć, że kod w Czkawkce Gtk nie jest zbytnio ładny, to jakby nic nie powiedzieć. Tworzyłem go głównie jak moja znajomość rusta była dość prymitywna — z biegiem czasu projekt się rozrósł, przez co ze względu na rozmiar całkowitego przepisania projektu Gtk w lepszym stylu nigdy się nie podjąłem, jedynie łatając na ślinę to co można było połatać. W obecnej wersji zrobiłem nieco więcej tego samego, wydzielając np. operacje działania na operatorach do wspólnych funkcji czy grupowania powiązanych ze sobą elementów(tj. scrollview, treeview i gestureclick), dzięki czemu kod powinien być obecnie nieco bardziej przystępny. W głowie miałem przygotowanie kodu pod potencjalne wsparcie dla GTK 5, jednak z uwagi na rozległość wymaganych zmian(np. konieczność przejścia z TreeView który jest bazą pod wszystkie modele na coś innego) nie mam zamiaru dalej się tym zajmować — ale jeśli ktokolwiek ma wystarczająco wiele odwagi może się tego podjąć(nie mam problemu nawet z szerokim użyciem AI do tego zadania, o ile kod będzie w miarę dobrej jakości, zgodny stylistycznie z resztą programu i przejdzie szereg moich recenzji)</p><h3>Koniec</h3><p>Repozytorium — <a href="http://github.com/qarmin/czkawka">github.com/qarmin/czkawka</a></p><p>Pliki do pobrania wraz z listą zmian — <a href="https://github.com/qarmin/czkawka/releases">https://github.com/qarmin/czkawka/releases</a></p><p>Licencja — MIT/GPL w zależności od programu (czyli w skrócie za darmo)</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c95ee35eccc2" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Czkawka/Krokiet 11.0]]></title>
            <link>https://medium.com/@qarmin/czkawka-krokiet-11-0-0f6cea385934?source=rss-9c8ac0ce6dcd------2</link>
            <guid isPermaLink="false">https://medium.com/p/0f6cea385934</guid>
            <category><![CDATA[gtk4]]></category>
            <category><![CDATA[duplicate]]></category>
            <category><![CDATA[rust-programming-language]]></category>
            <dc:creator><![CDATA[Qarmin]]></dc:creator>
            <pubDate>Sat, 14 Feb 2026 11:14:09 GMT</pubDate>
            <atom:updated>2026-02-14T11:14:09.203Z</atom:updated>
            <content:encoded><![CDATA[<h3>Czkawka/Krokiet 11.0 — fighting with GTK, EXIF cleaning, video optimizer, black bar cropping and new logo</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hbJZoHCeDa_YywTEeABuPw.png" /></figure><p>Krokiet and Czkawka have long ceased to be simple duplicate-finding apps. Over time, I needed more and more functionality, so I added features without much planning, which caused the project to grow in a rather chaotic way. I needed a break to sort some things out in my head and in the code. I managed to do that partially, so in this version I started adding new tools. As a result, the current release is the largest so far and at the same time the most interesting.</p><p>Usually, after releasing a new version, many comments appear saying I broke some modes, the app stopped launching or asking why basic features are still missing (like filtering results after a scan or a new button to select files created after 11 PM on the 23rd Sunday of an odd numbered year).</p><p>It was no different after the previous release. Amid a sea of regressions, like results not showing in the CLI, my attention was caught by freezes and a microscopic file preview, happening in parts of the code I hadn’t touched recently. After a long investigation it turned out the bug wasn’t caused by me, but was linked to one of the libraries I use. And that library is…</p><h3>GTK — bugs and stability</h3><p>Over five years ago, when I started developing this program, one of the main challenges was choosing the frontend Czkawka would use. At that time, Rust GUI libraries were still in their infancy, trying to find their niche. Choosing between QT or GTK seemed like a reasonable solution. Ultimately, I went with GTK 3, mainly because I believed it would be the most stable on Linux, which was my primary programming platform and initially the only supported system.</p><p>Apart from my own mistakes and some binding issues, the application ran fairly stable on Linux. In the meantime, GTK 4 was released, which required rewriting a significant part of the application. After many struggles, I also managed to compile the application for Windows and macOS. That’s when the problems started in earnest. Compilation issues, random crashes that didn’t occur on Linux and problems with icons and rendering made me start questioning whether choosing this library had been the right decision.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TFm--EjoHLeGT4qaXdWrCw.png" /><figcaption>Microscopic 12 MP image preview</figcaption></figure><p>However, with the merge request — <a href="https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/8627">https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/8627</a> — the previous idyll ended. The change affected GtkImage, which I had been using since GTK 3, and caused the image preview in every version of the program using GTK 4, up to the previous release 10.0, to shrink to the size to a few pixels. According to the description of the change, this was a breaking change and the authors expected some projects to encounter issues when updating. Unfortunately, Czkawka was affected. GTK 4.20 is now present on most of recent systems, meaning their users can hardly use the application.</p><p>The current version (11.0) includes a fix that replaces GtkImage with GtkPicture, straightforwardly solving the problem. However, this change, aside from some minor inconvenience, introduced a bigger issue — a problem with my confidence in GTK stability. Unlike Krokiet, where the Slint library is embedded in the binary and handles the entire interface, Czkawka relies on dynamically linked GTK, which is prone to changing behavior randomly due to a bug or a bug fix (as happened in my case), leaving both users and developers confused. I assume this change affected a relatively small percentage of applications, but for me and those using Czkawka, it was painful.</p><h3>EXIF File Cleaning — New Tool</h3><p>Somewhat spontaneously, I decided to add a new EXIF data removal tool to the program. I had personally used this feature only occasionally, and until now relied entirely on <a href="https://github.com/szTheory/exifcleaner">https://github.com/szTheory/exifcleaner</a>, which is no longer maintained and also uses Electron (terrible, right?). It also bothered me that the tool didn’t allow previewing changes before applying them.</p><p>For several versions, the number of tools in the program stayed at 11, and I focused mainly on improving them and simplifying the logic of adding and managing tools later. I finally decided this was a good moment to expand the set.</p><p>I knew Rust had crates for handling EXIF data, but they still lacked many features compared to the popular exiftool. After a quick review, I considered only three libraries that had been updated recently:</p><ul><li>nom_exif</li><li>little_exif</li><li>guf_exif</li></ul><p>I rejected guf_exif because it didn’t have a general interface for managing all tags. I already use nom_exif to read image rotation so images display correctly. In the past, I reported several bugs in it using a fuzzer, which made it rock-solid and crash-free (at least in my cases). The problem was that it couldn’t modify data, and the related issue hasn’t been updated since 2024.</p><p>That left little_exif as the only option. While learning how EXIF works, I also learned how to use little_exif. Getting it to work wasn’t hard. I only needed to extract the tags from a file - their name, ID and group, and allow them to be removed. I make sure all libraries working with external data are as crash resistant as possible, because in the past such operations often caused problems. Additionally, every risky operation is wrapped in a panic::catch_unwind block to safely catch any panics.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7RRpKRxJs7wgeWJky8rDqw.png" /><figcaption>Example of finding exif tags</figcaption></figure><p>As usual, I approached testing quite thoroughly, which resulted in several issues being created, dozens of crashes, and the realization that the world doesn’t end with EXIF, other formats like XMP or iTXt also exist. Without deep knowledge of the EXIF format, I was able to fix crashes only up to the point where the fuzzer stopped detecting them after testing around 1 million inputs, and I modified a few APIs that were inconvenient for me to use.</p><p>In its current form, the tool allows removing EXIF data from several popular image formats. It supports fewer file formats and recognized tags than exiftool, but it is enough for my needs so far. I would be happy to see support for other metadata formats, so I am keeping an eye out for libraries that could expand the tool’s capabilities in the future.</p><p>This tool, like most of the features described below, is only available in Krokiet and the CLI. Czkawka GUI, which is now in maintenance mode, will not receive this mode or any other major functionality (unless someone implements it themselves, which I honestly encourage).</p><h3>Gtk4-rs and mysterious bugs</h3><p>From the very first versions, Czkawka has used GTK to build its GUI. As is well known, GTK is written in C, and using it in Rust requires either writing the bridging code yourself or relying on an existing library. As a beginner, trying to build C &lt;-&gt; Rust interoperability from scratch would have been asking for trouble, so the only reasonable choice was a new and experimental gtk-rs library. This library provided fairly good capabilities for calling C functions from Rust, but the combination of my skills at the time and the early version of this crate, meant I had to deal with many strange bugs.</p><p>Over time, the code, despite being written in a not-so-great style, became “rock-solid” and increasingly difficult to break on a typical Linux system. Due to problems on other platforms and difficulties in further development, I shifted most of my efforts to Krokiet, and marked the GTK application as deprecated, meaning I did not plan to implement new features, tools, or modes in it. However, most(I think) Czkawka users still rely on the GTK frontend, so I couldn’t leave them on their own and decided to fix emerging bugs as much as possible.</p><p>I started cleaning up the code a bit to make it easier to fix issues, and everything worked fine until I created a single test that, like other tests, checked the function for removing items from a list that weren’t linked to other items. The logic was simple:</p><ul><li>Iterate over the model</li><li>If the iterator points to a regular item, save it to a vector</li><li>If the iterator points to a header or the model ends, then if the vector has only one item, remove both the previous header and that item (for example, if there are two similar images, and one is deleted from disk, the other must be removed from the list as well, because keeping an item in the GUI without a linked counterpart is pointless)</li></ul><p>This test would simply hang. I started analyzing why almost identical tests passed correctly while this one did not. The most troublesome part was that the test usually worked fine when compiled on its own, but failed when compiled together with others. In the meantime, on an older manually compiled version of Czkawka, I began noticing occasional freezes after deleting files, though at the time I assumed these were regressions caused by other changes in the code.</p><p>My first hypothesis was incorrect API usage, for example operating on freed memory or an invalid iterator. However, neither AddressSanitizer nor Valgrind reported any issues. While preparing a minimal example to report a bug in gtk4-rs, I noticed that the problem occurred mainly in release mode and when running a larger number of tests.</p><p>After reporting the issue in the gtk4-rs repository, I also opened a report in the Rust repository, since the problem started appearing with version 1.86.0. User yvt, followed by ds84182, determined that the code in gtk4-rs was unsound. Specifically, in the iter_next function, the iter parameter was an immutable reference without interior mutability, even though its state was in fact being modified internally.</p><pre>#[doc(alias = &quot;gtk_tree_model_iter_next&quot;)]<br>fn iter_next(&amp;self, iter: &amp;TreeIter) -&gt; bool {<br>    unsafe {<br>        from_glib(ffi::gtk_tree_model_iter_next(<br>            self.as_ref().to_glib_none().0,<br>            mut_override(iter.to_glib_none().0),<br>        ))<br>    }<br>}</pre><p>The fix, which changes iter to the type &amp;mut TreeIter, was included in the new release, gtk4-rs 0.11.0. Its publication was one of the blockers for releasing the new version of the application.</p><h3>Video optimizer — new tool</h3><h4>Codec Change</h4><p>Under constant pressure to keep enough free disk space to store random memes and videos, I decided to focus on reducing the size of the data I already had. I had several large MJPEG video files, and converting them to a more efficient format seemed like the simplest solution. This allowed me to shrink their size several times over and save more than a dozen gigabytes of space. I could have used ffmpeg manually for each file, but searching for such videos and converting them one by one would have been tedious. That is how the idea for a new tool was born.</p><p>Like the other tools, the logic is simple. Files that look like videos based on their extension are collected. Then, ffprobe is used to retrieve their metadata. The list of files is displayed in the GUI. The user can select files and “optimize” them by changing the codec and optionally limiting the video resolution using ffmpeg and its specific options.</p><p>A simple Python script would probably work just as well, but I prefer having everything in one place, with the ability to manually review and select the results.</p><h4>Cropping Black Bars and Static Content</h4><p>A slightly more advanced mode in this tool is video cropping.</p><p>While going through my collection of random video clips, I noticed that many of them have random black bars on the sides or colored, static borders. Such frames not only waste valuable kilobytes, but also make the video less comfortable to watch.</p><p>One thing I truly dislike is watching a horizontal video that has black bars at the top and bottom. In full screen, it ends up using only a fraction of the available space. It just looks very very bad.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0kPx7EW91hfwXp31TIr8Ug.png" /><figcaption>Example of a video with black bars at the top and bottom, played in full screen</figcaption></figure><p>There are programs where you can simply select the area you want to crop to, but they usually add their own logo to the video and or only work on one file at a time, which makes the process time consuming. So I decided to start with a simple mode that automatically removes black bars without adding anything to the video. To keep things straightforward, the logic works as follows:</p><ul><li>Videos are first detected and their metadata is retrieved using ffprobe, such as resolution, bitrate, codec, and so on.</li><li>Next, ffmpeg is used to extract the first frame of the video, which is then used to override the dimensions from the metadata. ffprobe does not account for rotation and sometimes reports dimensions that differ from the actual frame.</li><li>For each video, up to 60 frames are extracted at intervals of at least 0.25 seconds. For a 15 second video, this means a frame every 0.25 seconds. For a 60 minute video, roughly one frame per minute.</li><li>On each frame, a bounding box of the actual content is calculated, so that everything outside the rectangle is treated as black bars. The process runs from four directions, top, bottom, left, and right. Starting from the outer edge, lines are checked where at least a given percentage of pixels, by default 90 percent, have all three RGB components below a defined threshold, by default 20. If the condition is met, the next line is checked. If not, the analysis from that direction stops and the edge of the rectangle is determined.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/921/1*HqpsR56-hqwVXLqLIxAyjw.png" /><figcaption>Example of how narrowing is done</figcaption></figure><ul><li>The next steps repeat the same procedure for subsequent frames, with one important rule, the content rectangle can only expand, meaning the cropped area can only shrink. This follows a simple principle: if an element appears in a given area on even one frame, it cannot be removed, even if that area is dark on other frames.</li><li>If the final rectangle matches the full frame size, it means cropping is not needed because there is nothing to remove. Otherwise, ffmpeg performs the actual crop operation to the calculated dimensions.</li></ul><p>Some videos are more problematic. They contain white subtitles or backgrounds in other colors. At first, I considered extending the algorithm to detect colors other than black, for example white. The issue was that the number of analyzed colors and edge cases would quickly grow, making the solution harder to implement and still not covering every situation users might expect.</p><p>I also considered a more complex approach: extracting 60 frames and comparing each row and column against the previous frame. In practice, this made the comparisons significantly more complicated.</p><p>Instead, I decided to compare all extracted frames to the first frame using a simple function that calculates pixel differences. For each RGB component of a pixel, the absolute difference is computed relative to the pixel at the same coordinates in the first frame. For example: RGB(50, 0, 50) and RGB(75, 50, 25) results in RGB(25, 50, 25). This produces a new image where pixel values represent how much the frame differs from the first one.</p><p>Since areas that change very little compared to the first frame become very dark in this difference image, I could reuse the previous cropping logic. Unchanged regions, regardless of their original color, effectively turn black in the difference image and can be cropped using the same simple threshold-based method.</p><p>Initially, I was not fully convinced this would work well. In practice, the results exceeded my expectations and it performs very well on my collection of random videos.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*R-HNfmuoiVAtgoTXkXpgNA.png" /><figcaption>Visible videos and their automatically cropped versions</figcaption></figure><p>Of course, this tool isn’t perfect and has some flaws and limitations:</p><ul><li>Sometimes it leaves thin black lines, probably because quality loss causes the image to blur, making these lines not completely black anymore so algorithm doesn’t remove them.</li><li>Static parts mode can also crop parts of the video that are useful. The algorithm doesn’t know whether a static section is important or not. For example, in a moving beach scene, the upper part of the frame often shows almost static sky, which is frequently cropped out, leaving just the moving area of the beach where the algorithm detects motion.</li><li>Transcoding is necessary, which can reduce quality and sometimes even increase file size. You can use H.265 conversion to reduce the size, since the video has to be transcoded anyway.</li><li>If changes in certain parts of the image happen very rarely, sampling 60 frames at regular intervals might miss them. The algorithm could mistakenly consider that spot safe to crop. This is a trade-off between processing speed and reasonably good results. Checking every single frame would almost guarantee nothing important is cropped, but for longer videos it would increase processing time linearly. For instance, a 1-hour video might take around 5 hours to process, instead of just a few seconds or minutes as it does now.</li></ul><h3>Bad names — new mode</h3><p>Another new mode that nobody asked for and that probably few people will use is searching for potentially undesirable filenames.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*N3-r2bAFHmHfFA8jsVOHbw.png" /></figure><p>This includes filenames with uppercase extensions, emojis, spaces at the beginning or end, non-ASCII characters, character limits, or repeated sequences. All of these can be searched for.</p><p>To keep names reasonably sensible, the steps are applied in a strict, unchangeable order. This means a file like “ 🚀 świstak .txt” won’t suddenly turn into “rocket swistak.txt” when emojis, non-ASCII characters, and extra spaces are removed, but it will become “swistak.txt”.</p><p>These modes came to mind first when I looked at the rather strange filenames on my computer, so I might expand the list of searchable/replaceable elements or tweak the existing rules in future versions.</p><h3>Preview of video parameters and frames</h3><p>A more interesting feature in this version is the added ability to display parameters of the found video files. In addition to the file path, size, and duration, the GUI (added both to the Krokiet interface and the GTK version, which, as a reminder, is in maintenance mode) now also shows information such as video length, bitrate, codec, framerate, and pixel dimensions.</p><p>Using an external crate, all this information is read through ffprobe, so ffprobe needs to be installed alongside ffmpeg to access it. The earliest issues requesting this feature date back three years, but no one addressed them, and I didn’t feel particularly motivated to tackle it either.</p><p>The need arose when my collection of random videos grew and I couldn’t keep up with removing duplicates. Without having these parameters visible directly with each video, I had to manually check each one by playing them to identify which were lower quality or smaller in size.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*UTcbQ8aVV8HlfkDqlPOKBQ.png" /><figcaption>Example of added video parameters, along with a frame from the currently selected video (Lobster 2015)</figcaption></figure><p>Another improvement in the video view was adding preview generation for the results. Although it sounds simple, it wasn’t. To create the previews, I used ffmpeg, similar to how it’s currently used for hashing and comparing video files. This is a time-consuming operation (even hundreds of milliseconds), so generating previews on the fly would cause noticeable delays when clicking. Additionally, this would either have to discard these images and regenerate them on each click or store them in some kind of cache, which would complicate the logic.</p><p>It would also be problematic that, when clicking, either the entire GUI would freeze until the preview finished processing (as currently happens for normal images, excluding ffmpeg operations), or it would have to be loaded in a separate thread, adding complexity.</p><p>I decided that handling this entirely on the GUI side was unnecessarily difficult, so I moved it to the core. Now there is a new scanning step that, if the user has opted to generate previews, creates a preview for each file that is similar to others. The filename, modification time, file size, and the frame location are hashed so that the same parameters always produce the same path, avoiding regeneration in subsequent scans. Alternatively, this could be stored in a cache, but then you would lose the ability to choose which frame to extract.</p><p>There is also an option to generate a video preview as a single image made up of 9 frames taken from different points in the video. Since this requires generating 9 frames instead of 1, it is much slower.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4eNxJvnGD5ApOXRYHiR_mw.png" /><figcaption>Alternative video preview as a 3x3 grid (1670 2023)</figcaption></figure><p>Image previews are available only as an option in Krokiet. The CLI version has preview generation hardcoded as disabled (because, honestly, it doesn’t make sense there), and the GTK version, well, you know the situation.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*pzGcupZYok_EDRcZP57Mzw.png" /><figcaption>With crop mode, it also draws rectangles where video will be cropped</figcaption></figure><p>To avoid excessive memory and CPU usage, extracting previews from a file with ffmpeg is limited to a single thread per video. By default, ffmpeg would create as many threads as are available on the system, which could lead to out-of-memory errors. During testing, the program crashed several times before I implemented this limit.</p><h3>Other changes</h3><h4>GTK Czkawka dialog about Krokiet</h4><p>I was surprised that so few people know that Krokiet is a new and improved version of Czkawka, even though I tried to communicate it in various ways. So, following the “because I know better” principle, I decided to add a new dialog window that explicitly tells users this. Its display depends on whether a special file exists indicating the window has already been shown (it should appear only once) or whether the user has set the magic variable CZKAWKA_DONT_ANNOY_ME.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*FnKzl2b_P8J5q9SUvl1xbA.png" /></figure><h4>New Krokiet Logo</h4><p>The previous logo was a quick, hand-drawn unicorn in a simple raster image. That worked when the app was experimental. Today, since Krokiet is widely seen as an almost perfect application, it needed an almost perfect logo. The new logo depicts a unicorn with a shield, helmet, and a banner featuring a krokiet. Why this design? I’d answer: why not (though it probably has something to do with the Hussite Trilogy audiobook I was listening to at the time). It’s also in SVG format now, so it scales nicely.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/863/1*9gOZ4HFDvvX_8GnUe79mUA.png" /><figcaption>New program mascot</figcaption></figure><h4>Delayed cleaning of outdated cache records</h4><p>The usual application flow is:</p><ul><li>Collect file list</li><li>Load cache</li><li>Process files</li><li>Save cache</li><li>Display results</li></ul><p>The longest operations are usually processing files and collecting the file list, since both access the disk and check many small files. Loading and saving the cache operates on individual files, so it should be fast, right? Not entirely — there’s a default option to remove outdated records to keep the cache size under control, minimizing load and save times. During each load operation, all records are checked for existence, modification time, and size. This is usually quick, but on external drives connected via Samba or on HDDs, operations on many small files can take unnecessarily long.</p><p>To address this, I decided to delay these operations so they run as rarely as possible. Now, during cache loading, outdated records are checked only once per week. Users also have the option to manually clear the cache at any time if needed.</p><h4>Ability to scan single files</h4><p>Since the beginning, the only way to scan was to select a folder first. This makes sense because it covers all use cases and allows scanning any combination of files. In some situations, this required creating new folders and moving files into them to scan. Some users wanted to check just a single image/video/file for duplicates without creating a folder. Previously, this required manually creating a folder, adding the file, and setting it as a reference folder. Now, it’s possible to add individual files directly, set them as reference paths, and check them without moving them. Personally, this didn’t bother me much, and over the years I simply got used to testing files by moving them around on the disk.</p><h4>Sounds</h4><p>A subtle new feature in this version is adding sounds to Krokiet when scanning finishes. During long scans, it was easy to get distracted and miss the end. For now, I added only one test sound to see if it’s useful, and it may be expanded in the future. The scan-complete sound is a noise I made with my jaw, which probably isn’t very pleasant, so users can set any custom sound via an environment variable.</p><h4>AI translations</h4><p>Almost all Rust code in the project, build systems, and scripts are developed mainly by me, since I wrote most of it and understand it fairly well. The only exception is translations, where I’m responsible for the majority of texts, but only for English and Polish. Other languages, like Italian or French, were actively maintained in the past; some are currently being updated, such as Brazilian Portuguese, while others were created from scratch using online translators in response to requests for that language. Currently, dozens of languages are supported, amounting to tens of thousands of words, and every release requires updating dozens or hundreds of new phrases. The scale may not be huge for some projects, but for me it is, and faced with so many changes, I had to choose between:</p><ul><li>Leaving translations partially incomplete</li><li>Removing/hiding incomplete translations</li><li>Translating missing texts manually</li></ul><p>To prevent repeated requests for adding an existing language and to avoid exposing users to incomplete texts, I decided — without a few hundred or thousand dollars to hire translators or a dedicated team — to first manually translate certain phrases using publicly available translation websites, then use a script leveraging the zongwei/gemma3-translator:4b model. It’s not fast (a few words per second on my machine), but it seems to produce better results than other methods.</p><h4>Refactoring Czkawka GTK code</h4><p>Saying the code in Czkawka GTK isn’t very tidy would be an understatement. I wrote it mainly when my Rust knowledge was fairly primitive. Over time, the project grew, and because of its size, I never fully rewrote the GTK code in a cleaner style, only patching what I could. In this version, I’ve done a bit more of the same, extracting operations on operators into shared functions and grouping related elements (e.g., scrollview, treeview, and gestureclick), making the code somewhat more approachable.</p><p>I had considered preparing the code for potential GTK 5 support, but due to the extensive changes required (for example, replacing TreeView, which is the base for all models), I have no plans to pursue it. However, anyone brave enough could take it on — I have no objection to using AI extensively for this task, as long as the code is reasonably good, stylistically consistent with the rest of the program, and passes a series of my reviews.</p><h3>The End</h3><p>Repository — <a href="http://github.com/qarmin/czkawka">github.com/qarmin/czkawka</a></p><p>Download files with the changelog — <a href="https://github.com/qarmin/czkawka/releases">https://github.com/qarmin/czkawka/releases</a></p><p>License — MIT/GPL depending on the program (in short, it’s free)</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0f6cea385934" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Czkawka / Krokiet 10.0,]]></title>
            <link>https://medium.com/@qarmin/czkawka-krokiet-10-0-4991186b7ad1?source=rss-9c8ac0ce6dcd------2</link>
            <guid isPermaLink="false">https://medium.com/p/4991186b7ad1</guid>
            <dc:creator><![CDATA[Qarmin]]></dc:creator>
            <pubDate>Mon, 18 Aug 2025 15:00:41 GMT</pubDate>
            <atom:updated>2025-08-18T15:02:05.628Z</atom:updated>
            <content:encoded><![CDATA[<h3>Czkawka / Krokiet 10.0, cleaning duplicates, unifying features and a handful of Rust related statistics</h3><p>After a little less than six months, I’m releasing a new version of my three distinct (yet similar) duplicate-finding programs today.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BnQAWLVwLPxEZL6D0rffMg.png" /></figure><p>The list of fixes and new features may seem random, and in fact it is, because I tackled them in the order in which ideas for their solutions came to mind. I know that the list of reported issues on GitHub is quite long, and for each user their own problem seems the most important, but with limited time I can only address a small portion of them, and I don’t necessarily pick the most urgent ones.</p><p>Interestingly, this version is the largest so far (at least if you count the number of lines changed). Krokiet now contains almost all the features I used in the GTK version, so it looks like I myself will soon switch to it completely, setting an example for other undecided users (as a reminder, the GTK version is already in maintenance mode, and I focus there exclusively on bug fixes, not adding new features).</p><p>As usual, the binaries for all three projects (czkawka_cli, krokiet, and czkawka_gui), along with a short legend explaining what the individual names refer to and where these files can be used, can be found in the releases section on GitHub — <a href="https://github.com/qarmin/czkawka/releases">https://github.com/qarmin/czkawka/releases</a></p><h3><strong>Adding memory usage limits when loading the cache</strong></h3><p>One of the random errors that sometimes occurred due to the user, sometimes my fault, and sometimes — for example — because a power outage shut down the computer during operation, was a mysterious crash at the start of scanning, which printed the following information to the terminal:</p><pre>memory allocation of 201863446528 bytes failed</pre><p>Cache files that were corrupted by the user (or due to random events) would crash when loaded by the bincode library. Another situation, producing an error that looked identical, occurred when I tried to remove cache entries for non-existent or unavailable files using an incorrect struct for reading the data (in this case, the fix was simply changing the struct type into which I wanted to decode the data).</p><p>This was a rather unpleasant situation, because the application would crash for the user during scanning or when pressing the appropriate button, leaving them unsure of what to do next. Bincode provides the possibility of adding a memory limit for data decoding. The fix required only a few lines of code, and that could have been the end of it. However, during testing it turned out to be an unexpected breaking change—data saved with a memory-limited configuration cannot be read with a standard configuration, and vice versa.</p><pre>use std::collections::BTreeMap;<br>use bincode::{serialize_into, Options};<br><br>const MEMORY_LIMIT: u64 = 1024 * 1024 * 1024; // 1 GB<br>fn main() {<br>    let rands: Vec&lt;u32&gt; = (0..1).map(|_| rand::random::&lt;u32&gt;()).collect();<br>    let btreemap: BTreeMap&lt;u32, Vec&lt;u32&gt;&gt; =<br>        rands<br>            .iter()<br>            .map(|&amp;x| (x % 10, rands.clone()))<br>            .collect();<br>    let options = bincode::DefaultOptions::new().with_limit(MEMORY_LIMIT);<br>    let mut serialized: Vec&lt;_&gt; = Vec::new();<br>    options.serialize_into(&amp;mut serialized, &amp;btreemap).unwrap();<br>    println!(&quot;{:?}&quot;, serialized);<br>    let mut serialized2: Vec&lt;_&gt; = Vec::new();<br>    serialize_into(&amp;mut serialized2, &amp;btreemap).unwrap();<br>    println!(&quot;{:?}&quot;, serialized2);<br>}</pre><pre>[1, 1, 1, 252, 53, 7, 34, 7]<br>[1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 53, 7, 34, 7]</pre><p>The above code, when serializing data with and without the limit, produces two different results, which was very surprising to me because I thought that the limiting option applied only to the decoding code, and not to the file itself (it seems to me that most data encoding libraries write only the raw data to the file).</p><p>So, like it or not, this version (following the path of its predecessors) has a cache that is incompatible with previous versions. This was one of the reasons I didn’t implement it earlier — I had tried adding limits only when reading the file, not when writing it (where I considered it unnecessary), and it didn’t work, so I didn’t continue trying to add this functionality.</p><p>I know that for some users it’s probably inconvenient that in almost every new version they have to rebuild the cache from scratch, because due to changed structures or data calculation methods, it’s not possible to simply read old files. So in future versions, I’ll try not to tamper too much with the cache unless necessary (although, admittedly, I’m tempted to add a few extra parameters to video files in the next version, which would force the use of the new cache).</p><p>An alternative would be to create a built-in tool for migrating cache files. However, reading arbitrary external data without memory limits in place would make such a tool useless and prone to frequent crashes. Such a tool is only feasible from the current version onward, and it may be implemented in the future.</p><h3><strong>Translations in Krokiet</strong></h3><p>To match the feature set currently available in Czkawka, I decided to try to implement the missing translations, which make it harder for some users, less proficient in English, to use the application.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OhrakDHpB8TC_xAH0y97qQ.png" /><figcaption>Even Traditional Chinese poses no problem when displaying text.</figcaption></figure><p>One might think that since Slint itself is written in Rust, using the Fluent library inside it, which is also written in Rust, would be an obvious and natural choice. However, for various reasons, the authors decided that it’s better to use probably the most popular translation tool instead — gettext, which, however, complicates compilation and almost makes cross-compilation impossible (the issue aims to change this situation — <a href="https://github.com/slint-ui/slint/issues/3715">https://github.com/slint-ui/slint/issues/3715</a>).</p><p>Without built-in translation support in Slint, what seemed like a fairly simple functionality turned into a tricky puzzle of how to implement it best. My goal was to allow changing the language at runtime, without needing to restart the entire application.</p><p>Ultimately, I decided that the best approach would be to create a singleton containing all the translation texts, in a style like this:</p><pre>export global Translations {<br>    in-out property &lt;string&gt; ok_button_text: &quot;Ok&quot;;<br>    in-out property &lt;string&gt; cancel_button_text: &quot;Cancel&quot;;<br>    ...<br>}</pre><p>…and use it as</p><pre>export component PopupBase inherits PopupWindow {<br>    in-out property &lt;string&gt; ok_text &lt;=&gt; Translations.ok_button_text;<br>    ...<br>}</pre><p>then, when changing the language or launching the application, all these attributes are updated in such a way:</p><pre>app.global::&lt;Callabler&gt;().on_changed_language(move || {<br>    let app = a.upgrade().unwrap();<br>    let translation = app.global::&lt;Translations&gt;();    <br>    translation.set_ok_button_text(flk!(&quot;ok_button&quot;).into());<br>    translation.set_cancel_button_text(flk!(&quot;cancel_button&quot;).into());<br>    ...<br>});</pre><p>With over 200 texts to translate, it’s very easy to make a mistake or leave some translations unlinked, which is why I rely on Python helper scripts that verify everything is being used.</p><p>This adds more code than if built-in support for fluent-rs existed and could be used directly, similar to how gettext translations currently work. I hope that something like this will be implemented for Fluent soon:</p><pre>export component PopupBase inherits PopupWindow {<br>    in-out property &lt;string&gt; ok_text: @tr(&quot;ok_button&quot;);<br>    ...<br>}</pre><p>Regarding the translations themselves, they are hosted and updated on Crowdin — <a href="https://crowdin.com/project/czkawka">https://crowdin.com/project/czkawka</a> — and synchronized with GitHub from time to time. For each release, several dozen phrases are updated, so I’m forced to use machine translation for some languages. Not all texts may be fully translated or look as they should, so feel free to correct them if you come across any mistakes.</p><h3>Improving Krokiet</h3><p>The main goal of this version was to reduce the feature gaps between Czkawka (GUI) and Krokiet, so that I could confidently recommend Krokiet as a viable alternative. I think I largely succeeded in this area.</p><p>During this process, it often turned out that implementing the same features in Slint is much simpler than it was in the GTK version. Take sorting as an example. On the GTK side, due to the lack of better-known solutions (there probably are some, but I’ve lived until now in complete ignorance, which makes my eyes hurt when I look at the final implementation I once made), to sort a model, I would get an iterator over it and then iterate through each element one by one, collecting the TreeIters into a vector. Then I would extract the data from a specific column of each row and sort it using bubble sort within that vector.</p><pre>fn popover_sort_general&lt;T&gt;(tree_view: &amp;gtk4::TreeView, column_sort: i32, column_header: i32)<br>where<br>    T: Ord + for&lt;&#39;b&gt; glib::value::FromValue&lt;&#39;b&gt; + &#39;static + Debug,<br>{<br>    let model = get_list_store(tree_view);<br>if let Some(curr_iter) = model.iter_first() {<br>        assert!(model.get::&lt;bool&gt;(&amp;curr_iter, column_header)); // First item should be header<br>        assert!(model.iter_next(&amp;curr_iter)); // Must be at least two items<br>        loop {<br>            let mut iters = Vec::new();<br>            let mut all_have = false;<br>            loop {<br>                if model.get::&lt;bool&gt;(&amp;curr_iter, column_header) {<br>                    assert!(model.iter_next(&amp;curr_iter), &quot;Empty header, this should not happens&quot;);<br>                    break;<br>                }<br>                iters.push(curr_iter);<br>                if !model.iter_next(&amp;curr_iter) {<br>                    all_have = true;<br>                    break;<br>                }<br>            }<br>            if iters.len() == 1 {<br>                continue; // Can be equal 1 in reference folders<br>            }<br>            sort_iters::&lt;T&gt;(&amp;model, iters, column_sort);<br>            if all_have {<br>                break;<br>            }<br>        }<br>    }<br>}<br><br>fn sort_iters&lt;T&gt;(model: &amp;ListStore, mut iters: Vec&lt;TreeIter&gt;, column_sort: i32)<br>where<br>    T: Ord + for&lt;&#39;b&gt; glib::value::FromValue&lt;&#39;b&gt; + &#39;static + Debug,<br>{<br>    assert!(iters.len() &gt;= 2);<br>    loop {<br>        let mut changed_item = false;<br>        for idx in 0..(iters.len() - 1) {<br>            if model.get::&lt;T&gt;(&amp;iters[idx], column_sort) &gt; model.get::&lt;T&gt;(&amp;iters[idx + 1], column_sort) {<br>                model.swap(&amp;iters[idx], &amp;iters[idx + 1]);<br>                iters.swap(idx, idx + 1);<br>                changed_item = true;<br>            }<br>        }<br>        if !changed_item {<br>            return;<br>        }<br>    }<br>}</pre><p>Over time, I’ve realized that I should have wrapped the model management logic earlier, which would have made reading and modifying it much easier. But now, it’s too late to make changes. On the Slint side, the situation is much simpler and more “Rust-like”:</p><pre>pub(super) fn sort_modification_date(model: &amp;ModelRc&lt;MainListModel&gt;, active_tab: ActiveTab) -&gt; ModelRc&lt;MainListModel&gt; {<br>    let sort_function = |e: &amp;MainListModel| {<br>        let modification_date_col = active_tab.get_int_modification_date_idx();<br>        let val_int = e.val_int.iter().collect::&lt;Vec&lt;_&gt;&gt;();<br>        connect_i32_into_u64(val_int[modification_date_col], val_int[modification_date_col + 1])<br>    };<br>    let mut items = model.iter().collect::&lt;Vec&lt;_&gt;&gt;();<br>    items.sort_by_cached_key(&amp;sort_function);<br>    let new_model = ModelRc::new(VecModel::from(items));<br>    recalculate_small_selection_if_needed(&amp;new_model, active_tab);<br>    return new_model;<br>}</pre><p>It’s much shorter, more readable, and in most cases faster (the GTK version might be faster if the data is already almost sorted). Still, a few oddities remain, such as:</p><ul><li><strong>modification_date_col</strong> —to generalize the model for different tools a bit, for each row in the scan results, there are vectors containing numeric and string data. The amount and order of data differs for each tool, so it’s necessary to fetch from the current tab where the needed data currently resides</li><li><strong>connect_i32_into_u64 — </strong>as the name suggests, it combines two i32 values into a u64. This is a workaround for the fact that Slint doesn’t yet support 64-bit integers (though I’m hopeful that support will be added soon).</li><li><strong>recalculate_small_selection_if_needed — </strong>due to the lack of built-in widgets with multi-selection support in Slint (unlike GTK), I had to create such a widget along with all the logic for selecting items, modifying selections, etc. It adds quite a bit of extra code, but at least I now have more control over selection, which comes in handy in certain situations</li></ul><p>Another useful feature that already existed in Czkawka is the ability to start a scan, along with a list of selected folders, directly from the CLI. So now, running</p><pre>krokiet . Desktop -i /home/rafal/Downloads -e /home/rafal/Downloads/images</pre><p>will start scanning for files in three folders with one excluded (of course, only if the paths exist — otherwise, the path will be ignored). This mode uses a separate configuration file, which is loaded when the program is run with command-line arguments (configurations for other modes are not overwritten).</p><p>Since some things are easier to implement in Krokiet, I added several functions in this version that were missing in Czkawka:</p><ul><li>Remembering window size and column widths for each screen</li><li>The ability to hide text on icons (for a more compact UI)</li><li>Dark and light themes, switchable at runtime</li><li>Disabling certain buttons when no items are selected</li><li>Displaying the number of items queued for deletion</li></ul><h3><strong>Ending AppImage Support</strong></h3><p>Following the end of Snap support on Linux in the previous version, due to difficulties in building them, it’s now time to drop AppImage as well.</p><p>The main reasons for discontinuing AppImage are the nonstandard errors that would appear during use and its limited utility beyond what regular binary files provide.</p><p>Personally, I’m a fan of the AppImage format and use it whenever possible (unless the application is also available as a Flatpak or Snap), since it eliminates the need to worry about external dependencies. This works great for applications with a large number of dependencies. However, in Czkawka, the only dependencies bundled were GTK4 libraries — which didn’t make much sense, as almost every Linux distribution already has these libraries installed, often with patches to improve compatibility (for example, Debian patches: <a href="https://udd.debian.org/patches.cgi?src=gtk4&amp;version=4.18.6%2Bds-2">https://sources.debian.org/src/gtk4/4.18.6%2Bds-2/debian/patches/series/</a>).</p><p>It would make more sense to bundle optional libraries such as ffmpeg, libheif or libraw, but I didn’t have the time or interest to do that. Occasionally, some AppImage users started reporting issues that did not appear in other formats and could not be reproduced, making them impossible to diagnose and fix.</p><p>Additionally, the plugin itself (<a href="https://github.com/linuxdeploy/linuxdeploy-plugin-gtk">https://github.com/linuxdeploy/linuxdeploy-plugin-gtk</a>) used to bundle GTK dependencies hadn’t been updated in over two years. Its authors did a fantastic job creating and maintaining it in their free time, but a major issue for me was that it wasn’t officially supported by the GTK developers, who could have assisted with the development of this very useful project.</p><h3><strong>Multithreaded File Processing in Krokiet and CLI</strong></h3><p>Some users pointed out that deleting or copying files from within the application is time-consuming, and there is no feedback on progress. Additionally, during these operations, the entire GUI becomes unresponsive until the process finishes.</p><p>The problem stems from performing file operations in the same thread as the GUI rendering. Without interface updates, the system considers the application unresponsive and may display an os window prompting the user to kill it.</p><p>The solution is relatively straightforward — simply move the computations to a separate thread. However, this introduces two new challenges: the need to stop the file-processing task and to synchronize the state of completed operations with the GUI.</p><p>A simple implementation in this style is sufficient:</p><pre>let all_files = files.len();<br>let mut processing_files = Arc&lt;AtomicBool&lt;usize&gt;&gt;::new(0);<br>let _ = files.into_par_iter().map(|e| {<br>  if stop_flag.load(Ordering::Relaxed) {<br>    return None;<br>  }<br>  let processing_files = processing_files.fetch_add(1, Ordering::Relaxed);<br>  let status_to_send = Status { all_files, processing_files };<br>  progress_sender.send(status_to_send);<br>  // Processing file<br>}).while_some().collect::&lt;Vec&lt;_&gt;&gt;();</pre><p>The problem arises when a large number of messages are being sent, and updating the GUI/terminal for each of them would be completely unnecessary — after all, very few people could notice and process status changes appearing even 60 times per second.</p><p>This would also cause performance issues and unnecessarily increase system resource usage. I needed a way to limit the number of messages being sent. This could be implemented either on the side of the message generator (the thread deleting files) or on the recipient side (the GUI thread/progress bar in CLI). I decided it’s better to handle it sooner rather than later.</p><p>Ultimately, I created a simple structure that uses a lock to store the latest message to be sent. Then, in a separate thread, every ~100 ms, the message is fetched and sent to the GUI. Although the solution is simple, I do have some concerns about its performance on systems with a very large number of cores — there, thousands or even tens of thousands of messages per second could cause the mutex to become a bottleneck. For now, I haven’t tested it under such conditions, and it currently doesn’t cause problems, so I’ve postponed optimization (though I’m open to ideas on how it could be improved).</p><pre>pub struct DelayedSender&lt;T: Send + &#39;static&gt; {<br>    slot: Arc&lt;Mutex&lt;Option&lt;T&gt;&gt;&gt;,<br>    stop_flag: Arc&lt;AtomicBool&gt;,<br>}<br>impl&lt;T: Send + &#39;static&gt; DelayedSender&lt;T&gt; {<br>    pub fn new(sender: crossbeam_channel::Sender&lt;T&gt;, wait_time: Duration) -&gt; Self {<br>        let slot = Arc::new(Mutex::new(None));<br>        let slot_clone = Arc::clone(&amp;slot);<br>        let stop_flag = Arc::new(AtomicBool::new(false));<br>        let stop_flag_clone = Arc::clone(&amp;stop_flag);<br>        let _join = thread::spawn(move || {<br>            let mut last_send_time: Option&lt;Instant&gt; = None;<br>            let duration_between_checks = Duration::from_secs_f64(wait_time.as_secs_f64() / 5.0);<br>            loop {<br>                if stop_flag_clone.load(std::sync::atomic::Ordering::Relaxed) {<br>                    break;<br>                }<br>                if let Some(last_send_time) = last_send_time {<br>                    if last_send_time.elapsed() &lt; wait_time {<br>                        thread::sleep(duration_between_checks);<br>                        continue;<br>                    }<br>                }<br>                let Some(value) = slot_clone.lock().expect(&quot;Failed to lock slot in DelayedSender&quot;).take() else {<br>                    thread::sleep(duration_between_checks);<br>                    continue;<br>                };<br>                if stop_flag_clone.load(std::sync::atomic::Ordering::Relaxed) {<br>                    break;<br>                }<br>                if let Err(e) = sender.send(value) {<br>                    log::error!(&quot;Failed to send value: {e:?}&quot;);<br>                };<br>                last_send_time = Some(Instant::now());<br>            }<br>        });<br>        Self { slot, stop_flag }<br>    }<br>    pub fn send(&amp;self, value: T) {<br>        let mut slot = self.slot.lock().expect(&quot;Failed to lock slot in DelayedSender&quot;);<br>        *slot = Some(value);<br>    }<br>}<br>impl&lt;T: Send + &#39;static&gt; Drop for DelayedSender&lt;T&gt; {<br>    fn drop(&amp;mut self) {<br>        // We need to know, that after dropping DelayedSender, no more values will be sent<br>        // Previously some values were cached and sent after other later operations<br>        self.stop_flag.store(true, std::sync::atomic::Ordering::Relaxed);<br>    }<br>}</pre><h3>External Changes</h3><h4>Alternative GUI</h4><p>In the case of Krokiet and Czkawka, I decided to write the GUI in low-level languages (Slint is transpiled to Rust), instead of using higher-level languages — mainly for performance and simpler installation.</p><p>For Krokiet, I briefly considered using Tauri, but I decided that Slint would be a better solution in my case: simpler compilation and no need to use the heavy (and differently behaving on each system) webview with TS/JS.</p><p>However, one user apparently didn’t like the current gui and decided to create their own alternative using Tauri.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*V1uLPdvEaiQjrpDo8oI4Dw.png" /></figure><p>The author himself does not hide that he based the look of his program on Krokiet(which is obvious). Even so, differences can be noticed, stemming both from personal design preferences and limitations of the libraries that both projects use(for example, in the Tauri version popups are used more often, because Slint has issues with them, so I avoided using them whenever possible).</p><p>Since I am not very skilled in application design, it’s not surprising that I found several interesting solutions in this new GUI that I will want to either copy 1:1 or use as inspiration when modifying Krokiet.</p><p>Preliminary tests indicate that the application works surprisingly well, despite minor performance issues (one mode on Windows froze briefly — though the culprit might also be the czkawka_core package), small GUI shortcomings (e.g., the ability to save the application as an HTML page), or the lack of a working Linux version (a month or two ago I managed to compile it, but now I cannot).</p><p>Link — <a href="https://github.com/shixinhuang99/czkawka-tauri">https://github.com/shixinhuang99/czkawka-tauri</a></p><h4>Czkawka in the Debian Repository</h4><p>Recently, just before the release of Debian 13, a momentous event took place — Czkawka 8.0.0 was added to the Debian repository (even though version 9.0.0 already existed, but well… Debian has a preference for older, more stable versions, and that must be respected). The addition was made by user Fab Stz.</p><p>Links:<br>- <a href="https://packages.debian.org/sid/czkawka-gui">https://packages.debian.org/sid/czkawka-gui</a><br>- <a href="https://packages.debian.org/sid/czkawka-cli">https://packages.debian.org/sid/czkawka-cli</a></p><p>Debian takes reproducible builds very seriously, so it quickly became apparent that building Czkawka twice in the same environment produced two different binaries. I managed to reduce the problematic program to a few hundred lines. In my great wisdom (or naivety, assuming the bug wasn’t “between the chair and the keyboard”), I concluded that the problem must be in Rust itself. However, after analysis conducted by others, it turned out that the culprit was the i18n-cargo-fl library, whose proc-macro iterates over a hashmap of arguments, and in Rust the iteration order in such a case is random (<a href="https://github.com/kellpossible/cargo-i18n/issues/150">https://github.com/kellpossible/cargo-i18n/issues/150</a>).</p><p>With the source of the problem identified, I prepared a fix — <a href="https://github.com/kellpossible/cargo-i18n/pull/151">https://github.com/kellpossible/cargo-i18n/pull/151</a> — which has already been merged and is part of the new 0.10.0 version of the cargo-i18n library. Debian’s repository still uses version 0.9.3, but with this fix applied. Interestingly, cargo-i18n is also used in many other projects, including applications from <strong>Cosmic DE</strong>, so they too now have an easier path to achieving fully reproducible builds.</p><h3><strong>Compilation Times and Binary Size</strong></h3><p>I have never hidden the fact that I gladly use external libraries to easily extend the capabilities of an application, so I don’t have to waste time reinventing the wheel in a process that is both inefficient and error-prone.</p><p>Despite many obvious advantages, the biggest downsides are larger binary sizes and longer compilation times. On my older laptop with 4 weak cores, compilation times became so long that I stopped developing this program on it.</p><p>However, this doesn’t mean I use additional libraries without consideration. I often try to standardize dependency versions or use projects that are actively maintained and update the libraries they depend on — for example, rawler instead of rawloader, or image-hasher instead of img-hash (which I created as a fork of img-hash with updated dependencies).</p><p>To verify the issue of long compilation times, I generated several charts showing how long Krokiet takes to compile with different options, how large the binary is after various optimizations, and how long a recompilation takes after adding a comment (I didn’t test binary performance, as that is a more complicated matter). This allowed me to consider which options were worth including in CI. After reviewing the results, I decided it was worth switching from the current configuration— <strong><em>release + thin lto</em></strong> to <strong><em>release + fat lto + codegen units = 1</em></strong> .</p><p>The tests were conducted on a 12-core AMD Ryzen 9 9700 running Ubuntu 25.04, using the mold linker and rustc 1.91.0-nightly (cd7cbe818 2025–08–15). The base profiles were debug and release, and I adjusted some options based on them (not all combinations seemed worth testing, and some caused various errors) to see their impact on compilation. It’s important to note that Krokiet is a rather specific project with many dependencies, and Slint that generates a large (~100k lines) Rust file, so other projects may experience significantly different compilation times.</p><p>Test Results:</p><pre>|Config                                              | Output File Size   | Target Folder Size   | Compilation Time   | Rebuild Time   |<br>|:---------------------------------------------------|:-------------------|:---------------------|:-------------------|:---------------|<br>| release + overflow checks                          | 73.49 MiB          | 2.07 GiB             | 1m 11s             | 20s            |<br>| debug                                              | 1004.52 MiB        | 7.00 GiB             | 1m 54s             | 3s             |<br>| debug + cranelift                                  | 624.43 MiB         | 5.25 GiB             | 47s                | 3s             |<br>| debug + debug disabled                             | 131.64 MiB         | 2.52 GiB             | 1m 33s             | 2s             |<br>| check                                              | -                  | 1.66 GiB             | 58s                | 1s             |<br>| release                                            | 70.50 MiB          | 2.04 GiB             | 2m 58s             | 2m 11s         |<br>| release + cranelift                                | 70.50 MiB          | 2.04 GiB             | 2m 59s             | 2m 10s         |<br>| release + debug info                               | 786.19 MiB         | 5.40 GiB             | 3m 23s             | 2m 18s         |<br>| release + native                                   | 67.22 MiB          | 1.98 GiB             | 3m 5s              | 2m 13s         |<br>| release + opt o2                                   | 70.09 MiB          | 2.04 GiB             | 2m 56s             | 2m 9s          |<br>| release + opt o1                                   | 76.55 MiB          | 1.98 GiB             | 1m 1s              | 18s            |<br>| release + thin lto                                 | 63.77 MiB          | 2.06 GiB             | 3m 12s             | 2m 32s         |<br>| release + optimize size                            | 66.93 MiB          | 1.93 GiB             | 1m 1s              | 18s            |<br>| release + fat lto                                  | 45.46 MiB          | 2.03 GiB             | 6m 18s             | 5m 38s         |<br>| release + cu 1                                     | 50.93 MiB          | 1.92 GiB             | 4m 9s              | 2m 56s         |<br>| release + panic abort                              | 56.81 MiB          | 1.97 GiB             | 2m 56s             | 2m 15s         |<br>| release + build-std                                | 70.72 MiB          | 2.23 GiB             | 3m 7s              | 2m 11s         |<br>| release + fat lto + cu 1 + panic abort             | 35.71 MiB          | 1.92 GiB             | 5m 44s             | 4m 47s         |<br>| release + fat lto + cu 1 + panic abort + native    | 35.94 MiB          | 1.87 GiB             | 6m 23s             | 5m 24s         |<br>| release + fat lto + cu 1 + panic abort + build-std | 33.97 MiB          | 2.11 GiB             | 5m 45s             | 4m 44s         |<br>| release + fat lto + cu 1                           | 40.65 MiB          | 1.95 GiB             | 6m 3s              | 5m 2s          |<br>| release + incremental                              | 71.45 MiB          | 2.38 GiB             | 1m 8s              | 2s             |<br>| release + incremental + fat lto                    | 44.81 MiB          | 2.44 GiB             | 4m 25s             | 3m 36s         |</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-7BK9knU6AWu8N7ir13OnQ.png" /><figcaption>Compilation times</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_sdLOzU23sgueQr3-8Zs-A.png" /><figcaption>Size of generated binary</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KAnzq3obMky0dZUku5tuTA.png" /><figcaption>Rebuild times, after changing one line</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*W9INTNlyc0dbjCnTh5ZD1A.png" /><figcaption>Size of target folder</figcaption></figure><p>Some things that surprised me:</p><ul><li><strong><em>build-std</em></strong> increases, rather than decreases, the binary size</li><li><strong><em>optimize-size</em></strong> is fast but only slightly reduces the final binary size.</li><li><strong><em>fat-LTO</em></strong> works much better than <strong><em>thin-LTO</em></strong> in this project, even though I often read online that <strong><em>thin-LTO</em></strong> usually gives results very similar to <strong><em>fat-LTO</em></strong></li><li><strong><em>panic-abort</em></strong> — I thought using this option wouldn’t change the binary size much, but the file shrank by as much as 20%. However, I cannot disable this option and wouldn’t recommend it to anyone (at least for Krokiet and Czkawka), because with external libraries that process/validate/parse external files, panics can occur, and with <strong><em>panic-abort</em></strong> they cannot be caught, so the application will just terminate instead of printing an error and continuing</li><li><strong><em>release + incremental </em></strong>—this will probably become my new favorite flag, it gives release performance while keeping recompilation times similar to debug. Sometimes I need a combination of both, although I still need to test this more to be sure</li></ul><p>The project I used for testing (created for my own purposes, so it might simply not work for other users, and additionally it modifies the Git repository, so all changes need to be committed before use) — <a href="https://github.com/qarmin/czkawka/tree/master/misc/test_compilation_speed_size">https://github.com/qarmin/czkawka/tree/master/misc/test_compilation_speed_size</a></p><h3>Files from unverified sources</h3><p>Lately, I’ve both heard and noticed strange new websites that seem to imply they are directly connected to the project (though this is never explicitly stated) and offer only binaries repackaged from GitHub, hosted on their own servers. This isn’t inherently bad, but in the future it could allow them to be replaced with malicious files.</p><p>Personally, I only manage a few projects related to Czkawka: the code repository on GitHub along with the binaries hosted there, the Flatpak version of the application, and projects on crates.io. All other projects are either abandoned (e.g., the Snap Store application) or managed by other people.</p><p>Czkawka itself does not have a website, and its closest equivalent is the Readme.md file displayed on the main GitHub project page — I have no plans to create an official site.</p><p>So if you use alternative methods to install the program, make sure they come from trustworthy sources. In my view, these include projects like <a href="https://packages.msys2.org/base/mingw-w64-czkawka">https://packages.msys2.org/base/mingw-w64-czkawka</a> (MSYS2 Windows), <a href="https://formulae.brew.sh/formula/czkawka">https://formulae.brew.sh/formula/czkawka</a> (Brew macOS), and <a href="https://github.com/jlesage/docker-czkawka">https://github.com/jlesage/docker-czkawka</a> (Docker Linux).</p><h3>Other changes</h3><ul><li><strong>File logging</strong> — it’s now easier to check for panic errors and verify application behavior historically (mainly relevant for Windows, where both applications and users tend to avoid the terminal)</li><li><strong>Dependency updates</strong> — pdf-rs has been replaced with lopdf, and imagepipe + rawloader replaced with rawler (a fork of rawloader) which has more frequent commits, wider usage, and newer dependencies (making it easier to standardize across different libraries)</li><li><strong>More options for searching similar video files</strong> — I had been blissfully unaware that the vid_dup_finder_lib library only allowed adjusting video similarity levels; it turns out you can also configure the black-line detection algorithm and the amount of the ignored initial segment of a video</li><li><strong>Completely new icons</strong> — created by me (and admittedly uglier than the previous ones) under a CC BY 4.0 license, replacing the not-so-free icons</li><li>Binaries for Mac with HEIF support, czkawka_cli built with musl instead of eyre, and Krokiet with an alternative Skia backend — added to the release files on GitHub</li><li><strong>Faster resolution changes in image comparison mode (fast-image-resize crate) </strong>— this can no longer be disabled (because, honestly, why would anyone want to?)</li><li>Fixed a panic error that occurred when the GTK SVG decoder was missing or there was an issue loading icons using it (recently this problem appeared quite often on macOS)</li></ul><p>Full changelog: — <a href="https://github.com/qarmin/czkawka/blob/master/Changelog.md">https://github.com/qarmin/czkawka/blob/master/Changelog.md</a></p><p>Repository — <a href="https://github.com/qarmin/czkawka">https://github.com/qarmin/czkawka</a></p><p>License — MIT/GPL</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4991186b7ad1" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Czkawka / Krokiet 10.0, czyszczenie duplikatów, ujednolicanie funkcji i garść rustowych statystyk]]></title>
            <link>https://medium.com/@qarmin/czkawka-krokiet-10-0-czyszczenie-duplikat%C3%B3w-ujednolicanie-funkcji-i-gar%C5%9B%C4%87-rustowych-statystyk-7663ee6798ed?source=rss-9c8ac0ce6dcd------2</link>
            <guid isPermaLink="false">https://medium.com/p/7663ee6798ed</guid>
            <dc:creator><![CDATA[Qarmin]]></dc:creator>
            <pubDate>Mon, 18 Aug 2025 15:00:36 GMT</pubDate>
            <atom:updated>2025-08-18T15:00:36.612Z</atom:updated>
            <content:encoded><![CDATA[<p>Po prawie półrocznej przerwie ukazuje się dziś nowa wersja moich trzech różnych (choć zarazem podobnych do siebie) programów do wyszukiwania duplikatów, wraz z wbudowanymi kilkoma narzędziami do zarządzania plikami.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*BnQAWLVwLPxEZL6D0rffMg.png" /></figure><p>Lista poprawek i nowych funkcji może wydawać się przypadkowa i faktycznie taka jest, bo zajmowałem się nimi w takiej kolejności, w jakiej przychodziły mi do głowy pomysły na ich rozwiązanie. Wiem, że lista zgłoszonych issues na githubie jest dość długa i dla każdego użytkownika jego własny problem wydaje się najważniejszy, ale przy ograniczonym czasie mogę zająć się tylko niewielką częścią z nich i nie zawsze wybieram te najbardziej pilne.</p><p>Co ciekawe, ta wersja jest największa ze wszystkich dotychczasowych (przynajmniej jeśli liczyć liczbę zmienionych linii). Krokiet zawiera obecnie niemal wszystkie funkcje, z których korzystałem w wersji GTK, więc wygląda na to, że sam wkrótce w pełni na niego przeskoczę, dając przykład innym niezdecydowanym (przypominam, że wersja GTK jest już w trybie maintenance i skupiam się w niej wyłącznie na poprawkach błędów, a nie dodawaniu nowych funkcji).</p><p>Tradycyjnie pliki binarne dla wszystkich trzech projektów (czkawka_cli, krokiet i czkawka_gui), wraz z krótką legendą wyjaśniającą, do czego poszczególne nazwy się odnoszą i gdzie te pliki mogą być używane, znajdują się w zakładce <em>Releases</em> na GitHubie — <a href="https://github.com/qarmin/czkawka/releases">https://github.com/qarmin/czkawka/releases</a></p><h3>Dodanie limitów zużycia pamięci przy ładowaniu cache</h3><p>Jednym z losowych błędów, które występowały czasem z winy użytkownika, czasem z mojej winy a czasem np. z powodu tego, że brak prądu spowodował wyłączenie komputera w czasie działania, był tajemniczy crash przy rozpoczynaniu skanowania, który wypisywał do terminala informację</p><pre>memory allocation of 201863446528 bytes failed</pre><p>Odpowiednio uszkodzone przez użytkownika(lub zjawisko losowe) pliki cache, podczas ładowania przez bibliotekę bincode, wysypywały się przy próbie odczytania danych z tych plików. Inną sytuacją, jednak z identycznie wyglądającym błędem, było to, że przy próbie usunięcia z cache wpisów dotyczących nieistniejących/niedostępnych plików używałem niepoprawnej struktury do odczytywania danych(ale tutaj fixem, było to, że po prostu zmieniłem strukturę, do której chciałem dekodować te dane).</p><p>Sytuacja dość nieciekawa, bo użytkownikowi podczas skanowania/naciskania odpowiedniego przycisku, wysypuje się aplikacja i nie za bardzo wie co robić dalej. Bincode posiada możliwość dodania limitu pamięci, jaka będzie używana do dekodowania danych. Poprawka wymagała tylko kilku zmienionych linii i na tym możnaby zakończyć temat, jednak w czasie testów okazało się, że wprowadza to niekompatybilność wsteczną— danych zapisanych z konfiguracją z limitem pamięci, nie da się odczytać ze zwykłą konfiguracją i vice-versa.</p><pre>use std::collections::BTreeMap;<br>use bincode::{serialize_into, Options};<br><br>const MEMORY_LIMIT: u64 = 1024 * 1024 * 1024; // 1 GB<br>fn main() {<br>    let rands: Vec&lt;u32&gt; = (0..1).map(|_| rand::random::&lt;u32&gt;()).collect();<br>    let btreemap: BTreeMap&lt;u32, Vec&lt;u32&gt;&gt; =<br>        rands<br>            .iter()<br>            .map(|&amp;x| (x % 10, rands.clone()))<br>            .collect();<br><br>    let options = bincode::DefaultOptions::new().with_limit(MEMORY_LIMIT);<br>    let mut serialized: Vec&lt;_&gt; = Vec::new();<br>    options.serialize_into(&amp;mut serialized, &amp;btreemap).unwrap();<br>    println!(&quot;{:?}&quot;, serialized);<br><br>    let mut serialized2: Vec&lt;_&gt; = Vec::new();<br>    serialize_into(&amp;mut serialized2, &amp;btreemap).unwrap();<br>    println!(&quot;{:?}&quot;, serialized2);<br>}</pre><pre>[1, 1, 1, 252, 53, 7, 34, 7]<br>[1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 53, 7, 34, 7]</pre><p>Powyższy kod, przy serializacji danych, bez i z limitem, wypisuje dwa różne wyniki, co było dla mnie bardzo dziwne, bo myślałem, że ta opcja limitowania jest zawarta tylko kodzie dekodujący, a nie również w samym pliku(wydaje mi się, że większość bibliotek kodujących dane, nic poza samymi danymi do pliku). Tak więc chcąc nie chcąc ta wersja(idąc tropem poprzedniczek) ma niekompatybilny cache z poprzednimi wersjami. To była jedna z przyczyn dlaczego nie zrobiłem tego wcześniej — próbowałem limity dodawać jedynie przy wczytywaniu pliku, a nie zapisie(gdzie uznawałem, że to niepotrzebne) i to mi nie działało, więc nie kontynuowałem dalszych prób dodawania tej funkcjonalności.</p><p>Wiem, że pewnie to dla części z użytkowników problematyczne jest to, że niemal w każdej nowej wersji muszą od nowa zapełniać cache z rezultatami, bo z racji na zmienione struktury czy sposób obliczania danych nie jest możliwe po prostu odczytanie starych plików, więc w kolejnych wersjach postaram się nie grzebać zbytnio przy cache, jeśli nie będzie to potrzebne(choć niestety korci mnie, by kolejnej wersji dorzucić do plików wideo, kilka dodatkowych parametrów, wymuszających korzystanie z nowej wersji cache).</p><p>Alternatywą jest zrobienie wbudowanego narzędzia do migracji plików cache, jednak czytając losowe dane z zewnątrz bez zabezpieczenia w postaci limitów pamięci, to narzędzia byłoby bezużyteczne i skutkowałoby częstymi crashami. Takie narzędzie Jest możliwe do stworzenia dopiero od obecnej wersji i być może w przyszłości coś takiego będzie zaimplementowane.</p><h3>Tłumaczenia w Krokiecie</h3><p>By dorównać liście funkcji jakie obecnie są dostępne czkawce, postanowiłem wziąć się w garść i spróbować zaimplementować brakujące tłumaczenia, których brak powoduje, że dla części osób, mniej biegłych w porozumiewaniu się językiem angielskim trudniej jest używać aplikacji.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OhrakDHpB8TC_xAH0y97qQ.png" /><figcaption>Nawet chiński tradycyjny, nie stanowi problemu przy wyświetlaniu tekstów</figcaption></figure><p>Możnaby pomyśleć, że skoro sam Slint jest napisany w Rust, to użycie wewnątrz niego biblioteki Fluent, również napisanej w Rust, jest oczywistym i zrozumiałym krokiem. Otóż jednak, autorzy z różnych powodów uznali, że lepiej jest zamiast tego użyć chyba najpopularniejszego narzędzia do tłumaczeń — gettext, który jednak kompiluje kompilację i niemal uniemożliwia cross-compilację(issue celem zmiany tej sytuacji — <a href="https://github.com/slint-ui/slint/issues/3715">https://github.com/slint-ui/slint/issues/3715</a>).</p><p>Bez wbudowanego wsparcia dla tłumaczeń w slint, z całkiem prostej funkcjonalności zrobiła zagwozdka jak to w najlepszy rozwiązać. Moim celem była możliwość zmiany języka w runtime, bez konieczności restartu całej aplikacji.</p><p>Ostatecznie uznałem, że najlepiej będzie stworzyć Singleton ze wszystkimi tekstami do tłumaczeń w stylu:</p><pre>export global Translations {<br>    in-out property &lt;string&gt; ok_button_text: &quot;Ok&quot;;<br>    in-out property &lt;string&gt; cancel_button_text: &quot;Cancel&quot;;<br>    ...<br>}</pre><p>i używać tego jako</p><pre>export component PopupBase inherits PopupWindow {<br>    in-out property &lt;string&gt; ok_text &lt;=&gt; Translations.ok_button_text;<br>    ...<br>}</pre><p>a następnie przy zmianie języka oraz przy uruchamianiu aplikacji, wszystkie te atrybuty są aktualizowane w taki sposób</p><pre>app.global::&lt;Callabler&gt;().on_changed_language(move || {<br>    let app = a.upgrade().unwrap();<br>    let translation = app.global::&lt;Translations&gt;();    <br>    translation.set_ok_button_text(flk!(&quot;ok_button&quot;).into());<br>    translation.set_cancel_button_text(flk!(&quot;cancel_button&quot;).into());<br>    ...<br>});</pre><p>Przy ponad 200 tekstach do przetłumaczenia, bardzo łatwo o pomyłkę lub posiadanie niepodpiętych tłumaczeń, więc z tego powodu korzystam z pomocniczych skryptów pythonowych, które to weryfikują czy wszystko jest używane.</p><p>Trochę dużo kombinowania i niepotrzebnego kodu, niż gdyby wsparcie dla dla fluent-rs było wbudowane i można by było tego używać, podobnie jak obecnie to się ma z tłumaczeniami gettext, choć mam nadzieję, że wkrótce zostanie zaimplementowane coś w takim stylu dla fluent:</p><pre>export component PopupBase inherits PopupWindow {<br>    in-out property &lt;string&gt; ok_text: @tr(&quot;ok_button&quot;);<br>    ...<br>}</pre><p>Co do samych tłumaczeń, to są one hostowane i zmieniane na stronie crowdin — <a href="https://crowdin.com/project/czkawka">https://crowdin.com/project/czkawka</a> i co jakiś czas synchronizowane z githubem. Co każdą wersję, aktualizowanych jest kilka/kilkadziesiąt fraz, więc jestem zmuszony korzystać z maszynowego tłumaczenia na niektóre języki, więc nie wszystkie teksty mogą być przetłumaczone lub wyglądać tak jak powinny, więc nie krępujcie się ich poprawiać jeśli natraficie na niepoprawne.</p><h3>Ulepszanie Krokieta</h3><p>Jako główny cel tej wersji uznałem konieczność zmniejszenia różnic w funkcjach pomiędzy Czkawką (GUI) a Krokietem, tak bym mógł z czystym sumieniem polecać Krokieta jako alternatywę. Myślę, że w dużej mierze się to udało.</p><p>W trakcie tej operacji, często okazywało się, że tworzenie tych samych funkcji w slint jest to o wiele prostsze niż było to w wersji gtk. Weźmy jako przykład choćby sortowanie. Po stronie gtk, z racji braku lepszych znanych mi rozwiązań(a pewnie takie istnieją a ja żyję do tej pory w zupełnej nieświadomości, która to powoduje ból oczu, gdy patrzę końcową implementację jaką kiedyś zrobiłem) by posortować model, pobierałem iterator do niego a następnie przechodziłem przez kolejne elementy po kolei zbierając TreeIter do wektora. Następnie korzystając z sortowania bąbelkowego, pobierałem dane z określonej kolumny z danego wiersza i je sortowałem wewnątrz tego wektora. Nie muszę chyba wspominać, że nie dość, że to czasem jest wolne(zwłaszcza dla większych grup elementów), to kod jest trudny do zrozumienia i z jeszcze z niezrozumiałego powodu wysypuje się w losowych momentach</p><pre>fn popover_sort_general&lt;T&gt;(tree_view: &amp;gtk4::TreeView, column_sort: i32, column_header: i32)<br>where<br>    T: Ord + for&lt;&#39;b&gt; glib::value::FromValue&lt;&#39;b&gt; + &#39;static + Debug,<br>{<br>    let model = get_list_store(tree_view);<br>if let Some(curr_iter) = model.iter_first() {<br>        assert!(model.get::&lt;bool&gt;(&amp;curr_iter, column_header)); // First item should be header<br>        assert!(model.iter_next(&amp;curr_iter)); // Must be at least two items<br>        loop {<br>            let mut iters = Vec::new();<br>            let mut all_have = false;<br>            loop {<br>                if model.get::&lt;bool&gt;(&amp;curr_iter, column_header) {<br>                    assert!(model.iter_next(&amp;curr_iter), &quot;Empty header, this should not happens&quot;);<br>                    break;<br>                }<br>                iters.push(curr_iter);<br>                if !model.iter_next(&amp;curr_iter) {<br>                    all_have = true;<br>                    break;<br>                }<br>            }<br>            if iters.len() == 1 {<br>                continue; // Can be equal 1 in reference folders<br>            }<br>            sort_iters::&lt;T&gt;(&amp;model, iters, column_sort);<br>            if all_have {<br>                break;<br>            }<br>        }<br>    }<br>}<br><br>fn sort_iters&lt;T&gt;(model: &amp;ListStore, mut iters: Vec&lt;TreeIter&gt;, column_sort: i32)<br>where<br>    T: Ord + for&lt;&#39;b&gt; glib::value::FromValue&lt;&#39;b&gt; + &#39;static + Debug,<br>{<br>    assert!(iters.len() &gt;= 2);<br>    loop {<br>        let mut changed_item = false;<br>        for idx in 0..(iters.len() - 1) {<br>            if model.get::&lt;T&gt;(&amp;iters[idx], column_sort) &gt; model.get::&lt;T&gt;(&amp;iters[idx + 1], column_sort) {<br>                model.swap(&amp;iters[idx], &amp;iters[idx + 1]);<br>                iters.swap(idx, idx + 1);<br>                changed_item = true;<br>            }<br>        }<br>        if !changed_item {<br>            return;<br>        }<br>    }<br>}</pre><p>Z biegiem czasu widzę, że powinienem wcześniej opakować logikę zarządzania modelem, by mieć ułatwione zadanie przy jego odczytywaniu lub modyfikacji, ale obecnie już za późno na zmiany. Od strony Slinta, sytuacja wygląda o wiele prościej i bardziej “rustowo”</p><pre>pub(super) fn sort_modification_date(model: &amp;ModelRc&lt;MainListModel&gt;, active_tab: ActiveTab) -&gt; ModelRc&lt;MainListModel&gt; {<br>    let sort_function = |e: &amp;MainListModel| {<br>        let modification_date_col = active_tab.get_int_modification_date_idx();<br>        let val_int = e.val_int.iter().collect::&lt;Vec&lt;_&gt;&gt;();<br>        connect_i32_into_u64(val_int[modification_date_col], val_int[modification_date_col + 1])<br>    };<br><br>    let mut items = model.iter().collect::&lt;Vec&lt;_&gt;&gt;();<br>    items.sort_by_cached_key(&amp;sort_function);<br>    let new_model = ModelRc::new(VecModel::from(items));<br>    recalculate_small_selection_if_needed(&amp;new_model, active_tab);<br>    return new_model;<br>}</pre><p>Jest to o wiele krótsze, czytelniejsze i w większości przypadków szybsze(wersja gtk być może będzie szybsza, jeśli dane będą już niemal posortowane). Mimo to ciągle widać kilka dziwnych rzeczy typu</p><ul><li>zmienna <strong>modification_date_col</strong> —by nieco zgeneralizować model dla różnych narzędzi, dla każdego wiersza z rezultatem skanowania, istnieją wektory z danymi liczbowymi i stringowymi. Dla każdego narzędzia tych danych jest różna ilość i są w różnej kolejności, więc trzeba pobrać z informacji z aktualnej zakładki, gdzie potrzebne dane się obecnie znajdują</li><li>funkcja <strong>connect_i32_into_u64</strong>, jak sama nazwa wskazuje, łączy dwa i32 w u64 — jest to obejście problemu, że po stronie slinta nie ma obsługi intów 64 bitowych(ale mam dużą nadzieję, że wkrótce zostanie dodane dla nich wsparcie)</li><li>funkcja <strong>recalculate_small_selection_if_needed</strong>, przez brak wbudowanych widżetów z możliwością zaznaczania wielu elementów w slint(w przeciwieństwie do gtk), byłem zmuszony stworzyć taki widżet z całą logiką zaznaczania elementów, ich zmiany, etc. Dużo dodatkowego kodu, ale przynajmniej mam większą kontrolę nad zaznaczaniem, co się przydaje w niektórych sytuacjach</li></ul><p>Inną z przydatnych nowości które istniały już w czkawce, jest możliwość rozpoczęcia skanowania, wraz z listą wybranych folderów z poziomu CLI. Zatem obecnie wywołanie</p><pre>krokiet . Desktop -i /home/rafal/Downloads -e /home/rafal/Downloads/images</pre><p>odpali skanowanie z wyszukiwaniem plików w 3 folderach wraz z jednym wykluczonym (oczywiście o ile ścieżki istnieją — w przeciwnym razie, dana ścieżka będzie zignorowana). Z tym trybem jest powiązany osobny plik z konfiguracją, który załącza się w aplikacji gdy uruchomi się program z argumentami z wiersza poleceń(konfiguracja innych trybów nie jest przez to nadpisywana)</p><p>Dzięki temu, że niektóre rzeczy jest prościej robić w Krokiecie, dorobiłem w tej wersji do aplikacji kilka funkcji nieobecnych w czkawce:</p><ul><li>Zapamiętywanie rozmiaru okna oraz szerokości kolumn na poszczególnych ekranach</li><li>Możliwość ukrywania tekstu na ikonach(bardziej kompaktowe ui)</li><li>Ciemny i jasny motyw, zmieniane w czasie działania programu</li><li>Blokowanie niektórych przycisków, jeśli nie zaznaczono żadnych elementów</li><li>Informacja ile elementów jest skolejkowanych do usunięcia</li></ul><h3>Zakończenie wsparcia dla appimage</h3><p>Wraz z zakończeniem wspierania na linuxie snapów w poprzedniej wersji, z powodu trudności przy ich budowaniu, teraz przyszła pora na Appimage.</p><p>Głównym powodem ich porzucenia są niestandardowe błędy, które się wyświetlały podczas korzystania z nich oraz limitowana użyteczność ponad to co oferują zwykłe pliki binarne.</p><p>Osobiście jestem fanem formatu appimage i tam gdzie to możliwe go używam(no chyba, że aplikacja istnieje również w formacie flatpak/snap), przez co nie muszę się martwić o instalowanie zewnętrznych zależności i aktualizacje programu. Sprawdza się to świetnie dla programów z dużą liczbą zewnętrznych zależności, jednak w czkawce jedyne zależności jakie były wrzucane do środka to były zależności gtk4(co w zasadzie dużego sensu nie miało, bo niemal dla każdego linuxowego os, te biblioteki są już zainstalowane i często mają nałożone specjalne patche, by lepiej na nim działały np. w debianie są to — <a href="https://udd.debian.org/patches.cgi?src=gtk4&amp;version=4.18.6%2Bds-2">https://sources.debian.org/src/gtk4/4.18.6%2Bds-2/debian/patches/series/</a>).</p><p>Większy sens by był tego, gdybym wrzucał tam również inne biblioteki opcjonalne tj. ffmpeg, libheif czy libraw, ale nie miałem czasu i ochoty się w to bawić. Od czasu do czasu, część z użytkowników programu w wersji appimage, zaczęła zgłaszać problemy, niewystępujące w innych formatach, a jednocześnie niedające się zreprodukować a przez to zrozumieć i naprawić.</p><p>Dodatkowo, problematyczne było to, że sam plugin(<a href="https://github.com/linuxdeploy/linuxdeploy-plugin-gtk">https://github.com/linuxdeploy/linuxdeploy-plugin-gtk</a>) do pakowania zależności gtk, nie był aktualizowany od ponad 2 lat. Jego autorzy zrobili świetną robotę tworząc i rozwijając go w swoim wolnym czasie, jednak dużym problemem dla mnie było to, że nie trafiło to bezpośrednio pod skrzydła deweloperów gtk, którzy mogliby pomóc przy rozwoju przy tym przydatnym projekcie.</p><h3>Wielowątkowe przetwarzanie plików w Krokiecie i Cli</h3><p>Część użytkowników zwracała uwagę, że usuwanie czy kopiowanie plików z poziomu aplikacji jest czasochłonne, a przy tym brakuje jakiejkolwiek informacji zwrotnej o postępie. Dodatkowo w tym czasie całe GUI przestaje odpowiadać do czasu zakończenia procesu.</p><p>Problem wynikał z tego, że operacje na plikach były wykonywane w tym samym wątku, co rysowanie gui— przez brak aktualizacji interfejsu system uznawał aplikację za nieresponsywną i wyświetlał okno z możliwością jej ubicia.</p><p>Rozwiązanie tego problemu wydaje się stosunkowo proste — po prostu wystarczy wyrzucić obliczenia do nowego wątku. Pojawiają się jednak dwa nowe problemy — konieczność stopowania procesu przetwarzania plików i synchronizacji stanu wykonanych operacji z gui.</p><p>Do tego wystarczy prosta implementacja w stylu:</p><pre>let all_files = files.len();<br>let mut processing_files = Arc&lt;AtomicBool&lt;usize&gt;&gt;::new(0);<br>let _ = files.into_par_iter().map(|e| {<br>  if stop_flag.load(Ordering::Relaxed) {<br>    return None;<br>  }<br><br>  let processing_files = processing_files.fetch_add(1, Ordering::Relaxed);<br>  let status_to_send = Status { all_files, processing_files };<br>  progress_sender.send(status_to_send);<br><br>  // Processing file<br>}).while_some().collect::&lt;Vec&lt;_&gt;&gt;();</pre><p>Problem zaczyna się wtedy, gdy wysyłana jest duża ilość wiadomości, a aktualizowanie GUI/terminala dla każdej z nich byłoby kompletnie zbędne — w końcu mało kto byłby w stanie zauważyć i przetworzyć zmiany statusu pojawiające się nawet 60 razy na sekundę.</p><p>Powodowałoby to również problemy z wydajnością aplikacji oraz niepotrzebnie zwiększało zużycie zasobów systemowych. Potrzebowałem więc metody na ograniczenie liczby przesyłanych komunikatów. Mogłem to zrealizować po stronie elementu generującego komunikaty (wątek usuwający pliki) albo po stronie odbiorcy (wątek GUI/pasek postępu w CLI). Uznałem, że lepiej zrobić to wcześniej niż później.</p><p>Ostatecznie, „na czuja”, stworzyłem prostą strukturę, która za pomocą locka zapisuje najnowszą wiadomość do wysłania. Następnie w osobnym wątku, co ~100 ms, wiadomość jest pobierana i przekazywana do GUI. Mimo że rozwiązanie jest proste, to mam pewne obawy o jego wydajność na systemach z ogromną liczbą rdzeni — tam tysiące, a nawet dziesiątki tysięcy wiadomości na sekundę mogłyby powodować, że mutex zacznie ograniczać przepustowość. Na razie jednak nie testowałem tego w takich warunkach i obecnie nie sprawia to problemów, więc optymalizację odłożyłem na później (choć jestem otwarty na pomysły w jaki sposób można by to ulepszyć).</p><pre>pub struct DelayedSender&lt;T: Send + &#39;static&gt; {<br>    slot: Arc&lt;Mutex&lt;Option&lt;T&gt;&gt;&gt;,<br>    stop_flag: Arc&lt;AtomicBool&gt;,<br>}<br><br>impl&lt;T: Send + &#39;static&gt; DelayedSender&lt;T&gt; {<br>    pub fn new(sender: crossbeam_channel::Sender&lt;T&gt;, wait_time: Duration) -&gt; Self {<br>        let slot = Arc::new(Mutex::new(None));<br>        let slot_clone = Arc::clone(&amp;slot);<br>        let stop_flag = Arc::new(AtomicBool::new(false));<br>        let stop_flag_clone = Arc::clone(&amp;stop_flag);<br>        let _join = thread::spawn(move || {<br>            let mut last_send_time: Option&lt;Instant&gt; = None;<br>            let duration_between_checks = Duration::from_secs_f64(wait_time.as_secs_f64() / 5.0);<br><br>            loop {<br>                if stop_flag_clone.load(std::sync::atomic::Ordering::Relaxed) {<br>                    break;<br>                }<br>                if let Some(last_send_time) = last_send_time {<br>                    if last_send_time.elapsed() &lt; wait_time {<br>                        thread::sleep(duration_between_checks);<br>                        continue;<br>                    }<br>                }<br><br>                let Some(value) = slot_clone.lock().expect(&quot;Failed to lock slot in DelayedSender&quot;).take() else {<br>                    thread::sleep(duration_between_checks);<br>                    continue;<br>                };<br><br>                if stop_flag_clone.load(std::sync::atomic::Ordering::Relaxed) {<br>                    break;<br>                }<br>                if let Err(e) = sender.send(value) {<br>                    log::error!(&quot;Failed to send value: {e:?}&quot;);<br>                };<br>                last_send_time = Some(Instant::now());<br>            }<br>        });<br><br>        Self { slot, stop_flag }<br>    }<br><br>    pub fn send(&amp;self, value: T) {<br>        let mut slot = self.slot.lock().expect(&quot;Failed to lock slot in DelayedSender&quot;);<br>        *slot = Some(value);<br>    }<br>}<br><br>impl&lt;T: Send + &#39;static&gt; Drop for DelayedSender&lt;T&gt; {<br>    fn drop(&amp;mut self) {<br>        // We need to know, that after dropping DelayedSender, no more values will be sent<br>        // Previously some values were cached and sent after other later operations<br>        self.stop_flag.store(true, std::sync::atomic::Ordering::Relaxed);<br>    }<br>}</pre><h3>Zewnętrze zmiany</h3><h4>Alternatywne Gui</h4><p>W przypadku Krokieta i Czkawki zdecydowałem się pisać GUI w niskopoziomowych językach (Slint jest transpilowany do Rusta), zamiast korzystać z języków wyższego poziomu — głównie ze względu na wydajność oraz prostszą instalację.</p><p>W przypadku Krokieta przez chwilę zastanawiałem się nad użyciem Tauri, ale uznałem, że w moim przypadku Slint będzie lepszym rozwiązaniem, z prostszą kompilacją i brakiem potrzeby używania ciężkiego(i różnie działającego dla każdego systemu) webview z ts/js.</p><p>Jednak jednemu użytkownikowi chyba obecna sytuacja “guiaczowa”, niezbyt się podobała i postanowił stworzyć swoją własną alternatywę z wykorzystaniem Tauri.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*V1uLPdvEaiQjrpDo8oI4Dw.png" /></figure><p>Sam autor nie ukrywa, że wzorował się na wyglądzie Krokieta przy tworzeniu swojego programu, co widać na pierwszy rzut oka. Mimo to da się zauważyć różnice wynikające zarówno z kwestii gustu graficznego, jak i ograniczeń poszczególnych bibliotek (np. w tauriowej wersji popupy są stosowane częściej, ponieważ Slint ma z nimi problemy, więc unikałem ich używania, jak tylko mogłem).</p><p>Jako że nie jestem zbyt dobry w projektowaniu graficznym aplikacji, nie powinno dziwić, że w tym nowym GUI znalazłem kilka ciekawych rozwiązań, które będę chciał przenieść 1:1 albo wzorować się na nich, modyfikując Krokieta.</p><p>Ze wstępnych testów wynika, że aplikacja działa zaskakująco dobrze, pomimo drobnych problemów z wydajnością (jeden z trybów na Windowsie lekko się przyciął na dłuższą chwilę — choć tu może też być winny pakiet czkawka_core), lekkich niedociągnięć w GUI (np. możliwość zapisania aplikacji jako strony HTML) czy braku działającej wersji na Linuxa (miesiąc lub dwa temu udało mi się ją skompilować, a teraz już nie).</p><p>Link do projektu — <a href="https://github.com/shixinhuang99/czkawka-tauri">https://github.com/shixinhuang99/czkawka-tauri</a></p><h4>Czkawka w repo Debiana</h4><p>Ostatnio, tuż przed wydaniem Debiana 13, miała miejsce wiekopomna chwila — do repozytorium Debiana została dodana Czkawka 8.0.0 (choć istniała już wersja 9.0.0, ale cóż… Debian ma pewne upodobanie do starszych, bardziej stabilnych wersji i trzeba to uszanować). Dodania dokonał użytkownik <strong>Fab Stz</strong>.</p><p>Linki:<br>- <a href="https://packages.debian.org/sid/czkawka-gui">https://packages.debian.org/sid/czkawka-gui</a><br>- <a href="https://packages.debian.org/sid/czkawka-cli">https://packages.debian.org/sid/czkawka-cli</a></p><p>Debian bardzo poważnie podchodzi do kwestii powtarzalności kompilacji, więc szybko okazało się, że dwukrotne zbudowanie Czkawki w tym samym środowisku generuje dwa różne pliki binarne. Udało mi się zminimalizować problematyczny program do kilkuset linii. W swojej wielkiej mądrości (lub naiwności, zakładając, że błąd nie leży „między krzesłem a ekranem”) uznałem, że musi to być problem w samym Ruście. Jednak po analizie przeprowadzonej przez inne osoby okazało się, że winna jest biblioteka <strong>i18n-cargo-fl</strong>, której proc-macro iteruje po hashmapie z argumentami, a w Rust kolejność iteracji w takim przypadku jest losowa (<a href="https://github.com/kellpossible/cargo-i18n/issues/150">https://github.com/kellpossible/cargo-i18n/issues/150</a>)</p><p>Mając zidentyfikowane źródło problemu, przygotowałem poprawkę— <a href="https://github.com/kellpossible/cargo-i18n/pull/151">https://github.com/kellpossible/cargo-i18n/pull/151</a> która została już zmergowana i stała się częścią nowej wersji 0.10.0 biblioteki <em>cargo-i18n</em>. W repo debiana ciągle jest używana wersja 0.9.3, ale z wgraną tą poprawką. Co ciekawe, <em>cargo-i18n</em> jest używane także w wielu innych projektach, m.in. w aplikacjach z <strong>Cosmic DE</strong>, więc również one mają teraz ułatwioną drogę do osiągnięcia pełnej powtarzalności kompilacji.</p><h3>Czasy kompilacji i wielkość pliku binarnego</h3><p>Nigdy nie ukrywałem tego, że bardzo chętnie korzystam z zewnętrznych bibliotek, do prostego rozszerzania możliwości aplikacji, po to by nie musieć tracić czasu na wynajdywanie koła na nowo w niezbyt wydajnym i bardzo błędogennym procesie.</p><p>Mimo wielu oczywistych zalet, największymi minusami są większy rozmiar pliku wynikowego oraz dłuższe czasy kompilacji. Na moim starszym laptopie z 4 słabymi rdzeniami czasy kompilacji stały się tak długie, że przestałem na nim rozwijać ten program.</p><p>Jednak nie oznacza to, że bezrefleksyjnie korzystam z dodatkowych bibliotek. Często staram się ujednolicać wersje zależności lub korzystać z projektów, które są ciągle wspierane i aktualizują wersje używanych przez siebie bibliotek, np. rawler zamiast rawloader czy zamiast img-hash używania image-hasher(którego sam stworzyłem jako fork img-hash z podbitymi zależnościami)</p><p>Do celów weryfikacji problemu długich czasów kompilacji wygenerowałem kilka wykresów pokazujących, ile czasu Krokiet kompiluje się z różnymi opcjami, ile waży po poszczególnych optymalizacjach oraz ile trwa ponowna kompilacja po dodaniu komentarza (nie testowałem wydajności binarek, bo to dość skomplikowana kwestia). Dzięki temu mogłem rozważyć, które opcje warto wprowadzić do CI. Po zobaczeniu rezultatów uznałem, że warto przejść z obecnej konfiguracji — <strong><em>release + thin lto</em></strong> do <strong><em>release + fat lto + codegen units = 1</em></strong> .</p><p>Testy przeprowadzałem na 12 rdzeniowym AMD Ryzen 9 9700 i Ubuntu 25.04, linkerem mold oraz rustc 1.91.0-nightly (cd7cbe818 2025–08–15). Bazą są profile debug i release i to na ich podstawie zmieniałem niektóre opcje(nie wszystkie wydawały mi się warte testów a niektóre kombinacje wyrzucały różne błędy), by sprawdzić jaki mają wpływ na kompilację. Trzeba mieć na uwadze to, że Krokiet to projekt dość specyficzny, ma dużo zależności a dodatkowo slint generuje duży(~100k linii) plik rustowy, więc inne projekty mogą mieć znacząco inne czasy.</p><p>Wyniki testów:</p><pre>|Config                                              | Output File Size   | Target Folder Size   | Compilation Time   | Rebuild Time   |<br>|:---------------------------------------------------|:-------------------|:---------------------|:-------------------|:---------------|<br>| release + overflow checks                          | 73.49 MiB          | 2.07 GiB             | 1m 11s             | 20s            |<br>| debug                                              | 1004.52 MiB        | 7.00 GiB             | 1m 54s             | 3s             |<br>| debug + cranelift                                  | 624.43 MiB         | 5.25 GiB             | 47s                | 3s             |<br>| debug + debug disabled                             | 131.64 MiB         | 2.52 GiB             | 1m 33s             | 2s             |<br>| check                                              | -                  | 1.66 GiB             | 58s                | 1s             |<br>| release                                            | 70.50 MiB          | 2.04 GiB             | 2m 58s             | 2m 11s         |<br>| release + cranelift                                | 70.50 MiB          | 2.04 GiB             | 2m 59s             | 2m 10s         |<br>| release + debug info                               | 786.19 MiB         | 5.40 GiB             | 3m 23s             | 2m 18s         |<br>| release + native                                   | 67.22 MiB          | 1.98 GiB             | 3m 5s              | 2m 13s         |<br>| release + opt o2                                   | 70.09 MiB          | 2.04 GiB             | 2m 56s             | 2m 9s          |<br>| release + opt o1                                   | 76.55 MiB          | 1.98 GiB             | 1m 1s              | 18s            |<br>| release + thin lto                                 | 63.77 MiB          | 2.06 GiB             | 3m 12s             | 2m 32s         |<br>| release + optimize size                            | 66.93 MiB          | 1.93 GiB             | 1m 1s              | 18s            |<br>| release + fat lto                                  | 45.46 MiB          | 2.03 GiB             | 6m 18s             | 5m 38s         |<br>| release + cu 1                                     | 50.93 MiB          | 1.92 GiB             | 4m 9s              | 2m 56s         |<br>| release + panic abort                              | 56.81 MiB          | 1.97 GiB             | 2m 56s             | 2m 15s         |<br>| release + build-std                                | 70.72 MiB          | 2.23 GiB             | 3m 7s              | 2m 11s         |<br>| release + fat lto + cu 1 + panic abort             | 35.71 MiB          | 1.92 GiB             | 5m 44s             | 4m 47s         |<br>| release + fat lto + cu 1 + panic abort + native    | 35.94 MiB          | 1.87 GiB             | 6m 23s             | 5m 24s         |<br>| release + fat lto + cu 1 + panic abort + build-std | 33.97 MiB          | 2.11 GiB             | 5m 45s             | 4m 44s         |<br>| release + fat lto + cu 1                           | 40.65 MiB          | 1.95 GiB             | 6m 3s              | 5m 2s          |<br>| release + incremental                              | 71.45 MiB          | 2.38 GiB             | 1m 8s              | 2s             |<br>| release + incremental + fat lto                    | 44.81 MiB          | 2.44 GiB             | 4m 25s             | 3m 36s         |</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-7BK9knU6AWu8N7ir13OnQ.png" /><figcaption>Czasy kompilacji</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_sdLOzU23sgueQr3-8Zs-A.png" /><figcaption>Rozmiar pliku binarnego</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KAnzq3obMky0dZUku5tuTA.png" /><figcaption>Czas przebudowania aplikacji, po dodaniu komentarza</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*W9INTNlyc0dbjCnTh5ZD1A.png" /><figcaption>Rozmiar folderu target po kompilacji</figcaption></figure><p>Z takich rzeczy co mnie zadziwiły, to:</p><ul><li>build-std zwiększa, a nie zmniejsza rozmiar pliku</li><li>optimize-size jest szybkie, ale w niewielkim stopniu redukuje wielkość pliku wynikowy</li><li>fat-lto w tym projekcie działa o wiele lepiej niż thin-lto, choć często czytałem na internecie, że thin-tlo zwykle daje bardzo podobne wyniki do fat-lto</li><li>panic-abort — myślałem, że użycie tej opcji, nie zmieni zbytnio wielkości pliku, a plik zmniejszył się aż o 20%. Nie mogę jednak tej opcji wyłączać i nikomu tego nie radzę(mowa oczywiście o Krokiecie i Czkawce), bo przy pomocy zewnętrznych bibliotek, przetwarzają/sprawdzają/parsują obce pliki, które mogą powodować błędy paniki, a z opcją panic-abort nie da się ich łapać, więc aplikacja się wyłączy, zamiast wypisać błąd i kontynuować działanie.</li><li>release + incremental — to chyba będzie moja nowa ulubiona flaga, wydajność release i czasy rekompilacji z debug. Czasem potrzebuję połączenia tych dwóch rzeczy, choć dla pewności muszę to jeszcze lepiej potestować.</li></ul><p>Projekt, którego używałem do testów (stworzony na własne potrzeby więc może po prostu nie działać u innych osób a dodatkowo grzebie on w gicie, więc przed użyciem trzeba zacommitować wszystkie zmiany)— <a href="https://github.com/qarmin/czkawka/tree/master/misc/test_compilation_speed_size">https://github.com/qarmin/czkawka/tree/master/misc/test_compilation_speed_size</a></p><h3>Pliki z niesprawdzonych źródeł</h3><p>Ostatnimi czasy zarówno doszły mnie słuchy, jak i sam zauważyłem dziwne nowe strony, które wydają się sugerować, że są bezpośrednio powiązane z projektem (choć nigdzie nie jest to wprost napisane) i oferują jedynie pliki binarne przepakowane z GitHub, hostowane na własnych serwerach (co samo w sobie nie jest złe, ale w przyszłości może umożliwić ich podmianę na pliki złośliwe).</p><p>Osobiście zarządzam jedynie kilkoma projektami powiązanymi z Czkawką, a mianowicie repozytorium kodu na GitHub wraz z hostowanymi tam plikami binarnymi, wersją aplikacji w formacie Flatpak oraz projektami na crates.io. Wszystkie inne projekty są albo porzucone (np. aplikacja w Snap Store), albo zarządzane przez inne osoby.</p><p>Sama Czkawka nie posiada, żadnej strony internetowej i jej namiastką jest plik Readme.md wyświetlany na głównej stronie projektu na Githubie — nie mam w planach takiej oficjalnej strony tworzyć.</p><p>Zatem jeśli korzystacie z alternatywnych form instalacji programu, pamiętajcie by upewnić się, że są to źródła godne zaufania — według mnie, są nimi np. takie projekty jak np. <a href="https://packages.msys2.org/base/mingw-w64-czkawka">https://packages.msys2.org/base/mingw-w64-czkawka</a>(msys2 windows), <a href="https://formulae.brew.sh/formula/czkawka">https://formulae.brew.sh/formula/czkawka</a>(brew mac) czy<a href="https://github.com/jlesage/docker-czkawka"> https://github.com/jlesage/docker-czkawka</a>(docker linux)</p><h3>Inne zmiany</h3><ul><li><strong>Logowanie do pliku </strong>— teraz łatwiej będzie sprawdzać błędy paniki i weryfikować działanie aplikacji w przeszłości (głównie dotyczy Windowsa, na którym zarówno aplikacje, jak i sami użytkownicy zwykle trzymają się jak najdalej od terminala)</li><li><strong>Zmiany w zależnościach</strong> —pdf-rs został zastąpiony przez lopdf, a imagepipe + rawloader przez rawler (fork rawloader), które mają częstsze commity, są używane przez większą liczbę osób i mają nowsze zależności (ułatwia to ich ujednolicanie pomiędzy różnymi bibliotekami)</li><li><strong>Więcej opcji przy wyszukiwaniu podobnych plików wideo</strong> — żyłem w błogiej nieświadomości, że biblioteka <strong><em>vid_dup_finder_lib</em></strong> pozwala jedynie na ustawianie poziomu podobieństwa plików wideo, a okazało się, że można też regulować algorytm wykrywania czarnych linii i ilość ignorowanego fragmentu filmu z początku.</li><li><strong>Całkowicie nowe ikony</strong>, zrobione przeze mnie (i w dodatku brzydsze od poprzednich) na licencji CC BY 4.0, w miejsce niezbyt wolnych ikon.</li><li>Pliki binarne dla Maca ze wsparciem plików heif, czkawka_cli zbudowana z musl zamiast eyra oraz krokiet z alternatywnym backendem skia— zostały dodane do plików release na githubie</li><li><strong>Szybsza zmiana rozdzielczości w trybie porównywania obrazów</strong>(crate fast-image-resize) nie jest już możliwa do wyłączenia(bo w zasadzie po co to miałby ktoś robić?)</li><li>Naprawa błędu paniki, który występował, gdy w systemie brakowało dekodera GTK SVG lub pojawiał się problem z załadowaniem ikon przy jego użyciu (ostatnio problem ten pojawiał się dość często na Macu).</li></ul><p>Pełną listę zmian znajdziecie tutaj — <a href="https://github.com/qarmin/czkawka/blob/master/Changelog.md">https://github.com/qarmin/czkawka/blob/master/Changelog.md</a></p><p>Repozytorium — <a href="https://github.com/qarmin/czkawka">https://github.com/qarmin/czkawka</a></p><p>Licencja — MIT/GPL</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7663ee6798ed" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Czkawka/Krokiet 9.0 — Find duplicates faster than ever before]]></title>
            <link>https://medium.com/@qarmin/czkawka-krokiet-9-0-find-duplicates-faster-than-ever-before-c284ceaaad79?source=rss-9c8ac0ce6dcd------2</link>
            <guid isPermaLink="false">https://medium.com/p/c284ceaaad79</guid>
            <dc:creator><![CDATA[Qarmin]]></dc:creator>
            <pubDate>Sun, 16 Mar 2025 08:01:43 GMT</pubDate>
            <atom:updated>2025-03-16T08:01:43.410Z</atom:updated>
            <content:encoded><![CDATA[<h3>Czkawka/Krokiet 9.0 — Find duplicates faster than ever before</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0v-UCrj9mqpoS4SjzNrDfA.png" /><figcaption>Some say that Czkawka has one mode for removing duplicates and another for removing similar images. Nonsense. Both modes are for removing duplicates.</figcaption></figure><p>The current version primarily focuses on refining existing features and improving performance rather than introducing any spectacular new additions.</p><p>With each new release, it seems that I am slowly reaching the limits — of my patience, Rust’s performance, and the possibilities for further optimization.</p><p>Czkawka is now at a stage where, at first glance, it’s hard to see what exactly can still be optimized, though, of course, it’s not impossible.</p><h3>Changes and Optimizations</h3><p>For example, below is a performance graph from Hotspot, illustrating duplicate scanning in the current version on a 24-thread processor with an NVMe drive. It shows the application utilizing all available threads while searching for duplicates among 1.5 million files and computing hashes for Above:70 GB of data.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5Gq6yvLnXgRbfJApDgbuSQ.png" /></figure><p>As you can see, the application is highly parallelized. The view from the Hotspot program shows that it utilizes all available threads. However, in my case, this does not mean that each thread is fully utilized at 100%. Despite having a fast NVMe drive, the application spends most of its time waiting for the appropriate batch of data to be retrieved from the disk before it can process it.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d8uKKWxZ97mmREHNU12BQQ.png" /><figcaption>Above: RAM usage while hashing ~1 million files (max ~300MB).<br> Below: CPU usage ~10–40%</figcaption></figure><p>Still, in the left part of the hotspot graph, there is a visible 250 ms bar where only a single thread is active, handling memory operations (memory deallocation, vector operations). At the moment, I’m not entirely sure what causes this, but I believe it can be at least partially improved.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Wwbo9f-ktdO-h7yRMbKUvw.png" /></figure><p>One of the more unusual performance issues I encountered was that while scanning around a million files, the algorithm initially distributed tasks evenly across all available threads. However, in the second half of the process, it used only one thread.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*noMI5B1cKCWlSf6IyPTgBg.png" /></figure><p>An initial analysis indicated that Rayon, using its workload distribution algorithm, eventually move all the “leftover” tasks into a single thread, overloading it with work.</p><pre>files.into_par_iter().map(|e|…) // files is BTreeMap&lt;String, FileEntry&gt;</pre><p>Since these tasks are time-consuming (reading data from disk), I figured that enforcing a maximum grouping of elements up to 3 might help.</p><pre>let new_files = files.into_par_iter().collect::&lt;Vec&lt;_&gt;&gt;(); // Conversion to Vec&lt;&gt;, because with_max_len is not implemented for BTreeMap<br>non_cached_files_to_check.into_par_iter().with_max_len(3).map(|e|…) // Limiting group to max len 3,</pre><p>And indeed — this solution eliminated most of the issues, though I’m still not entirely sure why they occurred in the first place or why the problem wasn’t always present.</p><h3>Increasing the buffer size for file I/O operations</h3><p>Since the very beginning, file hashing has been performed by reading 16 KB chunks of a file’s content and then applying the hashing operation. This generally didn’t cause major issues (aside from stack overflow on some systems when using a 128 KB buffer).</p><p>However, some users reported that on HDDs under heavy load, frequent head jumps between scanned files occurred, negatively impacting performance and disc acoustics.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VSdGjtAQ8op4CawSQjMkIw.png" /><figcaption>Data read times for different buffer sizes</figcaption></figure><p>The simplest solution turned out to be increasing the buffer size to 2 MB — the downside is that each thread searching for duplicates will consume more memory. However, I find this trade-off fully acceptable, considering the performance boost it provides.</p><p>Tests showed that when scanning multiple files ranging from 50 KB to 50 MB on an HDD, scan time could be reduced by up to 50%, while on an SSD, the improvement ranged from a few to several percent.</p><p>To further enhance performance, I considered using semaphores to ensure that only one reading process would operate per disk at a time, minimizing head movement on HDDs.</p><p>However, tests showed no noticeable performance gains, so I abandoned the idea. That said, if you manage to convince me with a new implementation, I’m open to adding it in the future.</p><h3>Interrupting Hash Calculations</h3><p>In version 1.0, the entire GUI would freeze during hash calculations, making it impossible to monitor scan progress or stop the process.</p><p>By version 8.0, a scanned file counter already existed, and clicking <strong>Stop</strong> would cause the application to wait for all ongoing hash calculations to complete — meaning that if scanning a 4GB file had started, the user had to wait until it was fully processed.</p><p>In the latest version, I managed to implement <strong>near-instant scan interruption</strong> without any noticeable performance loss.</p><p>Additionally, in some modes, scan progress is now displayed in greater detail — not just the number of scanned files but also the total amount of bytes processed.</p><p>This provides a much clearer indication of how long the scan will take. Previously, the GUI sometimes displayed that nearly all files had been scanned, yet the process was still running, and the progress bar remained unchanged for a while.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*c0iRagUWVOB0ncsBUuz_CA.png" /></figure><h3>Multi-threading</h3><p>Probably the simplest way to improve performance is to process tasks in parallel across multiple threads.</p><p>One area where I applied this was in checking whether files within a given group are linked by hard links.<br>This operation requires access to disk resources, so in my case, checking 500,000 files initially took 4 seconds. After implementing the changes, the time dropped to around 1 second, suggesting that the task was no longer CPU-bound but instead limited by disk performance.</p><p>In practice, this modification came down to changing just a few lines of code, mainly replacing <strong>into_iter</strong> with its parallel equivalent <strong>into_par_iter</strong>. Since this is Rust, I was able to make this change without the slightest concern that it would introduce difficult-to-debug threading issues.</p><h3>Faster Image Resizing</h3><p>In the similar image search mode, the most demanding operations are reading the image from disk and subsequently resizing it (e.g., to 8x8, 16x16, or 32x32).<br>The larger the image, the slower these operations become.</p><p>There’s not much that can be done about the first issue — image-rs is already one of the most optimized libraries in this category (though it could likely be made even faster).</p><p>However, for image resizing, I discovered the <strong>fast-image-resize</strong> library, which — while introducing minimal differences in the appearance of the resized images compared to image-rs — can speed up the process by up to three times for large files.</p><p>Because of this, I decided to enable it by default, though users still have the option to disable it during compilation.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zot7d2JuRAZU05Emd4_fsw.png" /><figcaption>Resolution change time for 91 Images (4000x3000) depending on the used algorithm</figcaption></figure><h3>Portable mode</h3><p>Czkawka/Krokiet were designed to be portable applications by utilizing Rust and minimizing the number of dynamically linked libraries (mainly in the case of Krokiet). The goal was to ensure they run identically across different systems without requiring the installation of additional dependencies.</p><p>However, some users wanted the ability to store cache and settings on an external drive. I initially found this request somewhat unusual since each system typically has its own configuration and cache.</p><p>Nevertheless, since this was a frequently requested feature, I added the option to change the directory where the application creates its configuration and cache files. This makes it easy to transfer settings between computers without issues. When launching the application from the terminal, these paths are displayed there.</p><h3>Changes in current version</h3><h4>Breaking changes</h4><ul><li>Video, Duplicate (smaller prehash size), and Image cache (EXIF orientation + faster resize implementation) are incompatible with previous versions and need to be regenerated.</li></ul><h4>Core</h4><ul><li>Automatically rotating all images based on their EXIF orientation</li><li>Fixed a crash caused by negative time values on some operating systems</li><li>Updated `vid_dup_finder`; it can now detect similar videos shorter than 30 seconds</li><li>Added support for more JXL image formats (using a built-in JXL → image-rs converter)</li><li>Improved duplicate file detection by using a larger, reusable buffer for file reading</li><li>Added an option for significantly faster image resizing to speed up image hashing</li><li>Logs now include information about the operating system and compiled app features(only x86_64 versions)</li><li>Added size progress tracking in certain modes</li><li>Ability to stop hash calculations for large files mid-process</li><li>Implemented multithreading to speed up filtering of hard links</li><li>Reduced prehash read file size to a maximum of 4 KB</li><li>Fixed a slowdown at the end of scans when searching for duplicates on systems with a high number of CPU cores</li><li>Improved scan cancellation speed when collecting files to check</li><li>Added support for configuring config/cache paths using the `CZKAWKA_CONFIG_PATH` and `CZKAWKA_CACHE_PATH` environment variables</li><li>Fixed a crash in debug mode when checking broken files named `.mp3`</li><li>Catching panics from symphonia crashes in broken files mode</li><li>Printing a warning, when using `panic=abort`(that may speedup app and cause occasional crashes)</li></ul><h4>Krokiet</h4><ul><li>Changed the default tab to “Duplicate Files”</li></ul><h4>GTK GUI</h4><ul><li>Added a window icon in Wayland</li><li>Disabled the broken sort button</li></ul><h4>CLI</h4><ul><li>Added `-N` and `-M` flags to suppress printing results/warnings to the console</li><li>Fixed an issue where messages were not cleared at the end of a scan</li><li>Ability to disable cache via `-H` flag(useful for benchmarking)</li></ul><h4>Prebuild-binaries</h4><ul><li>This release is last version, that supports Ubuntu 20.04 github actions drops this OS in its runners</li><li>Linux and Mac binaries now are provided with two options x86_64 and arm64</li><li>Arm linux builds needs at least Ubuntu 24.04</li><li>Gtk 4.12 is used to build windows gtk gui instead gtk 4.10</li><li>Dropping support for snap builds — too much time-consuming to maintain and testing(also it is broken currently)</li><li>Removed native windows build krokiet version — now it is available only cross-compiled version from linux(should not be any difference)</li></ul><h3>Next version</h3><p>In the next version, I will likely focus on implementing missing features in Krokiet that are already available in Czkawka, such as selecting multiple items using the mouse and keyboard or comparing images.</p><p>Although I generally view the transition from GTK to Slint positively, I still encounter certain issues that require additional effort, even though they worked seamlessly in GTK. This includes problems with popups and the need to create some widgets almost from scratch due to the lack of documentation and examples for what I consider basic components, such as an equivalent of GTK’s TreeView.</p><p>Price — free, so take it for yourself, your friends, and your family. Licensed under MIT/GPL</p><p>Repository — <a href="https://github.com/qarmin/czkawka">https://github.com/qarmin/czkawka</a></p><p>Files to download — <a href="https://github.com/qarmin/czkawka/releases">https://github.com/qarmin/czkawka/releases</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c284ceaaad79" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Czkawka/Krokiet 9.0 — Wyszukiwarka duplikatów, szybsza niż kiedykolwiek]]></title>
            <link>https://medium.com/@qarmin/czkawka-krokiet-9-0-wyszukiwarka-duplikat%C3%B3w-szybsza-ni%C5%BC-kiedykolwiek-6276f23d4eb9?source=rss-9c8ac0ce6dcd------2</link>
            <guid isPermaLink="false">https://medium.com/p/6276f23d4eb9</guid>
            <dc:creator><![CDATA[Qarmin]]></dc:creator>
            <pubDate>Sun, 16 Mar 2025 08:01:30 GMT</pubDate>
            <atom:updated>2025-03-16T08:30:51.615Z</atom:updated>
            <content:encoded><![CDATA[<h3>Czkawka/Krokiet 9.0 — Wyszukiwarka duplikatów, szybsza niż kiedykolwiek</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0v-UCrj9mqpoS4SjzNrDfA.png" /><figcaption>Niektórzy powiadają, że Czkawka ma jeden tryb do usuwania duplikatów, a drugi do usuwania podobnych obrazów. Bzdura. Oba tryby są do usuwania duplikatów</figcaption></figure><p>Obecna wersja skupia się głównie na poprawie istniejących elementów i przyspieszeniu działania programu, bez nowych spektakularnych dużych elementów</p><p>Z każdym kolejnym wydaniem, wygląda na to, że powoli zbliżam się do granic — swojej cierpliwości, wydajności Rusta i możliwości dalszej optymalizacji.</p><p>Czkawka znajduje się w miejscu, gdzie na pierwszy rzut oka trudno dostrzec, co dokładnie można jeszcze zoptymalizować, choć oczywiście nie jest to niemożliwe. Konieczne jest więc używanie programów wspomagających, tj. hotspot —<a href="https://github.com/KDAB/hotspot"> https://github.com/KDAB/hotspot</a></p><h3>Zmiany i optymalizacje</h3><p>Przykładowo, poniżej znajdują się wyniki skanowania duplikatów w obecnej wersji na 24-wątkowym procesorze z dyskiem NVMe przy wyszukiwaniu duplikatów z 1,5 miliona plików, z obliczaniem hashy dla 70 GB danych:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*5Gq6yvLnXgRbfJApDgbuSQ.png" /></figure><p>Jak widać, aplikacja jest mocno zrównoleglona. Widok z programu Hotspot pokazuje, że wykorzystuje wszystkie dostępne wątki, jednak w moim przypadku nie oznacza to, że każdy z nich jest obciążony w 100%. Mimo szybkiego dysku NVMe, większość czasu aplikacja spędza, czekając na pobranie odpowiedniej partii danych z dysku, zanim będzie mogła nad nimi pracować.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d8uKKWxZ97mmREHNU12BQQ.png" /><figcaption>Wyżej zużycie ramu przy hashowaniu ~1 miliona plików(max ~300MB), a na dole zużycie CPU ~10–40%</figcaption></figure><p>Jednak nadal widoczny w lewej części wykresu pasek o długości 250 ms, w którym aktywny jest tylko jeden wątek, zajmujący się operacjami na pamięci(zwalnianie pamięci, operacje na wektorze) — obecnie nie jestem pewien, co dokładnie to powoduje ale myślę, że przynajmniej częściowo da się to poprawić.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Wwbo9f-ktdO-h7yRMbKUvw.png" /></figure><p>Jednym z bardziej nietypowych problemów z wydajnością, jakie miałem, było to, że podczas skanowania około miliona plików algorytm początkowo poprawnie rozdzielał zadania na wszystkie dostępne wątki, ale w drugiej połowie używał już tylko jednego.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*noMI5B1cKCWlSf6IyPTgBg.png" /></figure><p>Wstępna analiza wskazywała, że rayon, stosując swój algorytm podziału pracy, w pewnym momencie przekierowuje wszystkie „resztki” do jednego wątku, który zostaje przeciążony tymi zadaniami.</p><pre>files.into_par_iter().map(|e|…) // files is BTreeMap&lt;String, FileEntry&gt;</pre><p>Ponieważ zadania są czasochłonne (czytanie danych z dysku), uznałem, że wymuszenie maksymalnego grupowania elementów do 3 może pomóc.</p><pre>let new_files = files.into_par_iter().collect::&lt;Vec&lt;_&gt;&gt;(); // Conversion to Vec&lt;&gt;, because with_max_len is not implemented for BTreeMap<br>non_cached_files_to_check.into_par_iter().with_max_len(3).map(|e|…) // Limiting group to max len 3,</pre><p>I rzeczywiście — rozwiązanie to wyeliminowało większość problemów, choć dalej nie jestem w pełni pewien dlaczego takie coś występowało ani dlaczego nie zawsze problem występował.</p><h3><strong>Zwiększenie wielkości bufora do odczytywania plików</strong></h3><p>Od niemal samego początku hash plików był obliczany poprzez odczytywanie 16 KB ich zawartości, a następnie wykonywanie operacji haszowania.</p><p>Nie sprawiało to większych problemów (pomijając stack overflow na niektórych systemach przy buforze wielkości 128 KB).</p><p>Jednak część użytkowników zgłaszała, że w przypadku dysków HDD przy dużym obciążeniu dochodzi do częstych skoków głowicy pomiędzy sprawdzanymi plikami, co negatywnie wpływa na wydajność i akustykę.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*VSdGjtAQ8op4CawSQjMkIw.png" /><figcaption>Czasy odczytu danych z plików, dla określonych wielkości buforów</figcaption></figure><p>Najprostszym rozwiązaniem okazało się zwiększenie rozmiaru bufora do 2 MB — minusem jest jednak to, że każdy wątek wyszukujący duplikaty, będzie brał o tyle więcej pamięci, co jednak jest według mnie, w pełni akceptowalne zważając na zwiększenie wydajności.</p><p>Testy wykazały, że w przypadku skanowania wielu plików o rozmiarach 50 KB — 50 MB na HDD można spodziewać się skrócenia czasu skanowania nawet o 50%, a w przypadku SSD o kilka–kilkanaście procent.</p><p>Aby jeszcze bardziej poprawić wyniki, rozważałem zastosowanie semaforów, aby dla danego dysku w danym momencie działał tylko jeden proces czytający. Ograniczyłoby to skakanie głowicy po dysku.</p><p>Jednak testy nie wykazały żadnej poprawy wydajności, więc porzuciłem ten pomysł, choć jeśli uda się wam mnie przekonać nową implementacją, to oczywiście nic nie stoi na przeszkodzie bym to dodał.</p><h3><strong>Przerywanie obliczania hashy</strong></h3><p>W wersji 1.0, podczas obliczania hashy całe GUI się zawieszało, uniemożliwiając podgląd postępu skanowania i jego zatrzymanie.</p><p>W wersji 8.0 istnieje licznik przeskanowanych plików, a po kliknięciu w stop, aplikacja czeka na zakończenie wszystkich zadań obliczania hashy — co oznaczało, że jeśli rozpoczęto skanowanie pliku o rozmiarze 4 GB, należało poczekać na jego pełne przetworzenie.</p><p>W najnowszej wersji udało mi się dodać niemal natychmiastowe zatrzymywanie skanowania, bez widocznej straty wydajności.</p><p>Dodatkowo, w niektórych trybach widoczny jest teraz bardziej szczegółowy progres skanowania — nie tylko liczba przeskanowanych plików, ale także ilość sprawdzonych bajtów.</p><p>Dzięki temu użytkownik ma znacznie lepszy wgląd w to, jak długo potrwa skanowanie, gdyż wcześniej czasem gui pokazywało, że przeskanowano wszystkie/niemal wszystkie pliki ale jednak skanowanie wciąż trwało a pasek postępu się nie zmieniał.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*c0iRagUWVOB0ncsBUuz_CA.png" /></figure><h3><strong>Multi-threading</strong></h3><p>Chyba najprostszym sposobem zwiększenia wydajności jest równoległe przetwarzanie zadań w wielu wątkach.</p><p>Jednym z miejsc, gdzie to zastosowałem, jest sprawdzanie, czy pliki w danej grupie są powiązane hard-linkami.</p><p>Operacja ta wymaga dostępu do zasobów dyskowych, więc w moim przypadku sprawdzenie 500 tysięcy plików zajmowało 4 sekundy. Po wprowadzonych zmianach czas ten spadł do około 1 sekundy, co sugeruje, że zadanie przestało być ograniczane przez CPU i stało się zależne od wydajności dysku.</p><p>W praktyce ta modyfikacja ograniczała się do zmiany kilku linijek kodu, głównie zastąpienia into_iter jego równoległym odpowiednikiem into_par_iter. Z racji, ze to Rust, to mogłem to zrobić bez najmniejszej obawy, że doprowadzi to do trudnych do debugowania problemów z wątkami.</p><h3>Szybsza zmiana rozdzielczości obrazów</h3><p>W trybie wyszukiwania podobnych obrazów, najbardziej obciążającymi operacjami są odczytywanie obrazu z dysku oraz późniejsza zmiana rozdzielczości tego pliku(do np. 8x8, 16x16, 32x32)</p><p>Im większy obraz, tym wolniejsze są te operacje.</p><p>Na pierwszą kwestię niewiele można poradzić — image-rs to jedna z najlepiej zoptymalizowanych bibliotek w tej kategorii (choć prawdopodobnie można by ją jeszcze przyspieszyć).</p><p>Natomiast w kwestii zmiany rozmiaru obrazów odkryłem bibliotekę fast-image-resize, która – przy minimalnych różnicach w wyglądzie wynikowych obrazów względem image-rs – pozwala nawet trzykrotnie przyspieszyć ten proces w przypadku dużych plików.</p><p>Z tego powodu zdecydowałem się domyślnie ją włączyć, choć użytkownicy nadal mogą z niej zrezygnować przy kompilacji.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zot7d2JuRAZU05Emd4_fsw.png" /><figcaption>Czas zmiany rozdzielczości 91 obrazów 4000x3000, w zależności od użytego algorytmu</figcaption></figure><h3>Dodanie trybu portable dla aplikacji</h3><p>Czkawka/Krokiet w swoich założeniach, przez używanie rusta i minimalnej ilości dynamicznie linkowanych bibliotek(tu głównie Krokiet) miały być przenośnymi aplikacjami, które działałyby wszędzie identycznie, bez konieczności instalacji specjalnych zależności.</p><p>Jednak część użytkowników pragnęła zapisywania cache/ustawień na dysk zewnętrzny, co według mnie było dość dziwne, bo zwykle każdy system ma inny plik konfiguracji oraz cache.</p><p>Z racji, że jednak to był dość oczekiwany feature, to dodałem możliwość zmiany katalogu, gdzie aplikacja tworzy plik konfiguracyjny i pliki cache, tak by można było je przenosić bez problemu pomiędzy komputerami. Przy uruchamianiu aplikacji z poziomu terminala, te ścieżki są do niego wypisywane.</p><h3>Lista zmian w obecnej wersji</h3><h4>Kompatybilność z poprzednimi wersjami</h4><ul><li>Cache dla wideo(nowa wersja biblioteki), duplikatów (mniejszy rozmiar prehash) oraz obrazów (orientacja EXIF + szybsza zmiana rozmiaru) jest niekompatybilny z poprzednimi wersjami i wymaga ponownego wygenerowania</li></ul><h4>Core</h4><ul><li>Automatyczne obracanie obrazów na podstawie ich orientacji EXIF</li><li>Naprawiono błędy paniki spowodowane ujemnymi wartościami czasu w niektórych systemach operacyjnych</li><li>Zaktualizowano `vid_dup_finder` - teraz wykrywa podobne filmy krótsze niż 30 sekund</li><li>Dodano obsługę większej liczby formatów JXL (za pomocą wbudowanego konwertera JXL → image-rs)</li><li>Poprawiona detekcja zduplikowanych plików dzięki użyciu większego, współdzielonego bufora do odczytu plików</li><li>Dodano opcję znacznie szybszego skalowania obrazów, aby przyspieszyć hashowanie</li><li>Logi zawierają teraz informacje o systemie operacyjnym i opcjach kompilacji aplikacji (tylko wersje x86_64)</li><li>Dodano śledzenie progresu na podstawie rozmiaru w niektórych trybach</li><li>Możliwość zatrzymania obliczania hashy dla dużych plików w trakcie procesu</li><li>Wprowadzono wielowątkowość, aby przyspieszyć filtrowanie hard-linków</li><li>Dla funkcji “prehash”, zmniejszono rozmiar pliku koniecznego do odczytu do 4 KB</li><li>Naprawiono spowolnienie na końcu skanowania podczas wyszukiwania duplikatów na systemach z dużą liczbą rdzeni CPU</li><li>Przyspieszono przerywanie skanowania podczas zbierania plików do sprawdzenia</li><li>Dodano obsługę zmiany ścieżek konfiguracji/cache za pomocą zmiennych środowiskowych `CZKAWKA_CONFIG_PATH` i `CZKAWKA_CACHE_PATH`</li><li>Naprawiono awarię w trybie debugowym podczas sprawdzania uszkodzonych plików o nazwie typu `.mp3`.</li><li>Przechwytywanie błędów paniki z biblioteki symphonia w trybie sprawdzania uszkodzonych plików.</li><li>Dodano ostrzeżenie podczas korzystania z `panic=abort` (może to przyspieszyć działanie aplikacji, ale powodować sporadyczne błędy paniki).</li></ul><h4>Krokiet</h4><ul><li>Domyślna zakładka została zmieniona na “Duplicate Files”.</li></ul><h4>GTK GUI</h4><ul><li>Dodano ikonę okna w Wayland.</li><li>Wyłączono niedziałający przycisk sortowania.</li></ul><h4>CLI</h4><ul><li>Dodano flagi `-N` i `-M`, które pozwalają ukryć wyniki/ostrzeżenia w konsoli.</li><li>Naprawiono problem, w którym wiadomości nie były usuwane po zakończeniu skanowania.</li><li>Możliwość wyłączenia cache za pomocą flagi `-H` (przydatne do testów wydajnościowych).</li></ul><h4>Binarki</h4><ul><li>To ostatnia wersja wspierająca Ubuntu 20.04 — GitHub Actions usuwa to wydanie ze swoich runnerów</li><li>Binarki dla Linuxa i Maca są teraz dostępne w dwóch wariantach: x86_64 i arm64</li><li>Wersje dla procesorów ARM na Linuksie wymagają co najmniej Ubuntu 24.04</li><li>GTK GUI dla Windowsa używa GTK 4.12 zamiast 4.10</li><li>Usunięto wsparcie dla Snap — jego utrzymanie i testowanie zajmowało zbyt dużo czasu (i tak nie działało to poprawnie od dwóch wersji)</li><li>Usunięto natywną wersję Krokieta dla Windowsa — teraz dostępna jest tylko wersja cross-kompilowana z Linuksa (nie powinno być żadnych różnic)</li></ul><h3><strong>Następna wersja</strong></h3><p>Zapewne w następnej wersji skupię się na implementacji brakujących funkcji Krokieta, które są już dostępne w Czkawce, takich jak zaznaczanie wielu elementów za pomocą myszy i klawiatury czy porównywanie obrazów.</p><p>Mimo że ogólnie pozytywnie oceniam przejście z GTK na Slint, nadal napotykam na pewne problemy, które wymagają dodatkowej pracy, mimo że w GTK działały bez przeszkód. Dotyczy to m.in. kłopotów z popupami czy konieczności tworzenia niektórych widżetów niemal od zera, ze względu na brak dokumentacji i przykładów dla, moim zdaniem, podstawowych komponentów, takich jak odpowiednik TreeView z GTK.</p><p>Cena — darmo, więc bierzcie do woli dla siebie, znajomych i rodziny, licencja MIT/GPL</p><p>Repozytorium — <a href="https://github.com/qarmin/czkawka">https://github.com/qarmin/czkawka</a></p><p>Pliki do pobrania — <a href="https://github.com/qarmin/czkawka/releases">https://github.com/qarmin/czkawka/releases</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6276f23d4eb9" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Czkawka 7.0]]></title>
            <link>https://medium.com/@qarmin/czkawka-7-0-a465036e8788?source=rss-9c8ac0ce6dcd------2</link>
            <guid isPermaLink="false">https://medium.com/p/a465036e8788</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[czkawka]]></category>
            <category><![CDATA[rust]]></category>
            <category><![CDATA[cleaners]]></category>
            <category><![CDATA[duplicatecleaner]]></category>
            <dc:creator><![CDATA[Qarmin]]></dc:creator>
            <pubDate>Mon, 19 Feb 2024 11:47:35 GMT</pubDate>
            <atom:updated>2024-02-19T12:38:41.752Z</atom:updated>
            <content:encoded><![CDATA[<h3>Czkawka 7.0 — Krokiet, a new gui in Slint, performance improvements and a handful of new features for the data cleaner.</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9DSJvNxaEPeg-NlEFUVIbQ.png" /><figcaption>Krokiet 7.0, new gui</figcaption></figure><p>I am happy to introduce the new version 7.0 of <a href="https://github.com/qarmin/czkawka">Czkawka</a> app — a fully free program for finding duplicates, broken and temporary files, similar images, videos, empty files and folders and several other things.</p><p>In the three years of the application’s existence, I have thought about stopping development many times, because it contains almost everything I use on a daily basis, and adding and fixing thousands of lines of code, testing new features, modifying and verifying the CI or thinking about the implementation of the gui can be very tedious, so I have had to take breaks in order not to burn myself out completely and abandon the project, but each time I came across elements that would be nice to add/change, so the application is constantly being developed.</p><p>As you may have seen on <a href="https://github.com/qarmin/czkawka/issues">github</a>, the number of issues has long since exceeded 300 and is constantly growing. Most of them mainly concern new features, which I only consider a small part of as implementable, and implement even less. This is not due to my laziness (although I admit I am a lazy person), but because I do not see sufficient user need for the implementation of a feature, or it would take too much time to implement or maintain it - at the start of this project I implemented a large number of new features without thinking too much, so that over the following years I had to gradually improve and generalize the code.</p><p>This version is probably the biggest, and all due to:</p><h3>New Gui</h3><p>The biggest feature in this version, is the new user interface “Krokiet”, written completely from scratch, which, unlike the existing interface, uses the Slint library instead of GTK, which, like Czkawka, is written in Rust.</p><p>The reason for the change was a number of bugs, the difficulty of packaging, and the need for a major redesign of the application, because almost everywhere are used deprecated items like TreeView, which are obsolete and will not be available in GTK 5(<a href="https://docs.gtk.org/gtk4/class.TreeView.html">https://docs.gtk.org/gtk4/class.TreeView.html</a>).</p><p>I tested the use of various libraries such as gtk, qt, tauri, slint, egui, iced, but the final choice was slint — a cursory and subjective overview of the pros and cons is available here— <a href="https://github.com/qarmin/czkawka/tree/master/krokiet#why-slint">https://github.com/qarmin/czkawka/tree/master/krokiet#why-slint</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7dPKpjjPxCM3CDp2KFN85w.png" /><figcaption>Czkawka on the left and Krokiet on the right</figcaption></figure><p>Differences in favour of Krokiet(and Slint)</p><ul><li>Almost entirely static binary files — GTK is usually dynamically linked, making the application itself dependent on the GTK version as well as all dependent libraries on the system. On Windows, due to the lack of a gtk4 installer, I had to manually distribute all the dll files along with the application. Krokiet, on the other hand, is on each platform distributed as a single file, which is much easier to transfer and use.</li><li>Operation outside Linux — Gtk was primarily developed with Linux in mind. The Windows and Mac port is horribly buggy (although there are sometimes problems on Linux too). Random crashes, not starting up, not working correctly — these are just some of the problems reported on github, which usually I cannot reproduce and fix.</li><li>Compiling under Windows — I have tried to compile Czkawka several times under Windows, but as it happens with gtk, it failed to compile. For this reason, I had to use a complicated cross-compilation method due to the large amount of dynamic libraries. Slint uses almost entirely rust dependencies, which makes it very easy and pleasant to compile.</li><li>GUI creation without compilation — GTK 4 has Cambalache which is great for GUI creation, but it is not an official tool, so the author creating a program does not have to follow the standard that GTK promotes/imposes and also this application does not have the community support it deserves. Slint, unlike Cambalache, does not have a drag&amp;drop editor, but makes up for it with a built-in lsp editor which displays the interface in real time and allows to execute functions and code within the slint files, which is really useful and makes it easier to test changes, especially as compiling a program in slint is quite long. In the case of Krokiet, generated rust code from slint files have 150,000 lines after reformatting.</li><li>More rust api — in GTK, due of the use of TreeView, I often worked with TreeIter, which are wrappers created by gtk-rs, containing pointers to the model underneath, which were very easy to misuse them — and that’s not why I use rust, to deal with memory errors and use address sanitizer/valgrind.</li></ul><p>Differences in favour of Czkawka(and GTK):</p><ul><li>More features — I developed Czkawka for over 3 years, so it’s logical that the amount of code in it is significant, and I wasn’t able to rewrite everything, especially since a substantial part of the code is closely tied to GTK (mainly operations on iterators in models). Additionally, Slint doesn’t have sufficiently developed equivalents for some GTK widgets.</li><li>More advanced default components — For example, a ListView in GTK has intuitive logic for selecting multiple items using the keyboard, and in Slint, such functionality needs to be implemented manually. Also, other widgets in GTK have many more settings that can change their default behavior, eliminating the need to constantly reinvent the same widgets with only minor differences.</li><li>Multilingualism — In Czkawka, I used the fluent library to provide users ability to change the language, which worked very well, was easy to compile, and had a very simple key -&gt; value format. However, the creators of Slint have limited the use of external tools to gettext in their language. I have two issues with this translation method. Firstly, it is a tool written in C, which complicates compilation and cross-compilation. Secondly, the file format doesn’t appeal to me; while it may better illustrate which phrases are used in the code, for me, it is not necessary for a small project and complicates translations.</li></ul><p>I found a few bugs while using Slint, so it’s fair to say that in return for the product, I ran tests on it for free</p><ul><li>Detection of the problem of the build.rs script taking too long to execute, this was eventually reduced from 8s to 2s(in a medium sized project like Krokiet)— <a href="https://github.com/slint-ui/slint/discussions/4215">https://github.com/slint-ui/slint/discussions/4215</a></li><li>Performance issue with the accessibility feature — <a href="https://github.com/slint-ui/slint/issues/3867">https://github.com/slint-ui/slint/issues/3867</a></li><li>Various reports of application crashes — <a href="https://github.com/slint-ui/slint/issues/4035">https://github.com/slint-ui/slint/issues/4035</a>, <a href="https://github.com/slint-ui/slint/issues/3700">https://github.com/slint-ui/slint/issues/3700</a></li><li>Numerous other suggestions and error reports.</li></ul><p>As you can see, Krokiet is not yet finished, and there are several elements that can be improved:</p><ul><li>Logo — shows my graphic design skills in full glory; however, I am thinking about adding something more “official”</li><li>Icons — I managed to create two icons, which are not masterpieces either. I looked for an AI tool for generating icons, but found nothing.</li><li>Implementation of some missing elements from Czkawka</li><li>Progress bar during file deletion/movement</li><li>Improvement of the layout of certain elements — despite having the GUI already created, I still wonder if it can be enhanced to be more user-friendly</li><li>Moving the arrows through the results</li><li>Mouse/keyboard selection of multiple records</li><li>Reversing the selection with the middle mouse button</li></ul><h4>The Future of Czkawka — Gui GTK version</h4><p>Alright, but you’re probably wondering about the future of the current interface?</p><p>In fact, nothing spectacular is going to happen; I’m simply minimizing amount of bug fixing and adding new features. Since czkawka_core is shared by all components, changes/improvements in this library will be visible in this GUI version, even though it won’t be directly modified.</p><p>Application will probably live ~10–15 years until distributions phase out Gtk 4 from their package repositories, so there’s more than enough time for a transition. Of course, there’s a chance that someone might want to update the GTK version in the application, but being a realist, I know that it’s almost impossible.</p><h3>Performance improvements</h3><p>In this version, apart from the usual fixes and improvements, while reviewing the code after a break, I noticed that some elements in the project are not as efficient as they could be. This is because certain parts of the program were not updated since I created the app.</p><ul><li><strong>Reaching for file metadata only when necessary</strong></li></ul><p>While collecting files/folders to scan, I have to check whether a particular element in the folder is a file/folder to assign it appropriately depending on the mode. Previously, I fetched metadata of entry at the start and then using the is_file/is_dir methods on a variable of type Metadata, which result in unnecessary disk data retrieval.</p><pre>let Ok(read_dir) = fs::read_dir(current_folder) else {<br>  return;<br>};<br>for entry in read_dir.into_iter().flatten() {<br>  let Ok(metadata) = entry.metadata() else {<br>    continue;<br>  }<br>  if metadata.is_file() {<br>    if is_valid(&amp;entry) {<br>      // Process record<br>    }<br>  } else if metadata.is_dir() {<br>    if is_valid(&amp;entry) {<br>      // Process record<br>    }<br>  }<br>}</pre><p>While reviewing DirEntry API, it turned out that it already contains information about the file type. So, fetching file metadata could be delayed and performed on a smaller number of files, filtered by other, less time-consuming methods.</p><pre>let Ok(read_dir) = fs::read_dir(current_folder) else {<br>  return;<br>};<br>for entry in read_dir.into_iter().flatten() {<br>  let Ok(file_type) = entry.file_type() else { continue };<br>  if file_type.is_file() {<br>    if is_valid(&amp;entry) {<br>      let Ok(metadata) = entry.metadata() else {<br>        continue;<br>      }<br>      // Process record<br>    }<br>  } else if file_type.is_dir() {<br>    if is_valid(&amp;entry) {<br>      let Ok(metadata) = entry.metadata() else {<br>        continue;<br>      }<br>      // Process record<br>    }<br>  }<br>}</pre><p>During profiling, I found a similar issue in the fontdb library, and changing the way is_file/is_dir is handled should reduce the font search time by around 12%. So, if you have the time, you can try fixing it here— <a href="https://github.com/RazrFalcon/fontdb/issues/60">https://github.com/RazrFalcon/fontdb/issues/60</a></p><ul><li><strong>Optimization of excluded elements</strong></li></ul><p>To verify whether a file can be used in a particular tool, it needs to pass several validations, such as checking if it’s not in an excluded folder, has the appropriate size, or if its name is not excluded.</p><p>The name exclusion is done using manually parsed wildcards like “/home/*/.cache”, which ignores paths like “/home/user/.cache/otherapp”. The issue became more noticeable when I started using more of them and due to other optimizations, as it became much more visible on the flame graph.</p><p>The problem was that the function responsible for checking whether a given name is excluded, allocated a string (conversion from PathBuf to String), a vector for the indexes used to traverse the path, and a vector for parts of the wildcard for each file and wildcard. Caching strings and eliminating unnecessary allocations significantly improved the performance of this step, which, alongside fetching file metadata from the disk, was the most time-consuming operation.</p><ul><li><strong>Speeding up the search for empty folders</strong></li></ul><p>If you’ve ever been curious about the mode I use most often, it’s searching for empty folders. Strange? Perhaps — but even stranger is that it worked very slowly, with delays of several seconds, visible in the GUI on the bar showing the number of checked folders when it didn’t update.</p><p>When searching for empty folders, all folders are initially considered as potentially empty. Then, when any non-folder element is found in a folder, it and all its ancestors are marked as non-empty folders. This prevents situations where, after deleting an empty folder from a directory, another folder becomes empty, requiring multiple scans to remove all empty folders.</p><p>I&#39;ve always thought that the issue was due to having folders on the disk with thousands of files directly inside them. Imagine my surprise when I saw what the hotspot revealed (by the way, a great tool -<a href="https://github.com/KDAB/hotspot">https://github.com/KDAB/hotspot</a>)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bE0d4vAPgmd3Jj-Ao5GozQ.png" /><figcaption>Flamegraph from searching for empty folders in Czkawka 6.1</figcaption></figure><p>As can be seen, the code responsible for reading metadata entries from the disk is highly parallelized and, despite using ~25% of the instructions, is responsible for less than 10% of the total execution time.</p><p>The app spent most of its time in the std::path::prepare_components function, which of course was not called directly anywhere, so I had no idea where it came from.</p><p>Eventually I got to this function, which, however, doesn’t look like it does any comparisons on paths:</p><pre>fn set_as_not_empty_folder(folder_entries: &amp;mut BTreeMap&lt;PathBuf, FolderEntry&gt;, current_folder: &amp;Path) {<br>    let mut d = folder_entries.get_mut(current_folder).unwrap();<br>    d.is_empty = FolderEmptiness::No;<br>    // Loop to recursively set as non empty this and all parent folders<br>    loop {<br>        d.is_empty = FolderEmptiness::No;<br>        if d.parent_path.is_some() {<br>            let cf = d.parent_path.clone().unwrap();<br>            d = folder_entries.get_mut(&amp;cf).unwrap();<br>        } else {<br>            break;<br>        }<br>    }<br>}</pre><p>However, probably more experienced programmers know that, BTreeMap works internally by comparing elements with each other, so a single get/get_mut can compare elements up to a dozen times before it finds the particular one. So the mystery why there is visible comparing is explained, and only remains to find the reason why it works so slow. So let’s look at the compare function</p><pre>impl PartialEq for PathBuf {<br>    #[inline]<br>    fn eq(&amp;self, other: &amp;PathBuf) -&gt; bool {<br>        self.components() == other.components()<br>    }<br>}</pre><p>so the components are compared, and what are they and how are they compared?</p><pre>pub fn components(&amp;self) -&gt; Components&lt;&#39;_&gt; {<br>    let prefix = parse_prefix(self.as_os_str());<br>    Components {<br>        path: self.as_u8_slice(),<br>        prefix,<br>        has_physical_root: has_physical_root(self.as_u8_slice(), prefix)<br>            || has_redox_scheme(self.as_u8_slice()),<br>        front: State::Prefix,<br>        back: State::Body,<br>    }<br>}</pre><pre>impl&lt;&#39;a&gt; PartialEq for Components&lt;&#39;a&gt; {<br>    #[inline]<br>    fn eq(&amp;self, other: &amp;Components&lt;&#39;a&gt;) -&gt; bool {<br>        let Components { path: _, front: _, back: _, has_physical_root: _, prefix: _ } = self;<br><br>        // Fast path for exact matches, e.g. for hashmap lookups.<br>        // Don&#39;t explicitly compare the prefix or has_physical_root fields since they&#39;ll<br>        // either be covered by the `path` buffer or are only relevant for `prefix_verbatim()`.<br>        if self.path.len() == other.path.len()<br>            &amp;&amp; self.front == other.front<br>            &amp;&amp; self.back == State::Body<br>            &amp;&amp; other.back == State::Body<br>            &amp;&amp; self.prefix_verbatim() == other.prefix_verbatim()<br>        {<br>            // possible future improvement: this could bail out earlier if there were a<br>            // reverse memcmp/bcmp comparing back to front<br>            if self.path == other.path {<br>                return true;<br>            }<br>        }<br><br>        // compare back to front since absolute paths often share long prefixes<br>        Iterator::eq(self.clone().rev(), other.clone().rev())<br>    }<br>}impl&lt;&#39;a&gt; PartialEq for Components&lt;&#39;a&gt; {<br>    #[inline]<br>    fn eq(&amp;self, other: &amp;Components&lt;&#39;a&gt;) -&gt; bool {<br>        let Components { path: _, front: _, back: _, has_physical_root: _, prefix: _ } = self;</pre><p>It’s a lot of code and comparisons, right? It turned out that I was using the sorting provided by the binary tree only for displaying results in the CLI. So, in favor of manual sorting, I changed the BTreeMap type to HashMap and, just in case, replaced PathBuf with String. And now the function looks like this:</p><pre>pub(crate) fn set_as_not_empty_folder(folder_entries: &amp;mut HashMap&lt;String, FolderEntry&gt;, current_folder: &amp;str) {<br>    let mut d = folder_entries.get_mut(current_folder).unwrap();<br>    if d.is_empty == FolderEmptiness::No {<br>        return; // Already set as non empty by one of his child<br>    }<br><br>    // Loop to recursively set as non empty this and all his parent folders<br>    loop {<br>        d.is_empty = FolderEmptiness::No;<br>        if d.parent_path.is_some() {<br>            let cf = d.parent_path.clone().unwrap();<br>            d = folder_entries.get_mut(&amp;cf).unwrap();<br>            if d.is_empty == FolderEmptiness::No {<br>                break; // Already set as non empty, so one of child already set it to non empty<br>            }<br>        } else {<br>            break;<br>        }<br>    }<br>}</pre><p>What about performance? Previously, scanning 3 million files and folders, with filesystem caching (done automatically when scanning multiple times), took 48 seconds. Now, it takes only 1.75 seconds.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1k37dotfWNrkApuLMqaEbQ.png" /><figcaption>Flamegraph from searching for empty folders in Czkawka 7.0</figcaption></figure><ul><li><strong>Speeding up cache loading/saving</strong></li></ul><p>To avoid unnecessary conversions, the object where file information like name and size were read, was identical for each mode. The issue was that it also contained information about the hash or symlink, even though they were not used anywhere outside the duplicate or symlink modes.</p><pre>pub struct FileEntry {<br>    pub path: PathBuf,<br>    pub size: u64,<br>    pub modified_date: u64,<br>    pub hash: String,<br>    pub symlink_info: Option&lt;SymlinkInfo&gt;,<br>}</pre><p>Therefore, I concluded that it’s better to create basic structures and then convert them into more advanced types. This may put a bit of a burden on the CPU, but I believe the compiler will do its best to optimize it. Thanks to this, almost every mode now uses generic file searching, and the FileEntry structure has decreased from 96 bytes to 40 bytes (and it was usually created thousands/millions of times).</p><pre>pub struct FileEntry {<br>    pub path: PathBuf,<br>    pub size: u64,<br>    pub modified_date: u64,<br>}</pre><p>Unfortunately, I didn’t do any benchmarks, but it should help and better utilize the CPU cache — I based on this <a href="https://youtu.be/2EWejmkKlxs">https://youtu.be/2EWejmkKlxs</a></p><p>Also, the size of the cache files on disk has been reduced slightly by deleting some fields, so unfortunately, the files will have to be scanned again to populate the cache.</p><ul><li>Micro-optimisations</li></ul><p>In addition to major changes, while reviewing the code, I made various small optimizations where I thought appropriate. In one place, I removed an if statement, and elsewhere, unnecessary clones(probably some of these changes the compiler would have done automatically, but as you can see in <a href="https://youtu.be/V6ug3e3jC54">https://youtu.be/V6ug3e3jC54</a> not always this is automatically done).</p><p>In the hotspot, I noticed that the function check_if_entry_have_valid_extension, was using a bit more cpu than I thought is necessary and it initially looked like this</p><pre>let Some(extension) = entry_data.path().extension() else {return false};<br>let extension_str = extension.to_string_lossy();<br>// Logic to check if extensions_str is excluded/allowed</pre><p>Having in mind that PathBuf consists of several elements, I checked what code the `path()` method contains. To my surprise, with each invocation, it performs the concatenation of the directory with the file name, which is visible in the flame graph with thousands of elements. Until then, I thought it was a completely free function, simply returning an element that exists in memory.</p><pre>pub fn path(&amp;self) -&gt; PathBuf {<br>    self.dir.root.join(self.file_name_os_str())<br>}</pre><p>So, my goal became to avoid using it wherever possible. In this case, it was possible to make it faster, at the cost of introducing a somewhat less elegant solution — the benchmark I used showed a 5x performance improvement.</p><pre>let file_name = entry_data.file_name();<br>let Some(file_name_str) = file_name.to_str() else { return false };<br>let Some(extension_idx) = file_name_str.rfind(&#39;.&#39;) else { return false };<br>let extension = &amp;file_name_str[extension_idx + 1..];<br>// Logic to check if extensions_str is excluded/allowed</pre><p>As you can see, sometimes you need to be flexible and not assume that just using Rust will automatically fix performance issues or make it as fast as possible (though usually even sub-optimal Rust code will be orders of magnitude faster than similar code in Python).</p><h3>Other changes</h3><p>The new version also brings a number of minor changes:</p><ul><li>Support for dragging folders for scanning in Czkawka — unfortunately, Slint does not yet support this feature.</li><li>Generating (almost) fully static czkawka_cli files on Linux — no dynamic linking to libc.</li><li>Predefined stack size for threads — this should help when using musl, which by default used ridiculously small values (or maybe I used ridiculously large stack sizes, but to my justification, I couldn’t find any place with it). It used to crash in random places (mainly it was used by the Docker image <a href="https://github.com/jlesage/docker-czkawka">https://github.com/jlesage/docker-czkawka</a>)</li><li>Generalization of parts of the code — mainly in the file search area, but changes also affected CLI arguments and handling the file progress processing.</li><li>Adding a progress bar in the CLI — ugly and inconsistent, but still a good starting point.</li><li>Handling excluded file extensions — previously, you could only choose allowed extensions.</li><li>Compilation with Link-Time Optimization (LTO) — Files built in CI are now compiled with fat LTO, which typically results in a 25–50% reduction in file size and a 5–10% increase in performance.</li></ul><h3>Etymology of the name</h3><p>First Czkawka and Szyszka, and now Krokiet. Stupid names, misleading and not associated with anything specific, so why do I give them?</p><p>From the very beginning of the project, there have been quite a few voices suggesting that the application’s name should be changed to something like “Another Duplicate Cleaner.”</p><p>I understand that entirely because it’s much easier to remember a name that is commonly used than one that isn’t even among the simplest words in its original language.</p><p>This application is my side project, which I create in my free time outside of work, and I don’t really want to impose any strict rules on it (very high test coverage, very detailed checking of PR/new elements, etc.) because I don’t have the time or energy for that, and it would also slow down the development of the application.</p><p>So, the name suggests that it’s not an application for companies, created for profit, but just a simple application made purely for the joy of creating.</p><h3>Ending words</h3><p>This project emerged as a clumsy attempt to revive fslint and to learn Rust with a useful example. After all these years and versions, I can confidently confirm that it worked. It was my first larger project, before that, I mostly wrote small scripts in Python and C++ for studies and personal use.</p><p>The one thing that continues to amaze me is how popular this app has become. Based on <a href="https://github.com/EvanLi/Github-Ranking/blob/master/Top100/Rust.md">https://github.com/EvanLi/Github-Ranking/blob/master/Top100/Rust.md</a>, is among the 100 most popular rust programs/libraries on github with more than 14,000 stars.</p><p>Price — free, MIT/GPL license (GPL — gui code in slint, MIT — everything else, so entire app is GPL) — no ads, internet connection or statistics collection</p><p>Repository - <a href="https://github.com/qarmin/czkawka">https://github.com/qarmin/czkawka</a><br>Files to download — <a href="https://github.com/qarmin/czkawka/releases">https://github.com/qarmin/czkawka/releases</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a465036e8788" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Czkawka 7.0]]></title>
            <link>https://medium.com/@qarmin/czkawka-7-0-4941b9bdba55?source=rss-9c8ac0ce6dcd------2</link>
            <guid isPermaLink="false">https://medium.com/p/4941b9bdba55</guid>
            <category><![CDATA[programowanie]]></category>
            <category><![CDATA[czkawka]]></category>
            <category><![CDATA[duplikaty]]></category>
            <category><![CDATA[rust]]></category>
            <category><![CDATA[czyszczenie-systemu]]></category>
            <dc:creator><![CDATA[Qarmin]]></dc:creator>
            <pubDate>Mon, 19 Feb 2024 11:46:05 GMT</pubDate>
            <atom:updated>2024-02-19T11:46:05.848Z</atom:updated>
            <content:encoded><![CDATA[<h3>Czkawka 7.0 — Krokiet, nowe gui w Slint, poprawki wydajności i garść nowych funkcji dla czyściciela danych.</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9DSJvNxaEPeg-NlEFUVIbQ.png" /><figcaption>Krokiet 7.0, nowe gui</figcaption></figure><p>Z nieskrywaną radością przedstawiam nową wersję 7.0 aplikacji <a href="https://github.com/qarmin/czkawka">Czkawka</a> — w pełni darmowego programu do wyszukiwania duplikatów, popsutych i tymczasowych plików, podobnych obrazów, wideo, pustych plików i folderów i kilku innych rzeczy.</p><p>W przeciągu 3 lat istnienia programu, wielokrotnie już myślałem o tym by przestać rozwijać aplikację, ponieważ zawiera ona prawie wszystko to co na co dzień używałem a dodawanie i naprawa tysięcy linii kodu, testowanie nowych funkcji, modyfikacja i weryfikacja CI czy rozmyślania nad implementacją szaty graficznej bywały mocno nużące przez co musiałem robić sobie przerwy by całkowicie nie wypalić się i nie porzucić projektu, ale za każdym razem natrafiałem na elementy, które fajnie byłoby jeszcze dodać/zmienić, więc chcąc nie chcąc, aplikacja ciągle jest rozwijana.</p><p>Jak zapewne widzieliście na <a href="https://github.com/qarmin/czkawka/issues">githubie</a>, ilość issue już dawno przekroczyła 300 i stale rośnie. Większość z nich dotyczy głównie nowych funkcji, które jedynie w niewielkiej części rozpatruję jako możliwe do implementacji, a jeszcze mniej implementuję. Nie bierze się to z mojego lenistwa(choć przyznaję, że jestem leniem), ale z tego, że nie widzę wystarczającej potrzeby użytkowników na implementację danej funkcji lub jej implementacja czy utrzymanie, wymagałoby zbyt dużego nakładu czasu — na początku istnienia tego projektu od razu bez większego przemyślenia zaimplementowałem dużą ilość nowych funkcji, przez co przez następne lata musiałem stopniowo poprawiać i generalizować kod.</p><p>Ta wersja jest prawdopodobnie największą z obecnych, a wszystko to przez:</p><h3>Nowe GUI</h3><p>Największą nowością w tej wersji, jest napisany kompletnie od zera nowy interfejs użytkownika “Krokiet”, który w przeciwieństwie do już istniejącego interfejsu, korzysta z biblioteki Slint zamiast GTK, która podobnie jak Czkawka, napisana jest w Rust.</p><p>W skrócie powodem zmiany był szereg błędów, trudność w pakowaniu, oraz konieczność dużej przebudowy aplikacji, gdyż bazowo niemal wszędzie korzysta ona z elementów tj. TreeView, które są przestarzałe i nie będą dostępne w GTK 5(<a href="https://docs.gtk.org/gtk4/class.TreeView.html">https://docs.gtk.org/gtk4/class.TreeView.html</a>).</p><p>Sprawdzałem wykorzystanie różnych bibliotek takich jak gtk, qt, tauri, slint, egui, iced, jednak ostateczny wybór padł na slint — pobieżny i subiektywny przegląd plusów i minusów jest dostępny tutaj — <a href="https://github.com/qarmin/czkawka/tree/master/krokiet#why-slint">https://github.com/qarmin/czkawka/tree/master/krokiet#why-slint</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7dPKpjjPxCM3CDp2KFN85w.png" /><figcaption>Po lewej Czkawka a po prawej Krokiet</figcaption></figure><p>Różnice na korzyść Krokieta(i Slint)</p><ul><li>Niemal w całości statyczne pliki binarne — GTK jest z zasady dynamicznie linkowany, przez co samo działanie aplikacji jest uzależnione od wersji GTK jak i wszystkich zależnych bibliotek w systemie. W przypadku Windowsa, z powodu braku instalatora gtk4, musiałem ręcznie dystrybuować wszystkie pliki dll wraz z aplikacją. Krokiet natomiast wypluwa pojedynczy plik, który jest wiele łatwiejszy do przenoszenia i użycia.</li><li>Działanie poza Linuxem — Gtk zostało stworzone głównie z myślą o Linuxie. Port na Windowsa i Maca jest potwornie zbugowany(chociaż na Linuxie problemy też występują). Losowe crashe, nie uruchamianie się, niepoprawne działanie — to tylko niektóre z problemów, zgłoszonych na githubie, których co gorsza bardzo często nie mogę ani zreprodukować ani w żaden sposób obejść.</li><li>Kompilacja pod Windowsem— Nie raz, nie dwa próbowałem skompilować program pod Windowsem, ale jak to z gtk bywa nie udało się. Z tego też powodu musiałem korzystać ze skomplikowanej metody cross-kompilacji z powodu dużej ilości dynamicznych bibliotekA. Slint korzysta niemal w całości z zależności rustowych, przez co jest bardzo łatwo i przyjemnie to kompilować.</li><li>Tworzenie GUI bez kompilacji — GTK 4 posiada Cambalache które jest świetne do tworzenia gui, jednak nie jest oficjalnym narzędziem, przez co jego autor tworzący program, nie musi podążać za standardem który GTK promuje/narzuca i też ta aplikacja nie ma takiego wsparcia społeczności na jakie zasługuje. Slint w przeciwieństwie do Cambalache nie posiada edytora drag&amp;drop, ale nadrabia to wbudowanym edytorem lsp, który wyświetla w czasie rzeczywistym interfejs oraz pozwala na wykonywanie funkcji i kodu w obrębie plików slint, co jest potwornie przydatne i ułatwia testowanie zmian, zwłaszcza, że kompilacja programu w slint jest dość długa gdyż z plików slint tworzy się plik rs — w przypadku Krokieta ma on po przeformatowaniu 150 tysięcy linii.</li><li>Bardziej rustowe api — w GTK z racji użycia TreeView, operowałem często na TreeIter, które są opakowaniami stworzonymi przez gtk-rs, zawierającymi pod spodem wskaźniki do modelu, których było bardzo łatwo źle używać — a nie po to używam rusta, by się użerać z błędami pamięci i korzystać z address sanitizera/valgrinda.</li></ul><p>Różnice na korzyść Czkawki(i GTK):</p><ul><li>Więcej funkcji — Czkawkę rozwijałem przez ponad 3 lata, więc to logiczne, że ilość kodu w niej napisana jest duża i nie wszystko byłem w stanie przepisać, zwłaszcza, że znaczna część kodu jest ściśle powiązana z GTK(głównie operacje na iteratorach w modelach) a dodatkowo Slint miejscami nie jest też wystarczająco nie posiada wystarczająco rozwiniętych odpowiedników dla widżetów z GTK.</li><li>Bardziej rozbudowane domyślne komponenty — Np. taki Listview, posiada intuicyjną logikę zaznaczania wielu elementów przy pomocy klawiatury a w Slint taką funkcjonalność trzeba implementować samemu. Również inne widżety w GTK mają o wiele więcej ustawień mogących zmieniać ich domyślne działanie, przez co nie trzeba wymyślać ciągle od nowa tych samych widżetów różniącymi się jedynie szczegółami.</li><li>Wielojęzyczność — W Czkawce korzystałem z biblioteki fluent by zapewnić użytkownikowi możliwość zmiany języka, co działało bardzo dobrze, było łatwe w kompilacji i miało bardzo prosty format klucz -&gt; wartość. Twórcy Slint, jednak w swoim języku ograniczyli możliwość stosowania zewnętrznych narzędzi do gettext. Mam jednak dwa problemy z tą metodą tłumaczenia. Po pierwsze, jest to narzędzie napisane w C, co utrudnia nieco kompilację i bardzo cross-kompilację. Po drugie format plików po do mnie nie przemawia, niby lepiej obrazują jakie frazy w jakim miejscu w kodzie są użyte, jednak dla mnie, do niewielkiego projektu nie jest to wcale potrzebne i komplikuje tłumaczenia.</li></ul><p>Jak to bywa w świecie open-source, to, że użyłem Slinta, przyniosło nie tylko korzyści mnie, ale również samemu Slintowi, m.in. przez:</p><ul><li>Wykrycie problemu zbyt długiego czasu wykonywania się skryptu build.rs, ostatecznie udało się to zbić z 8s do 2s(w średniej wielkości projekcie) — <a href="https://github.com/slint-ui/slint/discussions/4215">https://github.com/slint-ui/slint/discussions/4215</a></li><li>Problem wydajnościowy z funkcją ułatwiania dostępu — <a href="https://github.com/slint-ui/slint/issues/3867">https://github.com/slint-ui/slint/issues/3867</a></li><li>Różne zgłoszenia z wysypywaniem się aplikacji — <a href="https://github.com/slint-ui/slint/issues/4035">https://github.com/slint-ui/slint/issues/4035</a>, <a href="https://github.com/slint-ui/slint/issues/3700">https://github.com/slint-ui/slint/issues/3700</a></li><li>Wiele innych sugestii i zgłoszeń błędów</li></ul><p>Krokiet jak widać nie jest jeszcze dokończony i kilka elementów można w nim poprawić:</p><ul><li>Logo — ukazuje moje umiejętności graficzne w pełnej krasie, jednak zastanawiam się nieco nad czymś bardziej “oficjalnym”</li><li>Ikony — resztkami sił udało mi się zrobić 2 ikony, które również nie są dziełami sztuki — szukałem narzędzia AI do generacji ikon, ale nic nie znalazłem</li><li>Implementacja niektórych brakujących elementów z Czkawki</li><li>Pasek progresu przy usuwaniu/przenoszeniu plików</li><li>Poprawa rozmieszczenia niektórych elementów — mimo że gui jest już stworzone, to ciągle się zastanawiam, czy nie można usprawnić go by był łatwiejszy w użytku</li><li>Poruszanie się strzałkami po rezultatach</li><li>Zaznaczanie wielu rekordów przy pomocy myszki/klawiatury</li><li>Odwracanie zaznaczania środkowym przyciskiem myszy</li></ul><h4>Przyszłość Czkawki, wersja z Gui GTK</h4><p>No dobrze, ale pewnie się zastanawiacie, jaka przyszłość czeka obecny interfejs?</p><p>W zasadzie to nic spektakularnego się dziać nie będzie, po prostu ograniczam do minimum naprawę problemów i dodawanie w niej nowych funkcji. Z racji, że czkawka_core jest współdzielona przez wszystkie komponenty, to zmiany/poprawki w tej bibliotece, też w dużej części będą widoczne w tej wersji GUI, mimo że bezpośrednio nie będzie modyfikowana.</p><p>Czas życia aplikacji wyniesie pewnie z 10/15 lat, nim dystrybucje nie porzucą ze swoich repozytoriów pakietów Gtk 4, więc czasu na przesiadkę jest więcej niż dużo. Oczywiście istnieje szansa, że ktoś zechce zaktualizować wersję GTK w aplikacji, jednak będąc realistą wiem, że to prawie niemożliwe.</p><h3><strong>Poprawki wydajnościowe</strong></h3><p>W tej wersji oprócz zwyczajowych poprawek i ulepszeń, przeglądając kod świeżym okiem po pewnej przerwie, zauważyłem, że kilka elementów w projekcie nie jest tak wydajne jak mogłoby być, gdyż niektóre części programu nie były aktualizowane od dawna, gdy jeszcze skupiałem się na tym by program działał a nie na optymalizowaniu go.</p><ul><li><strong>Sięganie po metadane pliku tylko gdy to konieczne</strong></li></ul><p>Podczas wyszukiwania plików/folderów do skanowania, muszę sprawdzić czy dany element w folderze jest plikiem/folderem by go odpowiednio przyporządkować w zależności od trybu. <br>Poprzednio, pobierałem metadane plików przy każdej iteracji pętli i używałem na zmiennej typu Metadata, metod is_file/is_dir co skutkowało niepotrzebnym sięganiem ponownie po dane na dysku.</p><pre>let Ok(read_dir) = fs::read_dir(current_folder) else {<br>  return;<br>};<br>for entry in read_dir.into_iter().flatten() {<br>  let Ok(metadata) = entry.metadata() else {<br>    continue;<br>  }<br>  if metadata.is_file() {<br>    if is_valid(&amp;entry) {<br>      // Process record<br>    }<br>  } else if metadata.is_dir() {<br>    if is_valid(&amp;entry) {<br>      // Process record<br>    }<br>  }<br>}</pre><p>Przeglądając api typu DirEntry, okazało się, że posiada już informację o typie pliku, więc sięganie po metadane pliku, mogło zostać odłożone na później i wykonane na mniejszej liczbie plików, wstępnie przefiltrowanych przez inne, mniej czasochłonne metody.</p><pre>let Ok(read_dir) = fs::read_dir(current_folder) else {<br>  return;<br>};<br>for entry in read_dir.into_iter().flatten() {<br>  let Ok(file_type) = entry.file_type() else { continue };<br>  if file_type.is_file() {<br>    if is_valid(&amp;entry) {<br>      let Ok(metadata) = entry.metadata() else {<br>        continue;<br>      }<br>      // Process record<br>    }<br>  } else if file_type.is_dir() {<br>    if is_valid(&amp;entry) {<br>      let Ok(metadata) = entry.metadata() else {<br>        continue;<br>      }<br>      // Process record<br>    }<br>  }<br>}</pre><p>Podczas profilowania, podobny problem znalazłem w bibliotece fontdb i zmiana sposobu obsługi is_file/is_dir, powinna skrócić czas wyszukiwania czcionek o około 12%, więc jak macie czas i ochotę to możecie spróbować to naprawić tutaj — <a href="https://github.com/RazrFalcon/fontdb/issues/60">https://github.com/RazrFalcon/fontdb/issues/60</a></p><ul><li><strong>Optymalizacja wykluczonych elementów</strong></li></ul><p>By zweryfikować, czy plik może być użyty w danym narzędziu, musi przejść kilka walidacji np. czy nie jest w wykluczonym folderze, ma odpowiedni rozmiar lub czy jego nazwa nie jest wykluczona.</p><p>To wykluczanie po nazwie jest zrobione przy pomocy ręcznie parsowanych wildcardów w stylu “/home/*/.cache”, które ignorują ścieżki w stylu “/home/user/.cache/otherapp”. Problem stał się głównie widoczny, gdy zacząłem korzystać z ich większej ilości oraz z powodu innych optymalizacji, ponieważ stawał się cbył dużo bardziej widoczny na flamegraphie.</p><p>Problemem było to że funkcja odpowiedzialna za sprawdzanie, czy dana nazwa jest wykluczona, dla każdego pliku oraz wildcarda alokowała stringa(konwersja Pathbuf -&gt; String), wektor dla indeksów używanych do przechodzenia po ścieżce oraz wektor dla części wildcarda. Cachowanie stringów i usunięcie zbędnych alokacji, poprawiło kilkukrotnie wydajność tego kroku, który obok sięgania po metadane plików na dysku był najbardziej czasochłonną operacją.</p><ul><li><strong>Przyspieszenie wyszukiwania pustych folderów</strong></li></ul><p>Jeśli kiedykolwiek was ciekawiło, jakiego trybu najczęściej używam, to właśnie wyszukiwania pustych folderów. Dziwne? Być może — ale dziwniejsze jest to że działało to bardzo wolno, nawet z kilkusekundami zwiechami, widocznymi w gui na pasku z liczbą sprawdzonych folderów, gdy ten się nie aktualizował.</p><p>Wyszukiwanie pustych folderów, działa tak, że na początku wszystkie foldery są określane jako może puste a następnie gdy w jakimkolwiek folderze znajdzie się element nie będący folderem, to zostaje on, jak i wszyscy jego rodzice określony jako nie pusty folder. Zapobiega to sytuacjom, w których po usunięciu pustego folderu z katalogu, ten drugi staje się pusty przez co trzebaby wielokrotnie uruchamiać skanowanie.</p><p>Przez cały czas myślałem że to kwestia tego, że na dysku mam foldery z tysiącami plików będącymi w nich bezpośrednio. Jakie było moje zdziwienie na widok tego co wypluł hotspot(btw, świetne narzędzie — <a href="https://github.com/KDAB/hotspot">https://github.com/KDAB/hotspot</a>)</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bE0d4vAPgmd3Jj-Ao5GozQ.png" /><figcaption>Flamegraph z wyszukiwania pustych folderów w Czkawce 6.1</figcaption></figure><p>Jak widać kod odpowiedzialny za poruszanie się po dysku jest wysoce zrównoleglony i mimo tego że stanowi ~25% wykonywanych instrukcji przez program, jest odpowiedzialny za mniej niż 10% czasu całego działania programu.</p><p>Większość czasu program spędzał w funkcji std::path::prepare_components, której oczywiście nigdzie nie wywoływałem bezpośrednio, więc nie miałem pojęcia skąd pochodzi.</p><p>W końcu dotarłem do tej funkcji, która jednak nie wygląda by robiła jakieś porównania na ścieżkach:</p><pre>fn set_as_not_empty_folder(folder_entries: &amp;mut BTreeMap&lt;PathBuf, FolderEntry&gt;, current_folder: &amp;Path) {<br>    let mut d = folder_entries.get_mut(current_folder).unwrap();<br>    d.is_empty = FolderEmptiness::No;<br>    // Loop to recursively set as non empty this and all parent folders<br>    loop {<br>        d.is_empty = FolderEmptiness::No;<br>        if d.parent_path.is_some() {<br>            let cf = d.parent_path.clone().unwrap();<br>            d = folder_entries.get_mut(&amp;cf).unwrap();<br>        } else {<br>            break;<br>        }<br>    }<br>}</pre><p>Jednak zapewne bardziej zaprawione w programowaniu osoby wiedzą, że przecież BTreeMap działa wewnętrznie przez porównywanie ze sobą elementów, więc jeden get/get_mut może nawet kilkanaście razy porównywać elementy nim znajdzie ten konkretny. Zatem zagadka dlaczego jest widoczne porównywanie jest wyjaśniona, tylko zostało znaleźć przyczynę dlaczego działa to tak potwornie wolno. Zobaczmy więc na funkcję porównywania</p><pre>impl PartialEq for PathBuf {<br>    #[inline]<br>    fn eq(&amp;self, other: &amp;PathBuf) -&gt; bool {<br>        self.components() == other.components()<br>    }<br>}</pre><p>więc porównywane są komponenty, a czym one są i jak się je porównuje?</p><pre>pub fn components(&amp;self) -&gt; Components&lt;&#39;_&gt; {<br>    let prefix = parse_prefix(self.as_os_str());<br>    Components {<br>        path: self.as_u8_slice(),<br>        prefix,<br>        has_physical_root: has_physical_root(self.as_u8_slice(), prefix)<br>            || has_redox_scheme(self.as_u8_slice()),<br>        front: State::Prefix,<br>        back: State::Body,<br>    }<br>}</pre><pre>impl&lt;&#39;a&gt; PartialEq for Components&lt;&#39;a&gt; {<br>    #[inline]<br>    fn eq(&amp;self, other: &amp;Components&lt;&#39;a&gt;) -&gt; bool {<br>        let Components { path: _, front: _, back: _, has_physical_root: _, prefix: _ } = self;<br><br>        // Fast path for exact matches, e.g. for hashmap lookups.<br>        // Don&#39;t explicitly compare the prefix or has_physical_root fields since they&#39;ll<br>        // either be covered by the `path` buffer or are only relevant for `prefix_verbatim()`.<br>        if self.path.len() == other.path.len()<br>            &amp;&amp; self.front == other.front<br>            &amp;&amp; self.back == State::Body<br>            &amp;&amp; other.back == State::Body<br>            &amp;&amp; self.prefix_verbatim() == other.prefix_verbatim()<br>        {<br>            // possible future improvement: this could bail out earlier if there were a<br>            // reverse memcmp/bcmp comparing back to front<br>            if self.path == other.path {<br>                return true;<br>            }<br>        }<br><br>        // compare back to front since absolute paths often share long prefixes<br>        Iterator::eq(self.clone().rev(), other.clone().rev())<br>    }<br>}</pre><p>toż to przecież masa kodu i porównań! Okazało, się że używałem sortowania oferowanego przez drzewo binarne tylko do wypisywania rezultatów w CLI, więc na rzecz ręcznego sortowania, zmieniłem typ BTreeMap na HashMap oraz na wszelki wypadek podmieniłem PathBuf na String i tak oto teraz się dana funkcja prezentuje:</p><pre>pub(crate) fn set_as_not_empty_folder(folder_entries: &amp;mut HashMap&lt;String, FolderEntry&gt;, current_folder: &amp;str) {<br>    let mut d = folder_entries.get_mut(current_folder).unwrap();<br>    if d.is_empty == FolderEmptiness::No {<br>        return; // Already set as non empty by one of his child<br>    }<br><br>    // Loop to recursively set as non empty this and all his parent folders<br>    loop {<br>        d.is_empty = FolderEmptiness::No;<br>        if d.parent_path.is_some() {<br>            let cf = d.parent_path.clone().unwrap();<br>            d = folder_entries.get_mut(&amp;cf).unwrap();<br>            if d.is_empty == FolderEmptiness::No {<br>                break; // Already set as non empty, so one of child already set it to non empty<br>            }<br>        } else {<br>            break;<br>        }<br>    }<br>}</pre><p>A co z wydajnością? Wcześniej 3 miliony plików i folderów, przy cachowaniu systemu plików(system automatycznie to robi jak się skanuje wielokrotnie) skanowały się 48s, teraz 1.75s</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*1k37dotfWNrkApuLMqaEbQ.png" /><figcaption>Flamegraph z wyszukiwania pustych folderów w Czkawce 7.0</figcaption></figure><ul><li><strong>Przyspieszenie ładowania/zapisywania cache</strong></li></ul><p>By uniknąć niepotrzebnych konwersji, obiekt do którego były zczytywane informacje o pliku typu nazwa, rozmiar był dla każdego z trybów identyczny. Problematyczne było to że były tam też informacje o hashu czy symlinka, mimo że nie były nigdzie używane poza trybem z duplikatami czy symlikami.</p><pre>pub struct FileEntry {<br>    pub path: PathBuf,<br>    pub size: u64,<br>    pub modified_date: u64,<br>    pub hash: String,<br>    pub symlink_info: Option&lt;SymlinkInfo&gt;,<br>}</pre><p>Uznałem więc że lepiej jest to prostu tworzyć bazowe struktury a potem je konwertować do bardziej zaawansowanych typów co pewnie nieco obciąży CPU, ale wierzę że kompilator zrobi wszystko co możliwe by to jak najlepiej zoptymalizować. Dzięki temu, teraz niemal każdy tryb korzysta z generycznego wyszukiwania plików i struktura FileEntry zmniejszyła się z 96 bajtów do 40 bajtów(a zwykle ona była tworzona tysiące/miliony razy).</p><pre>pub struct FileEntry {<br>    pub path: PathBuf,<br>    pub size: u64,<br>    pub modified_date: u64,<br>}</pre><p>Niestety nie robiłem benchmarków, ale powinno to pomóc i lepiej wypełniać cache cpu — bazowałem na <a href="https://youtu.be/2EWejmkKlxs">https://youtu.be/2EWejmkKlxs</a></p><p>Również rozmiar plików cache z rezultatami na dysku został zmniejszony nieznacznie przez usunięcie niektórych pól, przez co niestety trzeba będzie jeszcze raz przeskanować pliki by ponownie zapełnić cache.</p><ul><li><strong>Mikro-optymalizacje</strong></li></ul><p>Obok większych zmian, przeglądając kod robiłem tam gdzie uważałem za słuszne różne małe optymalizacje. W jednym miejscu usunąłem ifa, a gdzieś indziej niepotrzebne clone(zapewne część z tych zmian, kompilator sam by zrobił, jednak jak widać po <a href="https://youtu.be/V6ug3e3jC54">https://youtu.be/V6ug3e3jC54</a> nie zawsze mu to wychodzi).</p><p>W hotspot, zauważyłem, że funkcja check_if_entry_have_valid_extension, zużywa trochę więcej cpu, niż sądziłem że to konieczne i wstępnie ona wyglądała w taki sposób</p><pre>let Some(extension) = entry_data.path().extension() else {return false};<br>let extension_str = extension.to_string_lossy();<br>// Logic to check if extensions_str is excluded/allowed</pre><p>Mając w pamięci to, że PathBuf składa się z kilku elementów, coś mnie tknęło i sprawdziłem co zawiera metoda `path()` i ku mojemu zdziwieniu, przy każdym jej wywołaniu, wykonuje ona łączenie katalogu z nazwą pliku, co przy tysiącach elementów jest widoczne w flamegraph — do tamtej pory myślałem, że jest to funkcja w pełni darmowa, po prostu zwracająca gdzieś zapisany w pamięci element.</p><pre>pub fn path(&amp;self) -&gt; PathBuf {<br>    self.dir.root.join(self.file_name_os_str())<br>}</pre><p>Zatem moim celem stało się unikanie jej tam gdzie tylko to możliwe. W tym przypadku było to możliwe by zrobić to szybciej, kosztem wprowadzenia nieco brzydszego rozwiązania — benchmark, którym to testowałem pokazał 5-krotny wzrost wydajności.</p><pre>let file_name = entry_data.file_name();<br>let Some(file_name_str) = file_name.to_str() else { return false };<br>let Some(extension_idx) = file_name_str.rfind(&#39;.&#39;) else { return false };<br>let extension = &amp;file_name_str[extension_idx + 1..];<br>// Logic to check if extensions_str is excluded/allowed</pre><p>Jak widać, czasami trzeba być elastycznym i nie popadać w przekonanie że samo używanie Rusta z miejsca naprawi problemy wydajnościowe/zrobi to tak szybkie jak tylko to możliwe(choć zwykle pewnie nawet nieoptymalny kod, będzie rząd wielkości szybszy od podobnego kodu w Pythonie)</p><h3>Inne Zmiany</h3><p>Nowa wersja przynosi również szereg pomniejszych zmian:</p><ul><li>Wsparcie dla przeciągania folderów do skanowania w Czkawce — niestety Slint nie wspiera jeszcze tej funkcji.</li><li>Generowanie (niemal) w pełni statycznych plików czkawka_cli na Linuxie— czyli brak linkowania do np. libc</li><li>Z góry określony stack size dla wątków — powinno to pomóc przy używaniu musl, który domyślnie używał absurdalnie małych wartości(albo ja używałem absurdalnie dużo miejsca na stosie, ale na swoje usprawiedliwienie dodam, że owego miejsca nie znalazłem) i wysypywał się w losowych miejscach(głównie to było używane przez obraz dockerowy — <a href="https://github.com/jlesage/docker-czkawka">https://github.com/jlesage/docker-czkawka</a>)</li><li>Generalizacja części kodu —głównie w obszarze wyszukiwania plików, ale zmian nie uniknęły również argumenty CLI jak i obsługa wyświetlania progresu przetwarzania plików.</li><li>Dodanie paska progresu w CLI — brzydki i niespójny, ale zawsze to dobry punkt startowy</li><li>Obsługa wykluczonych rozszerzeń — poprzednio można było wybrać jedynie dozwolone rozszerzenia</li><li>Kompilacja plików z LTO — Budowane pliki w CI, są kompilowane teraz z fat LTO co daje zwykle od 25/50% mniejszy rozmiar plików i 5/10% wzrost wydajności.</li></ul><h3>Etymologia nazwy</h3><p>Najpierw Czkawka i Szyszka a teraz Krokiet. Nazwy głupie, wprowadzające w błąd i nie kojarzące się z niczym szczególnym, po co je więc nadaję?</p><p>Od samego początku projektu jest dość wiele głosów, że nazwa aplikacji powinna być zmieniona na coś w stylu Another Duplicate Cleaner.</p><p>W całości to rozumiem, bo o wiele łatwiej jest zapamiętać nazwę, która jest powszechnie stosowana, niż taką która nawet w swoim pierwotnym języku nie należy do najprostszych słów.</p><p>Ta aplikacja jest moim pobocznym projektem, który tworzę w wolnym czasie od pracy i nie mam zbytnio ochoty trzymać w niej specjalnego rygoru (bardzo duży coverage testów, bardzo szczegółowe sprawdzanie nowych elementów etc.), gdyż nie mam na to czasu i sił a dodatkowo spowolniłoby to rozwój aplikacji.</p><p>Zatem nazwa ukazuje, że to nie jest aplikacja dla firm, tworzona dla zysku, tylko najzwyklejsza aplikacja stworzona dla samej przyjemności tworzenia.</p><h3>Słowa końcowe</h3><p>Ten projekt powstał jako niezdarna chęć przywrócenia do życia fslint jak i nauki rusta na jakimś pożytecznym przykładzie, więc po tych wszystkich latach i wersjach, mogę z całą stanowczością potwierdzić że to się udało(i to jeszcze z zadatkiem) — był to mój pierwszy większy projekt, wcześniej jedynie pisałem raczej niewielkie skrypty w Pythonie i C++ na studia i własny użytek.</p><p>Jedyne co mnie ciągle zadziwia, jest to, jak bardzo popularną stała się ta aplikacja. Bazując na <a href="https://github.com/EvanLi/Github-Ranking/blob/master/Top100/Rust.md">https://github.com/EvanLi/Github-Ranking/blob/master/Top100/Rust.md</a>, znajduje się w gronie 100 najpopularniejszych rustowych programów/bibliotek na githubie z ponad 14000 gwiazdkami.</p><p>Cena — za darmo, licencja MIT/GPL(GPL — kod gui w slint, MIT — cała reszta) — brak reklam, łączenia z internetem czy zbierania statystyk<br>Repozytorium — <a href="https://github.com/qarmin/czkawka">https://github.com/qarmin/czkawka</a><br>Pliki do pobrania — <a href="https://github.com/qarmin/czkawka/releases">https://github.com/qarmin/czkawka/releases</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4941b9bdba55" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>