Skip to content

Commit c580010

Browse files
committed
Fix icon overlays, Unicode context menus, and localization gaps
Icon overlays (TortoiseSVN, TortoiseGit, and any other provider that supplies its icon as a .ico file) never appeared. The loader asked ExtractIcons for two icons in a single call with both sizes packed into one argument, which requests two consecutive icon resources rather than one icon at two sizes. A standalone .ico holds exactly one, so the second size came back empty and the handler was discarded, because an overlay is only used when every size loads. (#90) Note that Windows itself stops loading overlay handlers once roughly fifteen are registered. On a machine with many of them, Explorer may show no overlays in the same folder where Sally now does. Right-clicking a file whose name contains characters outside the system ANSI code page produced no context menu and no error at all. Menu entries are now built from the wide filename, the containing folder is bound without narrowing its path, and a name that cannot be resolved is skipped instead of discarding the whole selection. (#79) Files inside a folder whose own path contains such characters could not be opened with Enter -- including files with plain ASCII names, because the code path was chosen from the filename alone. Adds Help > Report a Bug, which writes a diagnostic report describing configuration and shell integration state, including what happened to each registered icon overlay handler and the outcome of recent context menu commands. Unlike the existing Task List > Break route it needs only one running instance and does not terminate the process. Completes localization for all ten shipped languages. Strings introduced by earlier releases -- the Light, Dark and System theme names, the Find results save and load commands, the Paste Shortcut failure message, and the Windows Terminal and default shell integration -- were still shown in English in every locale. Development builds carry the correct version again. It was pinned by cached build configuration, so binaries reported the previous release. Credits the people who report problems, test pre-release builds, and confirm fixes in AUTHORS.
1 parent 2301b43 commit c580010

32 files changed

Lines changed: 1740 additions & 372 deletions

AUTHORS

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,74 @@
1-
== Altap Salamander Fork
1+
== Sally Fork
2+
3+
Project lead and principal developer
24

35
Elias Bachaalany (0xeb), 2025-2026
4-
- Unicode/wide string support
5-
- Long path support (32K paths)
6-
- Dark mode
7-
- Windows Terminal integration and shell/overlay integration
8-
- WebView2-based Web Viewer and viewer modernization
9-
- Plugin packaging and bundled plugin set
10-
- Native ARM64 and Wine compatibility
11-
- Extended translations and localization pipeline
12-
- Code modernization and normalization
13-
- CMake build system and release pipeline
6+
- Project direction, architecture, maintenance, and releases
7+
- Unicode/UTF-16 filenames and 32K long-path support across panels, file
8+
operations, Find, viewers, editors, drag/drop, clipboard, history, and
9+
network/UNC paths
10+
- Safer destructive operations and Find-result validation, including
11+
protection against lossy ANSI fallbacks
12+
- Light, Dark, and System themes across the core UI and bundled plugins
13+
- Windows Terminal profile discovery, default-shell selection, shell
14+
extension maintenance, and expanded icon-overlay support
15+
- WebView2-based Web Viewer, Markdown rendering, Unicode text decoding, and
16+
viewer lifecycle and navigation fixes
17+
- Bundled archive, filesystem, viewer, comparison, checksum, automation, and
18+
system plugins, with dependency handling and automatic plugin discovery
19+
- Native x64, x86, and ARM64 packages and Wine compatibility
20+
- Maintained language packs, Translator workspaces, and a reproducible
21+
localization pipeline
22+
- Modern CMake builds for Sally, plugins, tools, language files, and the shell
23+
extension, including MSVC and clang-cl/xwin toolchains
24+
- GitHub release automation, update checks, runtime and symbol packages, and
25+
release artifact validation
26+
- Testable OS-abstraction boundaries, crash diagnostics, source cleanup, and
27+
Czech-to-English code-comment modernization
28+
29+
Translation contributors
30+
31+
bellus869, 2025-2026
32+
- Czech-to-English source-comment translation across the Sally core
33+
34+
Lemi257, 2025
35+
- Czech-to-English source-comment translation across dialogs, viewers,
36+
worker operations, and ZIP support
37+
38+
Acknowledgements
39+
40+
Sally is shaped by the people who take the time to report problems, test
41+
pre-release builds, and confirm fixes. Thank you to:
42+
43+
Adamaji (Adamaji74)
44+
ags1234
45+
Akrotkiv
46+
Martin Dařílek (darilek)
47+
Eduardo G. (xmiseggs)
48+
KubaDee
49+
LiquidatorFX
50+
Paro The Parrot (ParoTheParrot)
51+
Robert Pösel (Robyer)
52+
radek-sedlacik-LL
53+
SelfMan
54+
Patrik Švestka (tukanos)
55+
Thierry Lambert (thyresias)
56+
tormento
57+
Vector Rilke (vectorrilke)
58+
59+
Issues carried over from Open Salamander were originally reported by:
60+
61+
Lukáš Cerman (lcerman)
62+
emseecz
63+
Ralf Hahnloser (HahnloserR)
64+
mbeilste
65+
Melloware (melloware)
66+
Jan Ryšavý (janrysavy)
67+
vygi
68+
69+
Bug reports, testing, translations, and focused patches from the wider Sally
70+
community have also shaped the project. Individual issue and commit history is
71+
preserved on GitHub.
1472

1573
== Original Authors
1674

cmake/git_version.cmake

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,44 @@
44

55
find_package(Git QUIET)
66

7-
set(SALLY_LOCAL_DEV_VERSION "1.0.23" CACHE STRING "Sally local development version base")
7+
# Sally local development version base.
8+
#
9+
# Deliberately NOT a CACHE entry. `set(... CACHE ...)` only applies when the entry does
10+
# not already exist, so once a build directory had been configured, bumping this line did
11+
# nothing there - the stale cached value won and every build was stamped with the previous
12+
# release's version. That is silent and survives rebuilds, so it is only noticed when
13+
# someone reads the About box.
14+
#
15+
# As a normal variable it is re-evaluated on every configure, while an explicit
16+
# -DSALLY_LOCAL_DEV_VERSION=x.y.z on the command line still wins because that defines the
17+
# variable before this file runs.
18+
# Sally local development version base. THIS LINE IS THE SINGLE SOURCE OF TRUTH.
19+
#
20+
# Keep it AHEAD of the newest released tag: v1.0.23 is published, so leaving it at 1.0.23
21+
# would stamp unreleased builds with a version already shipped.
22+
#
23+
# It is deliberately neither a CACHE entry nor merely `if(NOT DEFINED)`-guarded, because
24+
# both let a value cached by an EARLIER configure win, and that has now shipped wrong
25+
# versions twice:
26+
#
27+
# * `set(... CACHE STRING ...)` is skipped entirely when the entry already exists, so a
28+
# build directory configured before a bump kept emitting the old version forever.
29+
# * `if(NOT DEFINED ...)` is satisfied by a stale cache entry just as well as by a real
30+
# -D, so sibling build trees (the private test suite configures Sally sub-builds of its
31+
# own) silently kept their old value.
32+
#
33+
# The second one bites harder than it looks, because configure_file below writes into the
34+
# SOURCE tree: every build directory shares one src/git_version.h, so whichever configured
35+
# last decides the version for all of them. Making this line unconditional means they all
36+
# agree, and the shared header stops being a race.
37+
#
38+
# If you genuinely need a different value, pass -DSALLY_VERSION_OVERRIDE=x.y.z. That is a
39+
# distinct name, so it cannot be confused with a leftover cache entry.
40+
if(SALLY_VERSION_OVERRIDE)
41+
set(SALLY_LOCAL_DEV_VERSION "${SALLY_VERSION_OVERRIDE}")
42+
else()
43+
set(SALLY_LOCAL_DEV_VERSION "1.0.24")
44+
endif()
845

946
if(GIT_FOUND AND EXISTS "${CMAKE_SOURCE_DIR}/.git")
1047
# Get the latest tag

cmake/targets/sally_sources.cmake

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,12 @@ set(SALLY_SOURCES
160160
"${SAL_SRC}/shares.cpp"
161161
"${SAL_SRC}/shellib.cpp"
162162
"${SAL_SRC}/shellsup.cpp"
163+
"${SAL_SRC}/shellsup_diag.cpp"
163164
"${SAL_SRC}/shexreg.c"
164165
"${SAL_SRC}/shexreg_ipc_names.c"
165166
"${SAL_SRC}/shiconov.cpp"
167+
"${SAL_SRC}/shiconov_diag.cpp"
168+
"${SAL_SRC}/shiconov_icons.cpp"
166169
"${SAL_SRC}/snooper.cpp"
167170
"${SAL_SRC}/sort.cpp"
168171
"${SAL_SRC}/stswnd.cpp"

src/bugreprt.cpp

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
#include <tlhelp32.h>
88
#include "common/IEnvironment.h"
99
#include "common/IRegistry.h"
10+
#include "shiconov_diag.h"
11+
#include "shellsup_diag.h"
1012

1113
// Cross-architecture register access macros for CONTEXT structure
1214
#ifdef _WIN64
@@ -555,6 +557,10 @@ void CCallStack::PrintBugReport(EXCEPTION_POINTERS* Exception, DWORD ThreadID, D
555557
static char num[50];
556558
static char avbuf[MAX_PATH];
557559
static char nameBuf[MAX_PATH];
560+
// Scratch for the shell-integration sections. Static like the rest: this runs inside a
561+
// process that may already be crashing, so we do not want it on the stack. Sized for a
562+
// \uXXXX-escaped long path plus the surrounding fields.
563+
static char BugReportDiagBuf[4096];
558564

559565
strcpy(buf, SALAMANDER_TEXT_VERSION);
560566
#ifdef _DEBUG
@@ -1478,6 +1484,77 @@ void CCallStack::PrintBugReport(EXCEPTION_POINTERS* Exception, DWORD ThreadID, D
14781484
PrintLine(param, "", FALSE);
14791485
}
14801486

1487+
// Shell integration: what actually happened to each registered icon-overlay handler,
1488+
// and to the last few context-menu interactions. These two sections exist because the
1489+
// TRACE_* calls in shiconov.cpp and shellsup.cpp are compiled out of Release, so this
1490+
// report is the only channel that reaches a reporter running a shipped build.
1491+
__try
1492+
{
1493+
PrintLine(param, "Icon Overlays:", FALSE);
1494+
1495+
_snprintf_s(buf, _TRUNCATE, "Config root in use: %s", ShellOverlayDiag.Header.ConfigRoot);
1496+
PrintLine(param, buf, TRUE);
1497+
_snprintf_s(buf, _TRUNCATE, "ANSI code page: %u Registered: %d Loaded: %d",
1498+
ShellOverlayDiag.Header.AnsiCodePage,
1499+
ShellOverlayDiag.Header.Registered, ShellOverlayDiag.Header.Loaded);
1500+
PrintLine(param, buf, TRUE);
1501+
_snprintf_s(buf, _TRUNCATE, "System DPI: %u Required icon sizes: %d/%d/%d",
1502+
ShellOverlayDiag.Header.SystemDpi, ShellOverlayDiag.Header.IconSizes[0],
1503+
ShellOverlayDiag.Header.IconSizes[1], ShellOverlayDiag.Header.IconSizes[2]);
1504+
PrintLine(param, buf, TRUE);
1505+
1506+
for (int i = 0; i < ShellOverlayDiag.Count(); i++)
1507+
{
1508+
const ShellOverlayDiagRecord* rec = ShellOverlayDiag.At(i);
1509+
if (rec == NULL)
1510+
continue;
1511+
BugReportDiagBuf[0] = 0;
1512+
FormatShellOverlayDiagRecord(*rec, BugReportDiagBuf, (int)sizeof(BugReportDiagBuf));
1513+
_snprintf_s(buf, _TRUNCATE, "[%2d] %s", i, BugReportDiagBuf);
1514+
PrintLine(param, buf, TRUE);
1515+
}
1516+
PrintLine(param, "", FALSE);
1517+
}
1518+
__except (EXCEPTION_EXECUTE_HANDLER)
1519+
{
1520+
sprintf(buf, "some exception has occured...");
1521+
PrintLine(param, buf, TRUE);
1522+
PrintLine(param, "", FALSE);
1523+
}
1524+
1525+
__try
1526+
{
1527+
PrintLine(param, "Shell Context Menu (oldest first):", FALSE);
1528+
for (int i = 0; i < ShellMenuDiag.Count(); i++)
1529+
{
1530+
const ShellMenuDiagRecord* rec = ShellMenuDiag.At(i);
1531+
if (rec == NULL)
1532+
continue;
1533+
BugReportDiagBuf[0] = 0;
1534+
FormatShellMenuDiagRecord(*rec, BugReportDiagBuf, (int)sizeof(BugReportDiagBuf));
1535+
1536+
// The formatter emits several lines; PrintLine takes one at a time.
1537+
char* line = BugReportDiagBuf;
1538+
while (*line != 0)
1539+
{
1540+
char* eol = strchr(line, '\n');
1541+
if (eol != NULL)
1542+
*eol = 0;
1543+
PrintLine(param, line, TRUE);
1544+
if (eol == NULL)
1545+
break;
1546+
line = eol + 1;
1547+
}
1548+
}
1549+
PrintLine(param, "", FALSE);
1550+
}
1551+
__except (EXCEPTION_EXECUTE_HANDLER)
1552+
{
1553+
sprintf(buf, "some exception has occured...");
1554+
PrintLine(param, buf, TRUE);
1555+
PrintLine(param, "", FALSE);
1556+
}
1557+
14811558
__try
14821559
{
14831560
PrintLine(param, "Panels:", FALSE);

src/consts.h

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -822,7 +822,10 @@ void SetClipCutCopyInfo(HWND hwnd, BOOL copy, BOOL salObject);
822822
void ShellAction(CFilesWindow* panel, CShellAction action, BOOL useSelection = TRUE,
823823
BOOL posByMouse = TRUE, BOOL onlyPanelMenu = FALSE);
824824
void ExecuteAssociation(HWND hWindow, const char* path, const char* name);
825-
void ExecuteAssociationW(HWND hWindow, const char* path, const wchar_t* nameW);
825+
// Wide sibling of ExecuteAssociation. BOTH the directory and the name are wide: a folder
826+
// whose own path is outside the ANSI code page must reach the shell intact even when the
827+
// file inside it has a perfectly ordinary ASCII name.
828+
void ExecuteAssociationW(HWND hWindow, const wchar_t* pathW, const wchar_t* nameW);
826829

827830
BOOL CanUseShellExecuteWndAsParent(const char* cmdName);
828831

@@ -2384,8 +2387,11 @@ struct CTmpEnumData
23842387

23852388
const char* EnumFileNames(int index, void* param);
23862389

2387-
void ShellActionAux5(UINT flags, CFilesWindow* panel, HMENU h);
2388-
void AuxInvokeCommand(CFilesWindow* panel, CMINVOKECOMMANDINFO* ici);
2390+
// Both return the HRESULT from the wrapped shell call, or E_UNEXPECTED if it raised an
2391+
// exception. Callers may ignore it, but it is the only evidence we get when a shell
2392+
// extension refuses a command (see shellsup_diag.h and issues #13/#15/#20).
2393+
HRESULT ShellActionAux5(UINT flags, CFilesWindow* panel, HMENU h);
2394+
HRESULT AuxInvokeCommand(CFilesWindow* panel, CMINVOKECOMMANDINFO* ici);
23892395
void ShellActionAux6(CFilesWindow* panel);
23902396

23912397
//******************************************************************************

src/files_window_actions.cpp

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
#include "precomp.h"
66

77
#include "common/IEnvironment.h"
8+
#include "common/unicode/helpers.h"
9+
#include "common/unicode/PanelPathPolicy.h"
810
#include "cfgdlg.h"
911
#include "mainwnd.h"
1012
#include "usermenu.h"
@@ -280,9 +282,21 @@ void CFilesWindow::Execute(int index)
280282
// launch of the default context menu item (association)
281283
HCURSOR oldCur = SetCursor(LoadCursor(NULL, IDC_WAIT));
282284
MainWindow->SetDefaultDirectories(); // to ensure the launching process inherits the correct current directories
283-
// Use wide version for Unicode filenames that can't be represented in ANSI
284-
if (file->UseWideName())
285-
ExecuteAssociationW(GetListBoxHWND(), GetPath(), file->NameW);
285+
286+
// Go wide when EITHER the file name or the panel path needs it. Testing only
287+
// the name meant that inside a folder whose own path is outside the ANSI code
288+
// page, every file took the legacy branch - including files with perfectly
289+
// ordinary ASCII names, because NameW is NULL for those. That branch resolves
290+
// the shell binding from the lossy CP_ACP directory, so it silently failed and
291+
// Enter did nothing at all for every file in the folder.
292+
//
293+
// Same predicate CFilesWindow::RenameFile already uses (files_window_view_edit.cpp).
294+
if (file->UseWideName() || sally::unicode::WidePathNeedsExactPreservation(GetPathW()))
295+
{
296+
std::wstring nameW = (file->NameW != NULL) ? std::wstring(file->NameW)
297+
: AnsiToWide(fileName);
298+
ExecuteAssociationW(GetListBoxHWND(), GetPathW(), nameW.c_str());
299+
}
286300
else
287301
ExecuteAssociation(GetListBoxHWND(), GetPath(), fileName);
288302

src/lang/texts.rc2

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ STRINGTABLE
169169
IDS_MENU_HELP_CONTEXT, "&What is This?\tShift+F1"
170170
// IDS_MENU_HELP_TIP, "&Tip of the Day"
171171
IDS_MENU_HELP_TASKLIST, "Task &List..."
172+
IDS_MENU_HELP_REPORTBUG, "&Report a Bug..."
172173
IDS_MENU_HELP_FORUM, "Official Support &Forum"
173174
IDS_MENU_HELP_ABOUTPLUGINS, "About &Plugin"
174175
IDS_MENU_HELP_CHECKNEWVERSION, "&Check For New Versions..."
@@ -1777,7 +1778,10 @@ IDS_BROKENFSPLUGINVERSION2, "The plugin (%s) targets broken Sally SDK version 10
17771778
IDS_ACLNOTSUPPORTEDONTGTPATH, "Owners and permissions cannot be preserved. They are not supported\non target path (it is not NTFS volume).\n\nDo you want to continue?"
17781779

17791780
IDS_BUGREPORTCNFRM_TITLE, "Report a Bug"
1780-
IDS_BUGREPORTCNFRM_TEXT, "For bug reports or feature requests please use menu Help > Official Support Forum.\n\nUse this bug report only for sending us sensitive data or when we ask you."
1781+
IDS_BUGREPORTCNFRM_TEXT, "For bug reports or feature requests please use menu Help > Official Support Forum.\n\nUse this bug report only for sending us sensitive data or when we ask you.\n\nThe report describes this running copy of Sally: configuration, loaded modules, panel paths, and shell integration state. Please review it before sending.\n\nCreate the report now?"
1782+
IDS_BUGREPORT_WRITTEN, "The bug report has been written to:"
1783+
IDS_BUGREPORT_FAILED, "The bug report could not be written."
1784+
IDS_SHELLMENU_NOSHELLITEMS, "Windows could not provide a context menu for the selected item(s)."
17811785

17821786
IDS_MSGBOXBTN_SKIP, "&Skip"
17831787
IDS_MSGBOXBTN_SKIPALL, "S&kip All"

src/main_window_commands_help.cpp

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3019,6 +3019,70 @@ MENU_TEMPLATE_ITEM AddToSystemMenu[] =
30193019
return 0;
30203020
}
30213021

3022+
case CM_REPORTBUG:
3023+
{
3024+
// Produce a bug report for THIS process, without crashing it.
3025+
//
3026+
// The pre-existing route to a bug report is Help > Task List > Break, which
3027+
// refuses to break its own process (so the user needs two Sally instances) and
3028+
// ends in TerminateProcess. Neither is something we can ask a bug reporter to
3029+
// do. PrintBugReport already supports Exception == NULL - the thread-suspending
3030+
// "Stack Back Trace" block is gated on Exception != NULL - so the manual case
3031+
// costs nothing but a file.
3032+
//
3033+
// This is the only channel that carries the shell-integration ledgers
3034+
// (shiconov_diag.h, shellsup_diag.h) to a user running a Release build, where
3035+
// every TRACE_* is compiled out.
3036+
if (gPrompter->AskYesNo(LoadStrW(IDS_BUGREPORTCNFRM_TITLE),
3037+
LoadStrW(IDS_BUGREPORTCNFRM_TEXT))
3038+
.type != PromptResult::kYes)
3039+
{
3040+
return 0;
3041+
}
3042+
3043+
// Same directory salmon uses, computed directly so this works even when salmon
3044+
// is not running: %LOCALAPPDATA%\Open Salamander.
3045+
char reportPath[MAX_PATH];
3046+
reportPath[0] = 0;
3047+
if (SHGetFolderPath(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, reportPath) != S_OK)
3048+
{
3049+
gPrompter->ShowError(LoadStrW(IDS_ERRORTITLE), LoadStrW(IDS_BUGREPORT_FAILED));
3050+
return 0;
3051+
}
3052+
int len = lstrlen(reportPath);
3053+
if (len > 0 && reportPath[len - 1] == '\\')
3054+
reportPath[len - 1] = 0;
3055+
lstrcat(reportPath, "\\Open Salamander");
3056+
3057+
// Directory creation goes through the interface, never Win32 directly, so this
3058+
// file stays out of the win32-isolation allowlist.
3059+
std::wstring reportDirW = AnsiToWide(reportPath);
3060+
if (gFileSystem != NULL && !gFileSystem->DirectoryExists(reportDirW.c_str()))
3061+
gFileSystem->CreateDirectory(reportDirW.c_str());
3062+
3063+
// Must end in .TXT: that is what salmon's existing scan of this directory looks
3064+
// for when it offers to compress and upload old reports.
3065+
SYSTEMTIME st;
3066+
GetLocalTime(&st);
3067+
char fileName[64];
3068+
_snprintf_s(fileName, _TRUNCATE, "\\Sally-report-%04d%02d%02d-%02d%02d%02d.TXT",
3069+
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
3070+
lstrcat(reportPath, fileName);
3071+
3072+
if (CCallStack::CreateBugReportFile(NULL, GetCurrentThreadId(), -1, reportPath))
3073+
{
3074+
char message[MAX_PATH + 200];
3075+
_snprintf_s(message, _TRUNCATE, "%s\n\n%s",
3076+
LoadStr(IDS_BUGREPORT_WRITTEN), reportPath);
3077+
gPrompter->ShowInfo(LoadStrW(IDS_INFOTITLE), AnsiToWide(message).c_str());
3078+
}
3079+
else
3080+
{
3081+
gPrompter->ShowError(LoadStrW(IDS_ERRORTITLE), LoadStrW(IDS_BUGREPORT_FAILED));
3082+
}
3083+
return 0;
3084+
}
3085+
30223086
case CM_CLIPCOPYFULLNAME:
30233087
{
30243088
activePanel->UserWorkedOnThisPath = TRUE;

0 commit comments

Comments
 (0)