|
1 | 1 | const Desklet = imports.ui.desklet; |
2 | | -const Lang = imports.lang; |
3 | 2 | const St = imports.gi.St; |
4 | 3 | const Mainloop = imports.mainloop; |
5 | 4 | const GLib = imports.gi.GLib; |
6 | 5 | const Settings = imports.ui.settings; |
| 6 | +const Gio = imports.gi.Gio; |
7 | 7 | const Gettext = imports.gettext; |
8 | | -const Clutter = imports.gi.Clutter; |
9 | | -const GdkPixbuf = imports.gi.GdkPixbuf; |
10 | | -const Cogl = imports.gi.Cogl; |
| 8 | +const ByteArray = imports.byteArray; |
11 | 9 |
|
12 | 10 | const UUID = "systemUptime@KopfDesDaemons"; |
13 | 11 |
|
14 | | -Gettext.bindtextdomain(UUID, GLib.get_home_dir() + "/.local/share/locale"); |
| 12 | +Gettext.bindtextdomain(UUID, GLib.get_user_data_dir() + "/locale"); |
15 | 13 |
|
16 | 14 | function _(str) { |
17 | 15 | return Gettext.dgettext(UUID, str); |
18 | 16 | } |
19 | 17 |
|
| 18 | +function getFileContents(path) { |
| 19 | + return new Promise((resolve, reject) => { |
| 20 | + const file = Gio.File.new_for_path(path); |
| 21 | + file.load_contents_async(null, (obj, res) => { |
| 22 | + try { |
| 23 | + const [success, contents] = obj.load_contents_finish(res); |
| 24 | + if (success) { |
| 25 | + resolve(contents); |
| 26 | + } else { |
| 27 | + reject(new Error(`Could not read ${path}`)); |
| 28 | + } |
| 29 | + } catch (e) { |
| 30 | + reject(e); |
| 31 | + } |
| 32 | + }); |
| 33 | + }); |
| 34 | +} |
| 35 | + |
20 | 36 | class MyDesklet extends Desklet.Desklet { |
21 | 37 | constructor(metadata, deskletId) { |
22 | 38 | super(metadata, deskletId); |
| 39 | + this.setHeader(_("System Uptime")); |
23 | 40 |
|
24 | | - this.settings = new Settings.DeskletSettings(this, metadata["uuid"], deskletId); |
| 41 | + this._refreshTimeoutId = null; |
| 42 | + this._isReloading = false; |
| 43 | + this._startTimeLabel = null; |
| 44 | + this._startupValue = null; |
| 45 | + this._uptimeLabel = null; |
| 46 | + this._uptimeValue = null; |
| 47 | + this._clockIcon = null; |
| 48 | + this._iconBox = null; |
| 49 | + this._currentUptimeText = ""; |
| 50 | + this._currentStartupText = ""; |
| 51 | + |
| 52 | + // Listen for changes in the system clock format (12h/24h) |
| 53 | + this._desktop_settings = new Gio.Settings({ schema_id: "org.cinnamon.desktop.interface" }); |
| 54 | + this._clockSettingsId = this._desktop_settings.connect("changed::clock-use-24h", () => this._updateValues()); |
| 55 | + |
| 56 | + // Default settings values |
| 57 | + this.scaleSize = 1; |
| 58 | + this.labelColor = "rgb(51, 209, 122)"; |
| 59 | + this.showStartDate = false; |
| 60 | + this.showUptimeInDays = false; |
| 61 | + this.hideDecorations = true; |
| 62 | + this.showIcon = true; |
25 | 63 |
|
26 | 64 | // Bind settings properties |
27 | | - this.settings.bindProperty(Settings.BindingDirection.IN, "fontSize", "fontSize", this.onSettingsChanged.bind(this)); |
28 | | - this.settings.bindProperty(Settings.BindingDirection.IN, "colorLabel", "colorLabel", this.onSettingsChanged.bind(this)); |
29 | | - this.settings.bindProperty(Settings.BindingDirection.IN, "showStartDate", "showStartDate", this.onSettingsChanged.bind(this)); |
30 | | - this.settings.bindProperty(Settings.BindingDirection.IN, "showUptimeInDays", "showUptimeInDays", this.onSettingsChanged.bind(this)); |
31 | | - this.settings.bindProperty(Settings.BindingDirection.IN, "hideDecorations", "hideDecorations", this.updateDecoration.bind(this)); |
| 65 | + this.settings = new Settings.DeskletSettings(this, metadata["uuid"], deskletId); |
| 66 | + this.settings.bindProperty(Settings.BindingDirection.IN, "scale-size", "scaleSize", this._setStyles); |
| 67 | + this.settings.bindProperty(Settings.BindingDirection.IN, "label-color", "labelColor", this._setStyles); |
| 68 | + this.settings.bindProperty(Settings.BindingDirection.IN, "show-start-date", "showStartDate", this._updateValues); |
| 69 | + this.settings.bindProperty(Settings.BindingDirection.IN, "show-uptime-in-days", "showUptimeInDays", this._updateValues); |
| 70 | + this.settings.bindProperty(Settings.BindingDirection.IN, "hide-decorations", "hideDecorations", this._onDecorationChanged); |
| 71 | + this.settings.bindProperty(Settings.BindingDirection.IN, "show-icon", "showIcon", this._onShowIconChanged); |
| 72 | + } |
| 73 | + |
| 74 | + async on_desklet_added_to_desktop() { |
| 75 | + this._onDecorationChanged(); |
| 76 | + await this._updateValues(); |
| 77 | + this._setupLayout(); |
| 78 | + this._setStyles(); |
| 79 | + this._setRefreshTimeout(); |
| 80 | + } |
32 | 81 |
|
33 | | - this.fontSize = this.settings.getValue("fontSize") || 20; |
34 | | - this.colorLabel = this.settings.getValue("colorLabel") || "rgb(51, 209, 122)"; |
35 | | - this._timeout = null; |
| 82 | + on_desklet_removed() { |
| 83 | + if (this._refreshTimeoutId) { |
| 84 | + Mainloop.source_remove(this._refreshTimeoutId); |
| 85 | + this._refreshTimeoutId = null; |
| 86 | + } |
| 87 | + if (this._clockSettingsId) { |
| 88 | + this._desktop_settings.disconnect(this._clockSettingsId); |
| 89 | + this._clockSettingsId = 0; |
| 90 | + } |
| 91 | + if (this.settings && !this._isReloading) { |
| 92 | + this.settings.finalize(); |
| 93 | + } |
| 94 | + } |
36 | 95 |
|
37 | | - this.setHeader(_("System Uptime")); |
38 | | - this.updateDecoration(); |
39 | | - this.setupLayout(); |
40 | | - this.updateValues(); |
| 96 | + on_desklet_reloaded() { |
| 97 | + this._isReloading = true; |
41 | 98 | } |
42 | 99 |
|
43 | | - setupLayout() { |
| 100 | + _setupLayout() { |
44 | 101 | // Create labels for uptime |
45 | | - this.uptimeLabel = this.createLabel(_("Uptime:") + " ", this.colorLabel); |
46 | | - this.uptimeValue = this.createLabel(_("Loading...")); |
| 102 | + this._uptimeLabel = new St.Label({ text: _("Uptime:") + " " }); |
| 103 | + this._uptimeValue = new St.Label({ text: this._currentUptimeText }); |
47 | 104 |
|
48 | | - const uptimeRow = this.createRow([this.uptimeLabel, this.uptimeValue]); |
| 105 | + const uptimeRow = new St.BoxLayout(); |
| 106 | + uptimeRow.add_child(this._uptimeLabel); |
| 107 | + uptimeRow.add_child(this._uptimeValue); |
49 | 108 |
|
50 | 109 | // Create labels for startup time |
51 | | - this.startTimeLabel = this.createLabel(_("Start time:") + " ", this.colorLabel); |
52 | | - this.startupValue = this.createLabel(_("Loading...")); |
| 110 | + this._startTimeLabel = new St.Label({ text: _("Start time:") + " " }); |
| 111 | + this._startupValue = new St.Label({ text: this._currentStartupText }); |
53 | 112 |
|
54 | | - const startupRow = this.createRow([this.startTimeLabel, this.startupValue]); |
| 113 | + const startupRow = new St.BoxLayout(); |
| 114 | + startupRow.add_child(this._startTimeLabel); |
| 115 | + startupRow.add_child(this._startupValue); |
55 | 116 |
|
56 | 117 | // Combine all into the main container |
57 | | - const contentBox = new St.BoxLayout({ vertical: true }); |
58 | | - contentBox.set_style("margin-left: 0.5em;"); |
59 | | - contentBox.add_child(startupRow); |
60 | | - contentBox.add_child(uptimeRow); |
61 | | - |
62 | | - this.container = new St.BoxLayout(); |
63 | | - this.container.add_child(contentBox); |
64 | | - |
65 | | - Mainloop.idle_add(() => { |
66 | | - const computedHeight = contentBox.get_height(); |
| 118 | + const labelBox = new St.BoxLayout({ vertical: true, y_align: St.Align.MIDDLE }); |
| 119 | + labelBox.set_style("margin-left: 0.5em;"); |
| 120 | + labelBox.add_child(startupRow); |
| 121 | + labelBox.add_child(uptimeRow); |
| 122 | + |
| 123 | + const container = new St.BoxLayout({ y_align: St.Align.MIDDLE }); |
| 124 | + |
| 125 | + if (this.showIcon) { |
| 126 | + this._clockIcon = new St.Icon({ |
| 127 | + gicon: Gio.icon_new_for_string(`${this.metadata.path}/icons/clock.svg`), |
| 128 | + icon_size: 5 * 16 * this.scaleSize, |
| 129 | + }); |
| 130 | + this._iconBox = new St.Bin({ child: this._clockIcon }); |
| 131 | + container.add_child(this._iconBox); |
| 132 | + } |
67 | 133 |
|
68 | | - const clockIcon = this.getImageAtScale(`${this.metadata.path}/clock.svg`, computedHeight, computedHeight); |
| 134 | + container.add_child(labelBox); |
69 | 135 |
|
70 | | - this.container.insert_child_below(clockIcon, contentBox); |
71 | | - clockIcon.queue_relayout(); |
| 136 | + this.setContent(container); |
| 137 | + } |
72 | 138 |
|
73 | | - return false; |
74 | | - }); |
| 139 | + _setStyles() { |
| 140 | + const valueStyle = `font-size: ${1.5 * this.scaleSize}em;`; |
| 141 | + const labelStyle = `${valueStyle} color: ${this.labelColor};`; |
| 142 | + const iconStyle = `width: ${3 * this.scaleSize}em; height: ${3 * this.scaleSize}em;`; |
75 | 143 |
|
76 | | - this.setContent(this.container); |
77 | | - } |
| 144 | + // Uptime |
| 145 | + this._uptimeLabel.set_style(labelStyle); |
| 146 | + this._startTimeLabel.set_style(labelStyle); |
78 | 147 |
|
79 | | - createLabel(text, color = "inherit") { |
80 | | - return new St.Label({ |
81 | | - text, |
82 | | - y_align: St.Align.START, |
83 | | - style: `font-size: ${this.fontSize}px; color: ${color};`, |
84 | | - }); |
85 | | - } |
| 148 | + // Values |
| 149 | + this._uptimeValue.set_style(valueStyle); |
| 150 | + this._startupValue.set_style(valueStyle); |
86 | 151 |
|
87 | | - createRow(children) { |
88 | | - const row = new St.BoxLayout(); |
89 | | - children.forEach(child => row.add_child(child)); |
90 | | - return row; |
| 152 | + // Icon |
| 153 | + this._clockIcon.set_style(iconStyle); |
| 154 | + this._iconBox.set_style(iconStyle); |
91 | 155 | } |
92 | 156 |
|
93 | | - updateUptime() { |
94 | | - let uptimeInSeconds = 0; |
| 157 | + async _fetchUptimeText() { |
95 | 158 | try { |
96 | | - const [result, out] = GLib.spawn_command_line_sync("awk '{print $1}' /proc/uptime"); |
97 | | - if (!result || !out) throw new Error("Could not get system uptime."); |
98 | | - uptimeInSeconds = parseFloat(out.toString().trim()); |
| 159 | + // Read uptime in seconds from /proc/uptime |
| 160 | + const contents = await getFileContents("/proc/uptime"); |
| 161 | + const uptimeInSeconds = parseFloat(ByteArray.toString(contents).split(" ")[0]); |
99 | 162 |
|
100 | 163 | if (this.showUptimeInDays) { |
| 164 | + // Convert uptime to days, hours, and minutes |
101 | 165 | const days = Math.floor(uptimeInSeconds / 86400); |
102 | 166 | const hours = Math.floor((uptimeInSeconds % 86400) / 3600); |
103 | 167 | const minutes = Math.floor(((uptimeInSeconds % 86400) % 3600) / 60); |
104 | 168 |
|
105 | | - this.uptimeValue.set_text(`${days} ${_("days")} ${hours} ${_("hrs")} ${minutes} ${_("min")}`); |
| 169 | + return `${days} ${_("days")} ${hours} ${_("hrs")} ${minutes} ${_("min")}`; |
106 | 170 | } else { |
| 171 | + // Hours can be more than 24 |
107 | 172 | const hours = Math.floor(uptimeInSeconds / 3600); |
108 | 173 | const minutes = Math.floor((uptimeInSeconds % 3600) / 60); |
109 | 174 |
|
110 | | - this.uptimeValue.set_text(`${hours} ${_("hours")} ${minutes} ${_("minutes")}`); |
| 175 | + return `${hours} ${_("hours")} ${minutes} ${_("minutes")}`; |
111 | 176 | } |
112 | 177 | } catch (error) { |
113 | | - this.uptimeValue.set_text("Error"); |
114 | | - global.logError(`${UUID}: ${error.message}`); |
| 178 | + global.logError(`${UUID} Error: ${error.message}`); |
| 179 | + return _("Error"); |
115 | 180 | } |
116 | 181 | } |
117 | 182 |
|
118 | | - getStartupTime() { |
| 183 | + async _fetchStartupTimeText() { |
119 | 184 | try { |
120 | | - const [result, out] = GLib.spawn_command_line_sync("uptime -s"); |
121 | | - if (!result || !out) throw new Error("Could not get system startup time."); |
| 185 | + // Read startup time from /proc/stat (btime) |
| 186 | + const contents = await getFileContents("/proc/stat"); |
| 187 | + |
| 188 | + // Search for the line starting with "btime " and parse the timestamp |
| 189 | + const lines = ByteArray.toString(contents).split("\n"); |
| 190 | + let btime = 0; |
| 191 | + for (const line of lines) { |
| 192 | + if (line.startsWith("btime ")) { |
| 193 | + btime = parseInt(line.split(/\s+/)[1]); |
| 194 | + break; |
| 195 | + } |
| 196 | + } |
| 197 | + if (!btime) throw new Error("Could not get system startup time."); |
122 | 198 |
|
123 | | - const dateTime = out.toString().split(" "); |
124 | | - const date = dateTime[0].split("-"); |
| 199 | + const dateTime = GLib.DateTime.new_from_unix_local(btime); |
| 200 | + const use24h = this._desktop_settings.get_boolean("clock-use-24h"); |
| 201 | + const timeFormat = use24h ? "%H:%M" : "%-l:%M %p"; |
125 | 202 |
|
126 | 203 | if (this.showStartDate) { |
127 | | - this.startupValue.set_text(date[2] + "." + date[1] + "." + date[0] + ", " + dateTime[1].trim()); |
| 204 | + // Show date and time |
| 205 | + return dateTime.format("%x") + ", " + dateTime.format(timeFormat); |
128 | 206 | } else { |
129 | | - this.startupValue.set_text(dateTime[1].trim()); |
| 207 | + // Show only time |
| 208 | + return dateTime.format(timeFormat); |
130 | 209 | } |
131 | 210 | } catch (error) { |
132 | | - this.startupValue.set_text("Error"); |
133 | 211 | global.logError(`${UUID}: ${error.message}`); |
| 212 | + return _("Error"); |
| 213 | + } |
| 214 | + } |
| 215 | + |
| 216 | + async _updateUptime() { |
| 217 | + this._currentUptimeText = await this._fetchUptimeText(); |
| 218 | + if (this._uptimeValue) { |
| 219 | + this._uptimeValue.set_text(this._currentUptimeText); |
134 | 220 | } |
135 | 221 | } |
136 | 222 |
|
137 | | - updateValues() { |
138 | | - this.updateUptime(); |
139 | | - this.getStartupTime(); |
140 | | - if (this._timeout) Mainloop.source_remove(this._timeout); |
141 | | - this._timeout = Mainloop.timeout_add_seconds(60, () => this.updateValues()); |
| 223 | + async _updateStartupTime() { |
| 224 | + this._currentStartupText = await this._fetchStartupTimeText(); |
| 225 | + if (this._startupValue) { |
| 226 | + this._startupValue.set_text(this._currentStartupText); |
| 227 | + } |
142 | 228 | } |
143 | 229 |
|
144 | | - onSettingsChanged() { |
145 | | - this.setupLayout(); |
146 | | - this.updateValues(); |
| 230 | + async _updateValues() { |
| 231 | + await this._updateStartupTime(); |
| 232 | + await this._updateUptime(); |
147 | 233 | } |
148 | 234 |
|
149 | | - updateDecoration() { |
150 | | - this.metadata["prevent-decorations"] = this.hideDecorations; |
151 | | - this._updateDecoration(); |
| 235 | + _setRefreshTimeout() { |
| 236 | + this._refreshTimeoutId = Mainloop.timeout_add_seconds(60, () => { |
| 237 | + this._updateValues(); |
| 238 | + return true; |
| 239 | + }); |
152 | 240 | } |
153 | 241 |
|
154 | | - on_desklet_removed() { |
155 | | - if (this._timeout) Mainloop.source_remove(this._timeout); |
| 242 | + _onDecorationChanged() { |
| 243 | + this.metadata["prevent-decorations"] = this.hideDecorations; |
| 244 | + this._updateDecoration(); |
156 | 245 | } |
157 | 246 |
|
158 | | - getImageAtScale(imageFileName, width, height) { |
159 | | - const pixBuf = GdkPixbuf.Pixbuf.new_from_file_at_size(imageFileName, width, height); |
160 | | - const image = new Clutter.Image(); |
161 | | - image.set_data( |
162 | | - pixBuf.get_pixels(), |
163 | | - pixBuf.get_has_alpha() ? Cogl.PixelFormat.RGBA_8888 : Cogl.PixelFormat.RGBA_888, |
164 | | - width, |
165 | | - height, |
166 | | - pixBuf.get_rowstride() |
167 | | - ); |
168 | | - |
169 | | - const actor = new Clutter.Actor({ width, height }); |
170 | | - actor.set_content(image); |
171 | | - return actor; |
| 247 | + _onShowIconChanged() { |
| 248 | + this._setupLayout(); |
| 249 | + this._setStyles(); |
172 | 250 | } |
173 | 251 | } |
174 | 252 |
|
|
0 commit comments