1 #include "modules/clock.hpp"
2 
3 #include <spdlog/spdlog.h>
4 #if FMT_VERSION < 60000
5 #include <fmt/time.h>
6 #else
7 #include <fmt/chrono.h>
8 #endif
9 
10 #include <ctime>
11 #include <sstream>
12 #include <type_traits>
13 
14 #include "util/ustring_clen.hpp"
15 #include "util/waybar_time.hpp"
16 #ifdef HAVE_LANGINFO_1STDAY
17 #include <langinfo.h>
18 #include <locale.h>
19 #endif
20 
21 using waybar::waybar_time;
22 
Clock(const std::string & id,const Json::Value & config)23 waybar::modules::Clock::Clock(const std::string& id, const Json::Value& config)
24     : ALabel(config, "clock", id, "{:%H:%M}", 60, false, false, true),
25       current_time_zone_idx_(0),
26       is_calendar_in_tooltip_(false),
27       is_timezoned_list_in_tooltip_(false)
28 {
29   if (config_["timezones"].isArray() && !config_["timezones"].empty()) {
30     for (const auto& zone_name: config_["timezones"]) {
31       if (!zone_name.isString() || zone_name.asString().empty()) {
32         time_zones_.push_back(nullptr);
33         continue;
34       }
35       time_zones_.push_back(
36         date::locate_zone(
37           zone_name.asString()
38         )
39       );
40     }
41   } else if (config_["timezone"].isString() && !config_["timezone"].asString().empty()) {
42     time_zones_.push_back(
43         date::locate_zone(
44           config_["timezone"].asString()
45         )
46       );
47   }
48 
49   // If all timezones are parsed and no one is good, add nullptr to the timezones vector, to mark that local time should be shown.
50   if (!time_zones_.size()) {
51     time_zones_.push_back(nullptr);
52   }
53 
54   if (!is_timezone_fixed()) {
55     spdlog::warn("As using a timezone, some format args may be missing as the date library haven't got a release since 2018.");
56   }
57 
58   // Check if a particular placeholder is present in the tooltip format, to know what to calculate on update.
59   if (config_["tooltip-format"].isString()) {
60     std::string trimmed_format = config_["tooltip-format"].asString();
61     trimmed_format.erase(std::remove_if(trimmed_format.begin(),
62                               trimmed_format.end(),
63                               [](unsigned char x){return std::isspace(x);}),
64                trimmed_format.end());
65     if (trimmed_format.find("{" + kCalendarPlaceholder + "}") != std::string::npos) {
66       is_calendar_in_tooltip_ = true;
67     }
68     if (trimmed_format.find("{" + KTimezonedTimeListPlaceholder + "}") != std::string::npos) {
69       is_timezoned_list_in_tooltip_ = true;
70     }
71   }
72 
73   if (config_["locale"].isString()) {
74     locale_ = std::locale(config_["locale"].asString());
75   } else {
76     locale_ = std::locale("");
77   }
78 
79   thread_ = [this] {
80     dp.emit();
81     auto now = std::chrono::system_clock::now();
82     auto timeout = std::chrono::floor<std::chrono::seconds>(now + interval_);
83     auto diff = std::chrono::seconds(timeout.time_since_epoch().count() % interval_.count());
84     thread_.sleep_until(timeout - diff);
85   };
86 }
87 
current_timezone()88 const date::time_zone* waybar::modules::Clock::current_timezone() {
89   return time_zones_[current_time_zone_idx_] ? time_zones_[current_time_zone_idx_] : date::current_zone();
90 }
91 
is_timezone_fixed()92 bool waybar::modules::Clock::is_timezone_fixed() {
93   return time_zones_[current_time_zone_idx_] != nullptr;
94 }
95 
update()96 auto waybar::modules::Clock::update() -> void {
97   auto time_zone = current_timezone();
98   auto now = std::chrono::system_clock::now();
99   waybar_time wtime = {locale_,
100                        date::make_zoned(time_zone, date::floor<std::chrono::seconds>(now))};
101   std::string text = "";
102   if (!is_timezone_fixed()) {
103     // As date dep is not fully compatible, prefer fmt
104     tzset();
105     auto localtime = fmt::localtime(std::chrono::system_clock::to_time_t(now));
106     text = fmt::format(locale_, format_, localtime);
107   } else {
108     text = fmt::format(format_, wtime);
109   }
110   label_.set_markup(text);
111 
112   if (tooltipEnabled()) {
113     if (config_["tooltip-format"].isString()) {
114       std::string calendar_lines = "";
115       std::string timezoned_time_lines = "";
116       if (is_calendar_in_tooltip_) {
117         calendar_lines = calendar_text(wtime);
118       }
119       if (is_timezoned_list_in_tooltip_) {
120         timezoned_time_lines = timezones_text(&now);
121       }
122       auto tooltip_format = config_["tooltip-format"].asString();
123       text = fmt::format(tooltip_format, wtime, fmt::arg(kCalendarPlaceholder.c_str(), calendar_lines), fmt::arg(KTimezonedTimeListPlaceholder.c_str(), timezoned_time_lines));
124       label_.set_tooltip_markup(text);
125     }
126   }
127 
128   // Call parent update
129   ALabel::update();
130 }
131 
handleScroll(GdkEventScroll * e)132 bool waybar::modules::Clock::handleScroll(GdkEventScroll *e) {
133   // defer to user commands if set
134   if (config_["on-scroll-up"].isString() || config_["on-scroll-down"].isString()) {
135     return AModule::handleScroll(e);
136   }
137 
138   auto dir = AModule::getScrollDir(e);
139   if (dir != SCROLL_DIR::UP && dir != SCROLL_DIR::DOWN) {
140     return true;
141   }
142   if (time_zones_.size() == 1) {
143     return true;
144   }
145 
146   auto nr_zones = time_zones_.size();
147   if (dir == SCROLL_DIR::UP) {
148     size_t new_idx = current_time_zone_idx_ + 1;
149     current_time_zone_idx_ = new_idx == nr_zones ? 0 : new_idx;
150   } else {
151     current_time_zone_idx_ = current_time_zone_idx_ == 0 ? nr_zones - 1 : current_time_zone_idx_ - 1;
152   }
153 
154   update();
155   return true;
156 }
157 
calendar_text(const waybar_time & wtime)158 auto waybar::modules::Clock::calendar_text(const waybar_time& wtime) -> std::string {
159   const auto daypoint = date::floor<date::days>(wtime.ztime.get_local_time());
160   const auto ymd = date::year_month_day(daypoint);
161   if (cached_calendar_ymd_ == ymd) {
162     return cached_calendar_text_;
163   }
164 
165   const date::year_month ym(ymd.year(), ymd.month());
166   const auto             curr_day = ymd.day();
167 
168   std::stringstream os;
169   const auto        first_dow = first_day_of_week();
170   weekdays_header(first_dow, os);
171 
172   // First week prefixed with spaces if needed.
173   auto wd = date::weekday(ym / 1);
174   auto empty_days = (wd - first_dow).count();
175   if (empty_days > 0) {
176     os << std::string(empty_days * 3 - 1, ' ');
177   }
178   auto last_day = (ym / date::literals::last).day();
179   for (auto d = date::day(1); d <= last_day; ++d, ++wd) {
180     if (wd != first_dow) {
181       os << ' ';
182     } else if (unsigned(d) != 1) {
183       os << '\n';
184     }
185     if (d == curr_day) {
186       if (config_["today-format"].isString()) {
187         auto today_format = config_["today-format"].asString();
188         os << fmt::format(today_format, date::format("%e", d));
189       } else {
190         os << "<b><u>" << date::format("%e", d) << "</u></b>";
191       }
192     } else {
193       os << date::format("%e", d);
194     }
195   }
196 
197   auto result = os.str();
198   cached_calendar_ymd_ = ymd;
199   cached_calendar_text_ = result;
200   return result;
201 }
202 
weekdays_header(const date::weekday & first_dow,std::ostream & os)203 auto waybar::modules::Clock::weekdays_header(const date::weekday& first_dow, std::ostream& os)
204     -> void {
205   auto wd = first_dow;
206   do {
207     if (wd != first_dow) os << ' ';
208     Glib::ustring wd_ustring(date::format(locale_, "%a", wd));
209     auto clen = ustring_clen(wd_ustring);
210     auto wd_len = wd_ustring.length();
211     while (clen > 2) {
212       wd_ustring = wd_ustring.substr(0, wd_len-1);
213       wd_len--;
214       clen = ustring_clen(wd_ustring);
215     }
216     const std::string pad(2 - clen, ' ');
217     os << pad << wd_ustring;
218   } while (++wd != first_dow);
219   os << "\n";
220 }
221 
timezones_text(std::chrono::system_clock::time_point * now)222 auto waybar::modules::Clock::timezones_text(std::chrono::system_clock::time_point *now) -> std::string {
223   if (time_zones_.size() == 1) {
224     return "";
225   }
226   std::stringstream os;
227   waybar_time wtime;
228   for (size_t time_zone_idx = 0; time_zone_idx < time_zones_.size(); ++time_zone_idx) {
229     if (static_cast<int>(time_zone_idx) == current_time_zone_idx_) {
230       continue;
231     }
232     const date::time_zone* timezone = time_zones_[time_zone_idx];
233     if (!timezone) {
234       timezone = date::current_zone();
235     }
236     wtime = {locale_, date::make_zoned(timezone, date::floor<std::chrono::seconds>(*now))};
237     os << fmt::format(format_, wtime) << "\n";
238   }
239   return os.str();
240 }
241 
242 #ifdef HAVE_LANGINFO_1STDAY
243 template <auto fn>
244 using deleter_from_fn = std::integral_constant<decltype(fn), fn>;
245 
246 template <typename T, auto fn>
247 using deleting_unique_ptr = std::unique_ptr<T, deleter_from_fn<fn>>;
248 #endif
249 
250 // Computations done similarly to Linux cal utility.
first_day_of_week()251 auto waybar::modules::Clock::first_day_of_week() -> date::weekday {
252 #ifdef HAVE_LANGINFO_1STDAY
253   deleting_unique_ptr<std::remove_pointer<locale_t>::type, freelocale> posix_locale{
254       newlocale(LC_ALL, locale_.name().c_str(), nullptr)};
255   if (posix_locale) {
256     const int i = (std::intptr_t)nl_langinfo_l(_NL_TIME_WEEK_1STDAY, posix_locale.get());
257     auto      ymd = date::year(i / 10000) / (i / 100 % 100) / (i % 100);
258     auto      wd = date::weekday(ymd);
259     uint8_t   j = *nl_langinfo_l(_NL_TIME_FIRST_WEEKDAY, posix_locale.get());
260     return wd + date::days(j - 1);
261   }
262 #endif
263   return date::Sunday;
264 }
265