I wrote a cron job!

This is a continuation of Finally switched to Linux Mint (https://cplusplus.com/forum/lounge/285724/).

Mint, by default, is constantly popping up notifications in the corner whenever my Bluetooth mouse connects and disconnects. It is kind of annoying, but I left them alone because they also reported the mouse’s battery status, which is useful.

But it did mean I have to regularly clear out the list of notifications for just the spam from the mouse connect/disconnect.

So I did this:

❶ Stop the obnoxious notifications

Right-click the Bluetooth icon in the Notification Tray, select “Plugins” and uncheck the “Connection Notifier”.


❷ Found which filename the upower tool has assigned to my mouse:

% upower -e
/org/freedesktop/UPower/devices/mouse_dev_D7_80_86_CB_3C_9F
/org/freedesktop/UPower/devices/DisplayDevice

%


❸ Wrote a little Tcl script to check the battery status and post a notification if it is 30% or less.

I stuck it in my local bin directory, but it could have been put anywhere.

~/bin/check-mouse-battery
1
2
3
4
5
6
7
8
9
10
11
#! /usr/bin/env tclsh

set s [exec -ignorestderr -- upower -i /org/freedesktop/UPower/devices/mouse_dev_D7_80_86_CB_3C_9F]
regexp -- {percentage:\s+(\d+)%} $s - percentage

set percentage "1$percentage"
set percentage [expr {$percentage - 100}]

if {$percentage < 31} {
  exec -ignorestderr -- notify-send "Mouse Battery" "Power level at ${percentage}%" --icon=dialog-info
}


❹ Added an hourly check for the battery status.

That might be overkill (daily might suffice), but getting a new battery in the mouse is something not to put off, so I figured this particular notification can be extra-annoying.

I also added the check to every time the PC starts.

To edit your user-local cron tasks, use the crontab facility:

% crontab -e

Chances are you will be creating a new user-local file, and crontab will tell you all about it, but ultimately you’ll get a blank file with a bunch of verbiage in #comments at the top. Ignore those and add the lines we want.

The first line just means to check at minute=0 of every hour of every day of every etc.

1
2
3
4

0 * * * * /home/michael/bin/check-mouse-battery
@reboot /home/michael/bin/check-mouse-battery

Saved, and done!

Oh, if you are unfamiliar with terminal text editors, choosing Nano is a pretty safe place to start. I prefer Vim, lol, but you can certainly choose another by assigning an editor environment variable: % EDITOR=gedit crontab -e.


Now, whenever my mouse battery starts getting suspiciously low, I’ll get a useful popup notification telling me how much power it has left. At 30% it really means “change me now”, so that works.

I’m kinda pleased with myself. 🙂
Last edited on
nice!
I hate battery powered gear. Don't have any at all currently, but the battery report is worse than a windows 3.0 progress bar for accuracy:

hours left reported
20 15 10 5 1 .5

03 3.5 4 4.5 dies.
actual time elapsed

The mouse & kbd are harmless, they just stop working, and you fix it, and they pick up where you left off without a fuss. Many other things... can't seem to manage that.
I usually agree, but mice on wires have bothered me enough times that I prefer wireless. (I don’t need anything tugging back on me when I move my mouse. And it is awesomely convenient to be able to pick it up and set it anywhere in the room and still control things like TV or youtube or music playback, etc.)

Also, my wife was annoyed by how loud my last mouse was, so I got the Logitech POP mouse (https://www.logitech.com/en-us/shop/p/pop-wireless-mouse.910-007408).

I’ll never buy anything else if I can avoid it. Thing is whisper quiet, three button + button wheel, easy to clean, and battery lasts months. And the gaudy yellow has grown on me too.


It _is_ annoying to have the battery suddenly die, though, so a little warning is all I need.


Sorry for the commercial, but this is one product I like enough to endorse (like that means anything to anyone, lol).
I don't like the tugging/scratching of the wire either, but rather have a guide/holder for the wire similar (in principle) to https://www.iconicmicro.com/cdn/shop/files/9h.n2agb.ace-06_700x.jpg


On RHEL distro the cron has directories:
/etc/cron.monthly /etc/cron.weekly /etc/cron.daily /etc/cron.hourly
and scripts within them are executed monthly|weekly|daily|hourly.
These are naturally run as root, so less useful for OP use, but comfy for some routines.

Oh my, my system seems to have a /etc/cron.daily/google-chrome
It describes itself as:
# This script is part of the google-chrome package.
#
# It creates the repository configuration file for package updates, since
# we cannot do this during the google-chrome installation since the repository
# is locked.

Eerie. I'm not quite convinced that such shenanigan is necessary.
Perhaps not, but remember, if it exists, there is usually a good reason. Much like finding some really weird sign somewhere and realizing it exists because of some very specific person’s actions. I suspect that someone, somewhere, with a broken system was surprised when Chrome didn’t behave as expected. So either that winds up everywhere as a stub (which is what it looks like for you) or your system is configured in a way that triggered the installer to put it there.

I had, until now, always been kind of afraid of cron (thought it was some deep magic or something), but it turns out it is really very easy to use.
it is really very easy to use.

Indeed. The most likely gotcha is that the environment (e.g. what is on the PATH) is likely to differ from what you have on your interactive session.

The cron can also email job output to you.


I saw on some system-provided cron job:
#!/bin/bash

LOCKFILE=/run/lock/something.lock

# the lockfile is not meant to be perfect, it's just in case the
# two maintain cron scripts get run close to each other to keep
# them from stepping on each other's toes.
[ -f $LOCKFILE ] && exit 0

trap "{ rm -f $LOCKFILE ; exit 255; }" EXIT
touch $LOCKFILE

# actual commands of the job
# ...

Which -- registering actions/callbacks on "hooks" -- is a quite interesting feature of the shell.
While I’m at it, something that always drives me bonkers is the need to clean the keyboard without frobbing something I’d prefer not to. (If you have a cat or a kid that wants to hammer your keyboard too, then this is for you as well.)

clean-kbd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
#! /usr/bin/env wish

# 2025 Michael Thomas Greer

# Little utility to enable/disable your keyboard
# so you can clean it (or so the cat can sit on it).
#
# Beware: if you close the program then you will have to run
# it again by navigating through your user folders and double
# clicking it in Nemo/whatever your file explorer is called.


package require Tk 8.6


# Colors meant to work with "Clam" theme
array set colors {
  blue.active #77BBFF
  red.normal  #FFAAAA
  red.active  #FF9797
  hr          #AAAAAA
  red.error   #FF5555
}


#--------------------------------------------------------------------------------------------------
proc extract.device.name.and.id {s nameVar idVar} {
  #
  # Arguments
  #   $s : a line of text from `xinput` command
  #
  # Returns
  #   True iff $s indicates a keyboard device,
  #   and sets $$nameVar and $$idVar to the name and ID of the device
  #
  upvar 1 $nameVar name $idVar id

  # Verify one of:
  #   ↳ Device Name   id=99
  #   ~ Device Name   id=99
  # (Except xinput uses fancy "TILDE OPERATOR" character instead of '~')
  if {![regexp -- {[↳\u223C]\s+(.+)\s+id=(\d+)} $s - name id]} {return 0}
  set name [string trim $name]

  # If it is a "floating slave"...
  if {[regexp -- {floating\s+slave} $s]} {
    # Return true if it is actually a keyboard, else false
    return [regexp -- {(?w)^KeyClass} [exec -- /usr/bin/xinput --query-state $id]]
  }

  # Else return whether device is a keyboard
  regexp -- {slave\s+keyboard} $s
}


#--------------------------------------------------------------------------------------------------
proc Initialize.GUI {} {

  # dictionary (id --> device name)
  global keyboards colors

  # array (id --> enabled)
  global {}

  wm title . {Clean Keyboard Helper}

  ttk::style theme use clam

  ttk::style map My.TButton       -background [list active $colors(blue.active)]
  ttk::style map My.TCheckbutton  -background [list active $colors(blue.active)]
  ttk::style map Err.TCheckbutton -background [list active $colors(red.active) !active $colors(red.normal)]

  # For each device
  foreach s [split [exec -- /usr/bin/xinput] \n] {

    # If it is a keyboard device
    if {[extract.device.name.and.id $s name id]} {

      # But not the popup virtual keyboard
      if {$name ne {Video Bus}} {

        # Add it to our dictionary of (ID --> device name)
        dict set keyboards $id $name
      }
    }
  }

  # Sort the devices by name
  set keyboards [lsort -dictionary -stride 2 -index 1 $keyboards]

  # Add list of devices
  pack [frame .btsp -relief flat -height 3] -expand yes -fill both
  dict for {id name} $keyboards {
    pack [ttk::checkbutton .c$id \
        -text $name -takefocus 0 -var ($id) -command "click $id" -style My.TCheckbutton] \
      -expand yes -fill both -padx 5
  }
  update.states
  pack [frame .bbsp -relief flat -height 3] -expand yes -fill both

  # separator
  pack [frame .hr -relief flat -height 1 -background $colors(hr)] -expand yes -fill both

  # Enable All / Disable All buttons
  pack [frame .actions -relief flat] -expand yes -fill both
  pack [ttk::button .actions.enable  -text {Enable All}  -takefocus 0 -style My.TButton -cursor hand2 -command {all enable}]  -side left -expand yes -fill both -padx 5 -pady 5
  pack [ttk::button .actions.disable -text {Disable All} -takefocus 0 -style My.TButton -cursor hand2 -command {all disable}] -side left -expand yes -fill both         -pady 5
  pack [frame .actions.endspace -relief flat -width 5] -expand no -fill both

  # User can press 'Esc' to quit
  bind . <Escape> exit
}


#--------------------------------------------------------------------------------------------------
proc update.states {} {
  #
  # Queries the current enabled/disabled status of each ID/device
  # in the $keyboards dictionary and updates the checkboxes to match
  #
  global keyboards {}
  foreach id [dict keys $keyboards] {
    set s [exec -- /usr/bin/xinput --list-props $id]
    regexp -- {Device Enabled \(\d+\):\s*(\d+)} $s - enabled
    set ($id) $enabled
  }
}


#--------------------------------------------------------------------------------------------------
proc no.message {} {
  catch {
    after cancel no.message
    pack forget .message
    destroy .message
  }
}


proc message text {
  # Display a 2.5 second message at the bottom of the window
  no.message
  pack [label .message \
      -text "<!> $text" -anchor w -background $::colors(red.error) -padx 5] \
    -expand yes -fill both
  after 2500 no.message
}


#--------------------------------------------------------------------------------------------------
proc xinput {enable/disable id} {
  exec -- /usr/bin/xinput --${enable/disable} $id
}


#--------------------------------------------------------------------------------------------------
proc click id {
  #
  # A device checkbox was toggled by the user
  #
  global keyboards {}

  # $($id) has the NEW (post-click) state
  set state $($id)

  try {
    xinput [lindex {disable enable} $state] $id
    .c$id config -style My.TCheckbutton

  } on error {} {
    set ($id) [expr {!$state}]
    .c$id config -style Err.TCheckbutton
    message "Could not [lindex {disable enable} $state] [dict get $keyboards $id]!"
  }
}


#--------------------------------------------------------------------------------------------------
proc all {enable/disable} {
  #
  # The "Enable All" or "Disable All" button was clicked
  #
  set error_count 0

  foreach id [dict keys $::keyboards] {
    try {
      xinput ${enable/disable} $id
      .c$id config -style My.TCheckbutton
      set ::($id) [expr {${enable/disable} eq "enable"}]
    } on error {} {
      incr error_count
      .c$id config -style Err.TCheckbutton
    }
  }

  if {$error_count} {
    message "Could not ${enable/disable} one or more keyboard devices"
  }
}


#--------------------------------------------------------------------------------------------------
Initialize.GUI
#--------------------------------------------------------------------------------------------------


Requires stuff you should already have. But if you don’t:

  • Tcl/Tk 8.6 wish (Tk shell):
    sudo apt install tcl8.6 tk8.6

  • xinput:
    sudo apt install xinput

Copy the script to somewhere in your path (I have it at ~/bin/clean-kbd, in the directory for my user-local tools) and make sure to chmod +x ~/bin/clean-kbd so that it is executable.

If you want you can also right-click your start menu button and select “Edit Menu” to add it into your menus as an “Accessory” program. (I have not created a pretty icon for it, alas.)

Now I can turn the keyboard off and clean it, then turn it back on — all without having to power down the PC!

I know that others have created similar programs, such as https://github.com/brandizzi/input-device-indicator/, but mine is simpler, I guess, if not as cool.

I hope someone enjoys, at least.
Last edited on

How to add an application to the start menu (the good way)!


Following on the last, I decided I wanted to be able to start my app without having to be at the shell prompt.

Sooo... the first thing I did was find a nice icon. I like this one, yrmv:
https://iconduck.com/icons/152922/xfce4-keyboard-svg

I downloaded it to ~/.local/share/icons/hicolor/scalable/clean-kbd.svg. This is the correct place to put a scalable (.svg) icon for the app!

Next I created a .desktop file:

~/.local/share/applications/clean-kbd.desktop
1
2
3
4
5
6
7
8
9
10
[Desktop Entry]
Name=Clean Keyboard Helper
Exec=/home/michael/bin/clean-kbd
Comment=Turn off the keyboard so you can clean it
Terminal=false
PrefersNonDefaultGPU=false
Icon=clean-kbd
Type=Application
Keywords=Accessories;Keyboard;Clean;
Categories=Utility

A few of those things are not so important, but Name, Exec, Icon, Type, and Categories are essential.

If you are unsure whether a .desktop file you create is valid, you can check it (from the terminal) using desktop-file-validate clean-kbd.desktop, substituting the correct name of the desktop file you create.

Next, from the terminal, execute sudo update-desktop-database.

Now you can see “Clean Keyboard Helper” with its pretty icon in the start menu under “Accessories”.


Okay, that’s it for now.
Last edited on
Nice, but one could also unplug the keyboard.


I seem to have that validator on RPM-based system:
dnf -q provides *bin/desktop-file-validate
desktop-file-utils-0.26-6.el9.x86_64 : Utilities for manipulating .desktop files
Repo        : @System
Matched from:
Other       : *bin/desktop-file-validate

and
# rpm -qi desktop-file-utils | grep -A10 Description
Description :
.desktop files are used to describe an application for inclusion in
GNOME or KDE menus.  This package contains desktop-file-validate which
checks whether a .desktop file complies with the specification at
http://www.freedesktop.org/standards/, and desktop-file-install
which installs a desktop file to the standard directory, optionally
fixing it up in the process.

The freedesktop.org offers standards. It is nice when standards are used.
one could also unplug the keyboard
No one wants to climb under their desk to unplug the keyboard. That’s why the things never get cleaned.

It is nice when standards are used.
Absolutely! .desktop files have been around at least 30 years, so there is no reason _every_ linux distro shouldn’t fully support them.
Last edited on
I've seen:
- (flight)simulator enthusiasts, who have many USB devices/controllers, to recommend powered USB hubs with individual on/off switches for each port. Presumably not "under the table"
- Professors climb under their desks. Yes, I'd rather watch C-beams glitter in the dark near the Tannhäuser Gate.
my early work was in unmanned air vehicles. We had sims, of course, and that was before usb, so you had extra serial ports on the computers for all the extra crap... foot pedals, button panel with a hat (stupid, our stuff was not armed with missiles), throttle, yoke, and I forget what all else. Time I quit we had all we needed off a PC version of an xbox controller on one usb port, and a xbox LIKE flight specific controller with trims and no springs on one of the joysticks (throttle). For the younguns here (looks at zap) a serial port cable had to be screwed into the computer to secure it...

Under the table is for suckers these days. My pc has the USB on the front top and a couple on the back. But all mine go in that top front part. Ive done my share of crawling on the floor back in the day, and that is over and done.
Last edited on
Registered users can post here. Sign in or register to post.