ptmr

Light-weight local project (task) minute timer(s)
Written in C++ for Linux Ubuntu (Cinnamon)
Contribution(s): Claude 4.5 [beware!]

Download

ptmr-346025.zip - 346.025 - 44.7kb

SHA256

7bd1af5aef895851e728bd625f3acc94
8ebde295e6a9075efa2c0d0cbdf0d847

Changelog

346025 - x-bold/border, italic, done-bg
344025 - bold/border active
343025 - percentage bg-color
342025 - up/down row adjustment
340025 - tab/focus limit
339025 - release

Install

1. unpack to location
2. create new desktop launcher (Linux)
3. name: ptmr
4. command: [location]/ptmr/ptmr-[version]
5. icon (click): alarm-symbolic (optional)

Source
  1. // project task timer
  2. // 346025
  3. // vgmlr
  4. #include <gtkmm.h>
  5. #include <libnotify/notify.h>
  6. #include <iostream>
  7. #include <iomanip>
  8. #include <sstream>
  9. #include <vector>
  10. const std::string CSS =
  11. "window { background-color: #EEEEEE; }"
  12. ".add-btn:hover { border-color: #BBBBBB; background-color:#FFFFFF; min-width: 12px; }"
  13. "list, list row, list row:hover, list row:selected, list row:focus { background-color: transparent; outline: none; box-shadow: none; }"
  14. "list row:first-child { margin-top: 2px; }"
  15. "entry { border-color: #BBBBBB; font-family: Monospace; font-size: 0.9em; }"
  16. "entry:focus { border-color: #BBBBBB; }"
  17. "entry.active-entry { font-style: italic; }"
  18. ".atn-btn, .atn-btn:active { border-color: #BBBBBB; background-color: #FFFFFF; min-width: 12px; }"
  19. ".atn-btn:hover, .atn-btn-done { border-color: #BBBBBB; background-color: #EEEEEE; min-width: 12px; }"
  20. "spinbutton button { border-color: #BBBBBB; }"
  21. "spinbutton button:hover, spinbutton.spinbutton-done, spinbutton.spinbutton-done button, spinbutton.spinbutton-done entry { border-color: #BBBBBB; background-color: #EEEEEE; }"
  22. ".start-btn { border-color: #BBBBBB; background-color: #FFFFFF; min-width: 50px; }"
  23. ".start-btn:hover, .start-btn-done { border-color: #BBBBBB; background-color: #EEEEEE; min-width: 50px; }"
  24. ".remove-btn { border-color: #BBBBBB; min-width: 12px; }"
  25. ".remove-btn:hover, .remove-btn-done { border-color: #BBBBBB; background-color: #EEEEEE; min-width: 12px; }";
  26. class NotificationHelper {
  27. public:
  28. NotificationHelper() {
  29. notify_init("ptmr");
  30. }
  31. void send_alert(const std::string& project_name, const std::string& next_project_name) {
  32. NotifyNotification* n = notify_notification_new(
  33. ("Finished: " + project_name).c_str(),
  34. ("Next: " + next_project_name).c_str(),
  35. "dialog-information"
  36. );
  37. notify_notification_set_urgency(n, NOTIFY_URGENCY_CRITICAL);
  38. notify_notification_show(n, nullptr);
  39. g_object_unref(G_OBJECT(n));
  40. }
  41. };
  42. class tmr_item : public Gtk::ListBoxRow {
  43. public:
  44. sigc::signal<void, tmr_item*> signal_start_clicked;
  45. sigc::signal<void, tmr_item*> signal_finished;
  46. sigc::signal<void, tmr_item*, bool> signal_move;
  47. sigc::signal<void, tmr_item*> signal_request_next_entry;
  48. sigc::signal<void, tmr_item*> signal_request_prev_entry;
  49. tmr_item(const std::string& name) :
  50. sec_left(0), sec_start(0), is_running(false), p_box(Gtk::ORIENTATION_HORIZONTAL, 5) {
  51. p_box.set_margin_top(5);
  52. p_box.set_margin_start(5);
  53. p_box.set_margin_end(5);
  54. add(p_box);
  55. up_btn.set_label("▲");
  56. up_btn.get_style_context()->add_class("atn-btn");
  57. up_btn.set_can_focus(false);
  58. up_btn.signal_clicked().connect([this] { signal_move.emit(this, true); });
  59. p_box.pack_start(up_btn, false, false, 0);
  60. down_btn.set_label("▼");
  61. down_btn.get_style_context()->add_class("atn-btn");
  62. down_btn.set_can_focus(false);
  63. down_btn.signal_clicked().connect([this] { signal_move.emit(this, false); });
  64. p_box.pack_start(down_btn, false, false, 0);
  65. entry.set_text(name);
  66. entry.signal_key_press_event().connect(sigc::mem_fun(*this, &tmr_item::on_entry_key_press));
  67. p_box.pack_start(entry, true, true, 0);
  68. auto adj = Gtk::Adjustment::create(60, 1, 360, 1);
  69. minutes_spin.set_adjustment(adj);
  70. p_box.pack_start(minutes_spin, false, false, 0);
  71. start_btn.set_label("Start");
  72. start_btn.get_style_context()->add_class("start-btn");
  73. start_btn.set_can_focus(false);
  74. start_btn.signal_clicked().connect(sigc::mem_fun(*this, &tmr_item::toggle_timer));
  75. p_box.pack_start(start_btn, false, false, 0);
  76. remove_btn.set_label("✖");
  77. remove_btn.get_style_context()->add_class("remove-btn");
  78. remove_btn.set_can_focus(false);
  79. remove_btn.signal_clicked().connect([this] {
  80. auto parent = dynamic_cast<Gtk::ListBox*>(get_parent());
  81. if (parent) parent->remove(*this);
  82. });
  83. p_box.pack_start(remove_btn, false, false, 0);
  84. show_all_children();
  85. }
  86. Gtk::Entry* get_entry() { return &entry; }
  87. std::string get_project_name() const { return entry.get_text(); }
  88. std::string get_time_string() const {
  89. int mins = sec_left / 60;
  90. int secs = sec_left % 60;
  91. std::stringstream ss;
  92. ss << std::setfill('0') << std::setw(2) << mins << ":"
  93. << std::setfill('0') << std::setw(2) << secs;
  94. return ss.str();
  95. }
  96. void set_entry_bold(bool bold) {
  97. auto style_ctx = entry.get_style_context();
  98. if (bold) {
  99. style_ctx->add_class("active-entry");
  100. } else {
  101. style_ctx->remove_class("active-entry");
  102. }
  103. }
  104. void update_entry_background() {
  105. if (sec_start <= 0) return;
  106. double percent = (sec_start - sec_left) / static_cast<double>(sec_start) * 100.0;
  107. percent = std::max(0.0, std::min(100.0, percent));
  108. std::stringstream ss;
  109. ss << std::fixed << std::setprecision(1) << percent;
  110. std::string percent_str = ss.str();
  111. std::string gradient = "entry { background: linear-gradient(to right, #EEEEEE 0%, #EEEEEE "
  112. + percent_str + "%, #FFFFFF " + percent_str + "%, #FFFFFF 100%); }";
  113. auto css_set = Gtk::CssProvider::create();
  114. css_set->load_from_data(gradient);
  115. entry.get_style_context()->add_provider(css_set, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
  116. }
  117. void toggle_timer() {
  118. if (!is_running) {
  119. if (sec_left <= 0) {
  120. sec_left = (int)minutes_spin.get_value() * 60;
  121. sec_start = sec_left;
  122. update_entry_background();
  123. }
  124. start_timer();
  125. signal_start_clicked.emit(this);
  126. } else {
  127. stop_timer();
  128. }
  129. }
  130. void start_timer() {
  131. is_running = true;
  132. start_btn.set_label("Pause");
  133. timer_conn = Glib::signal_timeout().connect(sigc::mem_fun(*this, &tmr_item::tick), 1000);
  134. }
  135. void stop_timer() {
  136. is_running = false;
  137. start_btn.set_label("Start");
  138. timer_conn.disconnect();
  139. }
  140. bool tick() {
  141. if (!is_running) return false;
  142. sec_left--;
  143. update_entry_background();
  144. if (sec_left <= 0) {
  145. stop_timer();
  146. start_btn.set_label("Done");
  147. up_btn.get_style_context()->add_class("atn-btn-done");
  148. down_btn.get_style_context()->add_class("atn-btn-done");
  149. minutes_spin.get_style_context()->add_class("spinbutton-done");
  150. start_btn.get_style_context()->add_class("start-btn-done");
  151. remove_btn.get_style_context()->add_class("remove-btn-done");
  152. signal_finished.emit(this);
  153. return false;
  154. }
  155. return true;
  156. }
  157. bool running() const { return is_running; }
  158. protected:
  159. bool on_entry_key_press(GdkEventKey* event) {
  160. if (event->keyval == GDK_KEY_Tab) {
  161. if (event->state & Gdk::SHIFT_MASK) {
  162. signal_request_prev_entry.emit(this);
  163. } else {
  164. signal_request_next_entry.emit(this);
  165. }
  166. return true;
  167. }
  168. return false;
  169. }
  170. private:
  171. int sec_left;
  172. int sec_start;
  173. bool is_running;
  174. sigc::connection timer_conn;
  175. Gtk::Box p_box;
  176. Gtk::Button up_btn;
  177. Gtk::Button down_btn;
  178. Gtk::Entry entry;
  179. Gtk::SpinButton minutes_spin;
  180. Gtk::Button start_btn;
  181. Gtk::Button remove_btn;
  182. };
  183. class PtmrApp : public Gtk::Window {
  184. public:
  185. PtmrApp() : active_item(nullptr), vbox(Gtk::ORIENTATION_VERTICAL, 10) {
  186. set_title("ptmr");
  187. set_default_size(550, 450);
  188. auto css_insert = Gtk::CssProvider::create();
  189. css_insert->load_from_data(CSS);
  190. Gtk::StyleContext::add_provider_for_screen(Gdk::Screen::get_default(), css_insert, GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
  191. add(vbox);
  192. Gtk::HeaderBar* header = Gtk::make_managed<Gtk::HeaderBar>();
  193. header->set_title("ptmr");
  194. header->set_show_close_button(true);
  195. set_titlebar(*header);
  196. Gtk::Button* add_btn = Gtk::make_managed<Gtk::Button>("Add Project");
  197. add_btn->signal_clicked().connect(sigc::mem_fun(*this, &PtmrApp::on_add_project));
  198. header->pack_start(*add_btn);
  199. listbox.set_selection_mode(Gtk::SELECTION_NONE);
  200. scrolled_window.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
  201. scrolled_window.add(listbox);
  202. vbox.pack_start(scrolled_window, true, true, 0);
  203. Glib::signal_timeout().connect(sigc::mem_fun(*this, &PtmrApp::update_title), 1000);
  204. show_all_children();
  205. }
  206. private:
  207. void on_add_project() {
  208. auto item = Gtk::make_managed<tmr_item>("");
  209. item->signal_start_clicked.connect(sigc::mem_fun(*this, &PtmrApp::on_start_selected));
  210. item->signal_finished.connect(sigc::mem_fun(*this, &PtmrApp::on_timer_finished));
  211. item->signal_move.connect(sigc::mem_fun(*this, &PtmrApp::on_move_item));
  212. item->signal_request_next_entry.connect(sigc::mem_fun(*this, &PtmrApp::on_request_next_entry));
  213. item->signal_request_prev_entry.connect(sigc::mem_fun(*this, &PtmrApp::on_request_prev_entry));
  214. listbox.add(*item);
  215. item->show();
  216. }
  217. void on_move_item(tmr_item* item, bool move_up) {
  218. int current_pos = item->get_index();
  219. int new_pos = move_up ? current_pos - 1 : current_pos + 1;
  220. auto chidlen = listbox.get_children();
  221. if (new_pos >= 0 && new_pos < (int)chidlen.size()) {
  222. listbox.remove(*item);
  223. listbox.insert(*item, new_pos);
  224. }
  225. }
  226. void on_request_next_entry(tmr_item* current_item) {
  227. int current_pos = current_item->get_index();
  228. auto chidlen = listbox.get_children();
  229. if (current_pos + 1 < (int)chidlen.size()) {
  230. auto next_row = dynamic_cast<tmr_item*>(chidlen[current_pos + 1]);
  231. if (next_row) {
  232. next_row->get_entry()->grab_focus();
  233. }
  234. }
  235. }
  236. void on_request_prev_entry(tmr_item* current_item) {
  237. int current_pos = current_item->get_index();
  238. if (current_pos - 1 >= 0) {
  239. auto chidlen = listbox.get_children();
  240. auto prev_row = dynamic_cast<tmr_item*>(chidlen[current_pos - 1]);
  241. if (prev_row) {
  242. prev_row->get_entry()->grab_focus();
  243. }
  244. }
  245. }
  246. void on_start_selected(tmr_item* item) {
  247. if (active_item && active_item != item) {
  248. active_item->stop_timer();
  249. active_item->set_entry_bold(false);
  250. }
  251. active_item = item;
  252. active_item->set_entry_bold(true);
  253. }
  254. void on_timer_finished(tmr_item* finished_item) {
  255. std::string next_name = "No more projects";
  256. auto chidlen = listbox.get_children();
  257. for (size_t i = 0; i < chidlen.size(); ++i) {
  258. if (chidlen[i] == finished_item && i + 1 < chidlen.size()) {
  259. auto next_row = dynamic_cast<tmr_item*>(chidlen[i+1]);
  260. if (next_row) next_name = next_row->get_project_name();
  261. break;
  262. }
  263. }
  264. notifier.send_alert(finished_item->get_project_name(), next_name);
  265. finished_item->set_entry_bold(false);
  266. active_item = nullptr;
  267. set_title("ptmr");
  268. }
  269. bool update_title() {
  270. if (active_item && active_item->running()) {
  271. set_title(active_item->get_time_string() + " - " + active_item->get_project_name());
  272. }
  273. return true;
  274. }
  275. tmr_item* active_item;
  276. NotificationHelper notifier;
  277. Gtk::Box vbox;
  278. Gtk::ListBox listbox;
  279. Gtk::ScrolledWindow scrolled_window;
  280. };
  281. int main(int argc, char* argv[]) {
  282. auto app = Gtk::Application::create(argc, argv, "org.gtkmm.ptmr");
  283. PtmrApp window;
  284. return app->run(window);
  285. }
Update(s)-
1. Kerf - 360.025 [ChangeLog]
2. ptmr - 346.025 [ChangeLog]
15:37 359 025
Dev-
1. TVShow (227) 'CSA'
2. TVShow (228) 'APT'
3. TVProgram (83) 'BXT'
4. Miter update(s)
14:37 359 025

Menu
Index
Engineering
Entertainment
Literature
Miscellaneous
Contact
Search
tiktok.com/@vgmlr
youtube.com/@vgmlr
Miter
@vgmlr
=SUM(parts)
87 miters
132 tenons
Subscribe
0.0021