ptmr

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

Download

ptmr-343025.zip - 343.025 - 44.0kb

SHA256

da584d79bae3e165f5d2fdfa00753872
dbfed118a792f681df3119714586ea69

Changelog

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

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