Repository :
https://github.com/FarGroup/FarManager
On branch : master
Link :
https://github.com/FarGroup/FarManager/commit/26325b7cfd1134f601ae8d848176e78c57b55b09
>---------------------------------------------------------------
commit 26325b7cfd1134f601ae8d848176e78c57b55b09
Author: Alex Alabuzhev <
alab...@gmail.com>
Date: Mon Apr 6 19:37:17 2026 +0100
Allow view/edit times as UTC in Attributes and Filters
>---------------------------------------------------------------
26325b7cfd1134f601ae8d848176e78c57b55b09
far/datetime.cpp | 9 +++++
far/datetime.hpp | 1 +
far/filefilterparams.cpp | 49 +++++++++++++++++++++--
far/setattr.cpp | 101 +++++++++++++++++++++++++++++++++++++++++++----
4 files changed, 150 insertions(+), 10 deletions(-)
diff --git a/far/datetime.cpp b/far/datetime.cpp
index bd2db1196..df338867d 100644
--- a/far/datetime.cpp
+++ b/far/datetime.cpp
@@ -648,6 +648,15 @@ std::tuple<string, string> time_point_to_localtime_string(os::chrono::time_point
return time_to_string(LocalTime, TimeLength, FullYear, Brief, is_recent_date(Point, CurrentTime), TextMonth);
}
+std::tuple<string, string> time_point_to_utc_string(os::chrono::time_point const Point, int const TimeLength, int const FullYear, bool const Brief, bool const TextMonth, os::chrono::time_point const CurrentTime)
+{
+ os::chrono::utc_time UtcTime;
+ if (!os::chrono::timepoint_to_utc(Point, UtcTime))
+ return {};
+
+ return time_to_string(UtcTime, TimeLength, FullYear, Brief, is_recent_date(Point, CurrentTime), TextMonth);
+}
+
template<typename T> requires (T::period::num < T::period::den && T::period::num == 1 && T::period::den % 10 == 0)
static constexpr auto decimal_duration_width()
{
diff --git a/far/datetime.hpp b/far/datetime.hpp
index 3953cdef3..9730e4364 100644
--- a/far/datetime.hpp
+++ b/far/datetime.hpp
@@ -89,6 +89,7 @@ FullYear:
*/
// (date, time)
std::tuple<string, string> time_point_to_localtime_string(os::chrono::time_point Point, int TimeLength, int FullYear, bool Brief = false, bool TextMonth = false, os::chrono::time_point CurrentTime = {});
+std::tuple<string, string> time_point_to_utc_string(os::chrono::time_point Point, int TimeLength, int FullYear, bool Brief = false, bool TextMonth = false, os::chrono::time_point CurrentTime = {});
// (days, time)
std::tuple<string, string> duration_to_string(os::chrono::duration Duration);
diff --git a/far/filefilterparams.cpp b/far/filefilterparams.cpp
index e2c8190cf..6c3499ed2 100644
--- a/far/filefilterparams.cpp
+++ b/far/filefilterparams.cpp
@@ -541,6 +541,7 @@ enum enumFileFilterConfig
ID_FF_TIMEAFTEREDIT,
ID_FF_CURRENT,
ID_FF_BLANK,
+ ID_FF_UTC,
ID_FF_SEPARATOR3,
ID_FF_SEPARATOR4,
@@ -658,7 +659,9 @@ static intptr_t FileFilterConfigDlgProc(Dialog* Dlg,intptr_t Msg,intptr_t Param1
else if (Param1==ID_FF_CURRENT || Param1==ID_FF_BLANK)
{
const auto& [Date, Time] = Param1==ID_FF_CURRENT?
- time_point_to_localtime_string(os::chrono::nt_clock::now(), 16, 2) :
+ static_cast<FARCHECKEDSTATE>(Dlg->SendMessage(DM_GETCHECK, ID_FF_UTC, {})) == BSTATE_CHECKED?
+ time_point_to_utc_string(os::chrono::nt_clock::now(), 16, 2) :
+ time_point_to_localtime_string(os::chrono::nt_clock::now(), 16, 2) :
std::tuple<string, string>{};
SCOPED_ACTION(Dialog::suppress_redraw)(Dlg);
@@ -679,6 +682,43 @@ static intptr_t FileFilterConfigDlgProc(Dialog* Dlg,intptr_t Msg,intptr_t Param1
Dlg->SendMessage(DM_SETCURSORPOS,db,&r);
break;
}
+ else if (Param1 == ID_FF_UTC)
+ {
+ SCOPED_ACTION(Dialog::suppress_redraw)(Dlg);
+
+ if (Dlg->SendMessage(DM_GETCHECK, ID_FF_DATERELATIVE, nullptr) == BSTATE_CHECKED)
+ break; // Relative dates don't have UTC/local distinction
+
+ const auto ToUTC = static_cast<FARCHECKEDSTATE>(std::bit_cast<intptr_t>(Param2)) == BSTATE_CHECKED;
+
+ const auto Update = [&](int const DateId, int const TimeId)
+ {
+ string_view const
+ Date = std::bit_cast<const wchar_t*>(Dlg->SendMessage(DM_GETCONSTTEXTPTR, DateId, {})),
+ Time = std::bit_cast<const wchar_t*>(Dlg->SendMessage(DM_GETCONSTTEXTPTR, TimeId, {}));
+
+ const auto MergedTime = merge_time({}, parse_time(Date, Time, static_cast<int>(locale.date_format())));
+
+ os::chrono::time_point TimePoint;
+
+ ToUTC?
+ os::chrono::localtime_to_timepoint(os::chrono::local_time{ MergedTime }, TimePoint) :
+ os::chrono::utc_to_timepoint(os::chrono::utc_time{ MergedTime }, TimePoint);
+
+ if (TimePoint == os::chrono::time_point{})
+ return;
+
+ const auto [NewDate, NewTime] = ToUTC?
+ time_point_to_utc_string(TimePoint, 16, 2) :
+ time_point_to_localtime_string(TimePoint, 16, 2);
+
+ Dlg->SendMessage(DM_SETTEXTPTR, DateId, UNSAFE_CSTR(NewDate));
+ Dlg->SendMessage(DM_SETTEXTPTR, TimeId, UNSAFE_CSTR(NewTime));
+ };
+
+ Update(ID_FF_DATEBEFOREEDIT, ID_FF_TIMEBEFOREEDIT);
+ Update(ID_FF_DATEAFTEREDIT, ID_FF_TIMEAFTEREDIT);
+ }
else if (Param1==ID_FF_RESET)
{
SCOPED_ACTION(Dialog::suppress_redraw)(Dlg);
@@ -885,7 +925,8 @@ bool FileFilterConfig(FileFilterParams& Filter, bool ColorConfig)
{ DI_FIXEDIT, {{47, 8 }, {57, 8 }}, DIF_MASKEDIT, },
{ DI_FIXEDIT, {{59, 8 }, {74, 8 }}, DIF_MASKEDIT, },
{ DI_BUTTON, {{0, 6 }, {0, 6 }}, DIF_BTNNOCLOSE, msg(lng::MFileFilterCurrent), },
- { DI_BUTTON, {{0, 6 }, {74, 6 }}, DIF_BTNNOCLOSE, msg(lng::MFileFilterBlank), },
+ { DI_BUTTON, {{0, 6 }, {66, 6 }}, DIF_BTNNOCLOSE, msg(lng::MFileFilterBlank), },
+ { DI_CHECKBOX, {{68, 6 }, {74, 6 }}, DIF_NONE, L"UTC"sv, },
{ DI_TEXT, {{-1, 9 }, {0, 9 }}, DIF_SEPARATOR, },
{ DI_VTEXT, {{22, 5 }, {22, 9 }}, DIF_SEPARATORUSER, },
{ DI_CHECKBOX, {{5, 10}, {0, 10}}, DIF_AUTOMATION, msg(lng::MFileFilterAttr)},
@@ -1130,7 +1171,9 @@ bool FileFilterConfig(FileFilterParams& Filter, bool ColorConfig)
const auto MergedTime = merge_time({}, parse_time(FilterDlg[DateId].strData, FilterDlg[TimeId].strData, static_cast<int>(DateFormat)));
os::chrono::time_point TimePoint;
- (void)os::chrono::localtime_to_timepoint(os::chrono::local_time{ MergedTime }, TimePoint);
+ static_cast<FARCHECKEDSTATE>(Dlg->SendMessage(DM_GETCHECK, ID_FF_UTC, {})) == BSTATE_CHECKED?
+ os::chrono::utc_to_timepoint(os::chrono::utc_time{ MergedTime }, TimePoint) :
+ os::chrono::localtime_to_timepoint(os::chrono::local_time{ MergedTime }, TimePoint);
return TimePoint;
};
diff --git a/far/setattr.cpp b/far/setattr.cpp
index fd7f7cdbb..952cdfdc2 100644
--- a/far/setattr.cpp
+++ b/far/setattr.cpp
@@ -111,6 +111,7 @@ enum SETATTRDLG
SA_TEXT_DATETIME,
SA_TEXT_TITLEDATE,
SA_TEXT_TITLETIME,
+ SA_CHECKBOX_UTC,
SA_TEXT_LASTWRITE,
SA_EDIT_WDATE,
SA_EDIT_WTIME,
@@ -253,6 +254,7 @@ struct SetAttrDlgParam
struct
{
+ os::chrono::time_point InitialValue;
struct
{
string InitialValue;
@@ -275,6 +277,11 @@ static auto setattr_time_point_to_localtime_string(os::chrono::time_point const
return time_point_to_localtime_string(TimePoint, 16, 2);
}
+static auto setattr_time_point_to_utc_string(os::chrono::time_point const TimePoint)
+{
+ return time_point_to_utc_string(TimePoint, 16, 2);
+}
+
static os::chrono::time construct_time(
os::chrono::time const OriginalDateTime,
string_view const Date,
@@ -303,6 +310,26 @@ static std::optional<os::chrono::time_point> construct_time_from_localtime(
return {};
}
+static std::optional<os::chrono::time_point> construct_time_from_utc(
+ os::chrono::time_point const OriginalTimePoint,
+ string_view const Date,
+ string_view const Time)
+{
+ os::chrono::utc_time OriginalDateTime;
+ // OriginalDateTime is only needed for inheriting, i.e. when the user leaves some fields empty.
+ // If we can't obtain it for some reason, e.g. the timestamp is invalid, just use 0.
+ // This will allow to set the timestamp to something reasonable at least.
+ if (!os::chrono::timepoint_to_utc(OriginalTimePoint, OriginalDateTime))
+ OriginalDateTime = {};
+
+ const auto DateTime = os::chrono::utc_time{ construct_time(OriginalDateTime, Date, Time) };
+
+ if (os::chrono::time_point Result; os::chrono::utc_to_timepoint(DateTime, Result))
+ return Result;
+
+ return {};
+}
+
static void set_date_or_time(Dialog* const Dlg, int const Id, string const& Value, bool const MakeUnchanged)
{
Dlg->SendMessage(DM_SETTEXTPTR, Id, UNSAFE_CSTR(Value));
@@ -312,11 +339,15 @@ static void set_date_or_time(Dialog* const Dlg, int const Id, string const& Valu
static void set_dates_and_times(Dialog* const Dlg, const time_map& TimeMapEntry, std::optional<os::chrono::time_point> const TimePoint)
{
+ const auto IsUTC = static_cast<FARCHECKEDSTATE>(Dlg->SendMessage(DM_GETCHECK, SA_CHECKBOX_UTC, {})) == BSTATE_CHECKED;
+
string Date, Time;
if (TimePoint)
{
- std::tie(Date, Time) = setattr_time_point_to_localtime_string(*TimePoint);
+ std::tie(Date, Time) = IsUTC?
+ setattr_time_point_to_utc_string(*TimePoint) :
+ setattr_time_point_to_localtime_string(*TimePoint);
}
set_date_or_time(Dlg, TimeMapEntry.DateId, Date, false);
@@ -394,6 +425,56 @@ static intptr_t SetAttrDlgProc(Dialog* Dlg,intptr_t Msg,intptr_t Param1,void* Pa
AdvancedAttributesDialog(DlgParam);
return true;
}
+ else if (Param1 == SA_CHECKBOX_UTC)
+ {
+ SCOPED_ACTION(Dialog::suppress_redraw)(Dlg);
+
+ const auto ToUTC = static_cast<FARCHECKEDSTATE>(std::bit_cast<intptr_t>(Param2)) == BSTATE_CHECKED;
+
+ for (const auto& [i, State]: zip(TimeMap, DlgParam.Times))
+ {
+ const auto set_original = [&]
+ {
+ if (ToUTC)
+ {
+ const auto [Date, Time] = setattr_time_point_to_utc_string(State.InitialValue);
+ set_date_or_time(Dlg, i.DateId, Date, true);
+ set_date_or_time(Dlg, i.TimeId, Time, true);
+ }
+ else
+ {
+ set_date_or_time(Dlg, i.DateId, State.Date.InitialValue, true);
+ set_date_or_time(Dlg, i.TimeId, State.Time.InitialValue, true);
+ }
+ State.Date.ChangedManually = false;
+ State.Time.ChangedManually = false;
+ };
+
+ if (!State.Date.ChangedManually && !State.Time.ChangedManually)
+ set_original(); // Not touched, we can use initial UTC strings
+ else
+ {
+ string_view const
+ Date = std::bit_cast<const wchar_t*>(Dlg->SendMessage(DM_GETCONSTTEXTPTR, i.DateId, {})),
+ Time = std::bit_cast<const wchar_t*>(Dlg->SendMessage(DM_GETCONSTTEXTPTR, i.TimeId, {}));
+
+ const auto Result = (ToUTC? construct_time_from_localtime : construct_time_from_utc)(State.InitialValue, Date, Time);
+
+ if (!Result)
+ // The user entered some rubbish, just keep whatever is there without converting
+ LOGWARNING(L"Error constructing time from {{{}}} {{{}}}: {}"sv, Date, Time, os::last_error());
+ else if (Result == State.InitialValue)
+ // Touched, but resulted in the same time, we can still use initial UTC strings
+ set_original();
+ else
+ {
+ const auto& [UtcDate, UtcTime] = (ToUTC? setattr_time_point_to_utc_string : setattr_time_point_to_localtime_string)(*Result);
+ set_date_or_time(Dlg, i.DateId, UtcDate, false);
+ set_date_or_time(Dlg, i.TimeId, UtcTime, false);
+ }
+ }
+ }
+ }
else if (Param1 == SA_CHECKBOX_SUBFOLDERS)
{
// этот кусок всегда работает если есть хотя бы одна папка
@@ -627,6 +708,7 @@ static bool process_single_file(
state const& Current,
state const& New,
function_ref<const string&(int)> const DateTimeAccessor,
+ bool const IsUTC,
bool& SkipErrors)
{
if (!New.Owner.empty() && !equal_icase(Current.Owner, New.Owner))
@@ -664,7 +746,7 @@ static bool process_single_file(
for (const auto& [i, Time]: zip(TimeMap, Times))
{
const auto OriginalTime = std::invoke(i.Accessor, Current.FindData);
- if (const auto Result = construct_time_from_localtime(OriginalTime, DateTimeAccessor(i.DateId), DateTimeAccessor(i.TimeId)); Result && *Result != OriginalTime)
+ if (const auto Result = (IsUTC? construct_time_from_utc : construct_time_from_localtime)(OriginalTime, DateTimeAccessor(i.DateId), DateTimeAccessor(i.TimeId)); Result && *Result != OriginalTime)
Time = *Result;
}
@@ -747,6 +829,7 @@ static bool ShellSetFileAttributesImpl(Panel* SrcPanel, const string* Object)
{ DI_TEXT, {{C2, TB+0 }, {0, TB+0 }}, DIF_NONE, msg(lng::MSetAttrDate), },
{ DI_TEXT, {{DlgX-33, TB+0 }, {0, TB+0 }}, DIF_NONE, },
{ DI_TEXT, {{DlgX-21, TB+0 }, {0, TB+0 }}, DIF_NONE, },
+ { DI_CHECKBOX, {{DlgX-12, TB+0 }, {DlgX-6, TB+0 }}, DIF_NONE, L"UTC"sv, },
{ DI_TEXT, {{C2, TB+1 }, {0, TB+1 }}, DIF_NONE, msg(lng::MSetAttrWrite), },
{ DI_FIXEDIT, {{DlgX-33, TB+1 }, {DlgX-23, TB+1 }}, DIF_MASKEDIT, },
{ DI_FIXEDIT, {{DlgX-21, TB+1 }, {DlgX-6, TB+1 }}, DIF_MASKEDIT, },
@@ -945,7 +1028,8 @@ static bool ShellSetFileAttributesImpl(Panel* SrcPanel, const string* Object)
for (const auto& [i, State]: zip(TimeMap, DlgParam.Times))
{
- std::tie(State.Date.InitialValue, State.Time.InitialValue) = setattr_time_point_to_localtime_string(std::invoke(i.Accessor, SingleSelFindData));
+ State.InitialValue = std::invoke(i.Accessor, SingleSelFindData);
+ std::tie(State.Date.InitialValue, State.Time.InitialValue) = setattr_time_point_to_localtime_string(State.InitialValue);
AttrDlg[i.DateId].strData = State.Date.InitialValue;
AttrDlg[i.TimeId].strData = State.Time.InitialValue;
@@ -1269,7 +1353,8 @@ static bool ShellSetFileAttributesImpl(Panel* SrcPanel, const string* Object)
if (!Time)
continue;
- std::tie(State.Date.InitialValue, State.Time.InitialValue) = setattr_time_point_to_localtime_string(*Time);
+ State.InitialValue = *Time;
+ std::tie(State.Date.InitialValue, State.Time.InitialValue) = setattr_time_point_to_localtime_string(State.InitialValue);
AttrDlg[i.DateId].strData = State.Date.InitialValue;
AttrDlg[i.TimeId].strData = State.Time.InitialValue;
@@ -1351,6 +1436,8 @@ static bool ShellSetFileAttributesImpl(Panel* SrcPanel, const string* Object)
}
}
+ const auto IsUTC = static_cast<FARCHECKEDSTATE>(Dlg->SendMessage(DM_GETCHECK, SA_CHECKBOX_UTC, {})) == BSTATE_CHECKED;
+
for (const auto& i: TimeMap)
{
AttrDlg[i.TimeId].strData[8] = locale.time_separator();
@@ -1379,7 +1466,7 @@ static bool ShellSetFileAttributesImpl(Panel* SrcPanel, const string* Object)
Current{ DlgParam.Owner.InitialValue, SingleSelFindData },
New{ AttrDlg[SA_EDIT_OWNER].strData, NewFindData };
- if (!process_single_file(ComputerName, SingleSelFileName, Current, New, AttrDlgAccessor, SkipErrors))
+ if (!process_single_file(ComputerName, SingleSelFileName, Current, New, AttrDlgAccessor, IsUTC, SkipErrors))
{
return false;
}
@@ -1419,7 +1506,7 @@ static bool ShellSetFileAttributesImpl(Panel* SrcPanel, const string* Object)
Current{ AttrDlg[SA_CHECKBOX_SUBFOLDERS].Selected? Empty : DlgParam.Owner.InitialValue, SingleSelFindData},
New{ AttrDlg[SA_EDIT_OWNER].strData, NewFindData };
- if (!process_single_file(ComputerName, SingleSelFileName, Current, New, AttrDlgAccessor, SkipErrors))
+ if (!process_single_file(ComputerName, SingleSelFileName, Current, New, AttrDlgAccessor, IsUTC, SkipErrors))
{
return false;
}
@@ -1453,7 +1540,7 @@ static bool ShellSetFileAttributesImpl(Panel* SrcPanel, const string* Object)
Current{ Empty, SingleSelFindData }, // BUGBUG, should we read the owner?
New{ AttrDlg[SA_EDIT_OWNER].strData, NewFindData };
- if (!process_single_file(ComputerName, strFullName, Current, New, AttrDlgAccessor, SkipErrors))
+ if (!process_single_file(ComputerName, strFullName, Current, New, AttrDlgAccessor, IsUTC, SkipErrors))
{
return false;
}