New HW sensors script
* Rewritten in python * Report CoreTemps first then others * Wrap into two columns as necessary (if the window is big enough)
This commit is contained in:
parent
a84b2dfef6
commit
96ef259b4c
6 changed files with 420 additions and 44 deletions
35
.bin/Scripts/borrowed/sensors-README.md
Normal file
35
.bin/Scripts/borrowed/sensors-README.md
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
sensors.py
|
||||||
|
==========
|
||||||
|
python bindings using ctypes for libsensors3 of the [lm-sensors project](https://github.com/groeck/lm-sensors). The code was written against libsensors 3.3.4.
|
||||||
|
|
||||||
|
For documentation of the low level API see [sensors.h](https://github.com/groeck/lm-sensors/blob/master/lib/sensors.h). For an example of the high level API see [example.py](example.py).
|
||||||
|
|
||||||
|
For a GUI application that displays the sensor readings and is based on this library, take a look at [sensors-unity](https://launchpad.net/sensors-unity).
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
* Full access to low level libsensors3 API
|
||||||
|
* High level iterator API
|
||||||
|
* unicode handling
|
||||||
|
* Python2 and Python3 compatible
|
||||||
|
|
||||||
|
Licensing
|
||||||
|
---------
|
||||||
|
LGPLv2 (same as libsensors3)
|
||||||
|
|
||||||
|
Usage Notes
|
||||||
|
-----------
|
||||||
|
As Python does not support call by reference for primitive types some of the libsensors API had to be adapted:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# nr is changed by refrence in the C API
|
||||||
|
chip_name, nr = sensors.get_detected_chips(None, nr)
|
||||||
|
|
||||||
|
# returns the value. throws on error
|
||||||
|
val = sensors.get_value(chip, subfeature_nr)
|
||||||
|
```
|
||||||
|
|
||||||
|
Missing Features (pull requests are welcome):
|
||||||
|
* `sensors_subfeature_type` enum
|
||||||
|
* `sensors_get_subfeature`
|
||||||
|
* Error handlers
|
||||||
236
.bin/Scripts/borrowed/sensors.py
Normal file
236
.bin/Scripts/borrowed/sensors.py
Normal file
|
|
@ -0,0 +1,236 @@
|
||||||
|
"""
|
||||||
|
@package sensors.py
|
||||||
|
Python Bindings for libsensors3
|
||||||
|
|
||||||
|
use the documentation of libsensors for the low level API.
|
||||||
|
see example.py for high level API usage.
|
||||||
|
|
||||||
|
@author: Pavel Rojtberg (http://www.rojtberg.net)
|
||||||
|
@see: https://github.com/paroj/sensors.py
|
||||||
|
@copyright: LGPLv2 (same as libsensors) <http://opensource.org/licenses/LGPL-2.1>
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ctypes import *
|
||||||
|
import ctypes.util
|
||||||
|
|
||||||
|
_libc = cdll.LoadLibrary(ctypes.util.find_library("c"))
|
||||||
|
# see https://github.com/paroj/sensors.py/issues/1
|
||||||
|
_libc.free.argtypes = [c_void_p]
|
||||||
|
|
||||||
|
_hdl = cdll.LoadLibrary(ctypes.util.find_library("sensors"))
|
||||||
|
|
||||||
|
version = c_char_p.in_dll(_hdl, "libsensors_version").value.decode("ascii")
|
||||||
|
|
||||||
|
class bus_id(Structure):
|
||||||
|
_fields_ = [("type", c_short),
|
||||||
|
("nr", c_short)]
|
||||||
|
|
||||||
|
class chip_name(Structure):
|
||||||
|
_fields_ = [("prefix", c_char_p),
|
||||||
|
("bus", bus_id),
|
||||||
|
("addr", c_int),
|
||||||
|
("path", c_char_p)]
|
||||||
|
|
||||||
|
class feature(Structure):
|
||||||
|
_fields_ = [("name", c_char_p),
|
||||||
|
("number", c_int),
|
||||||
|
("type", c_int)]
|
||||||
|
|
||||||
|
# sensors_feature_type
|
||||||
|
IN = 0x00
|
||||||
|
FAN = 0x01
|
||||||
|
TEMP = 0x02
|
||||||
|
POWER = 0x03
|
||||||
|
ENERGY = 0x04
|
||||||
|
CURR = 0x05
|
||||||
|
HUMIDITY = 0x06
|
||||||
|
MAX_MAIN = 0x7
|
||||||
|
VID = 0x10
|
||||||
|
INTRUSION = 0x11
|
||||||
|
MAX_OTHER = 0x12
|
||||||
|
BEEP_ENABLE = 0x18
|
||||||
|
|
||||||
|
class subfeature(Structure):
|
||||||
|
_fields_ = [("name", c_char_p),
|
||||||
|
("number", c_int),
|
||||||
|
("type", c_int),
|
||||||
|
("mapping", c_int),
|
||||||
|
("flags", c_uint)]
|
||||||
|
|
||||||
|
_hdl.sensors_get_detected_chips.restype = POINTER(chip_name)
|
||||||
|
_hdl.sensors_get_features.restype = POINTER(feature)
|
||||||
|
_hdl.sensors_get_all_subfeatures.restype = POINTER(subfeature)
|
||||||
|
_hdl.sensors_get_label.restype = c_void_p # return pointer instead of str so we can free it
|
||||||
|
_hdl.sensors_get_adapter_name.restype = c_char_p # docs do not say whether to free this or not
|
||||||
|
_hdl.sensors_strerror.restype = c_char_p
|
||||||
|
|
||||||
|
### RAW API ###
|
||||||
|
MODE_R = 1
|
||||||
|
MODE_W = 2
|
||||||
|
COMPUTE_MAPPING = 4
|
||||||
|
|
||||||
|
def init(cfg_file = None):
|
||||||
|
file = _libc.fopen(cfg_file.encode("utf-8"), "r") if cfg_file is not None else None
|
||||||
|
|
||||||
|
if _hdl.sensors_init(file) != 0:
|
||||||
|
raise Exception("sensors_init failed")
|
||||||
|
|
||||||
|
if file is not None:
|
||||||
|
_libc.fclose(file)
|
||||||
|
|
||||||
|
def cleanup():
|
||||||
|
_hdl.sensors_cleanup()
|
||||||
|
|
||||||
|
def parse_chip_name(orig_name):
|
||||||
|
ret = chip_name()
|
||||||
|
err= _hdl.sensors_parse_chip_name(orig_name.encode("utf-8"), byref(ret))
|
||||||
|
|
||||||
|
if err < 0:
|
||||||
|
raise Exception(strerror(err))
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def strerror(errnum):
|
||||||
|
return _hdl.sensors_strerror(errnum).decode("utf-8")
|
||||||
|
|
||||||
|
def free_chip_name(chip):
|
||||||
|
_hdl.sensors_free_chip_name(byref(chip))
|
||||||
|
|
||||||
|
def get_detected_chips(match, nr):
|
||||||
|
"""
|
||||||
|
@return: (chip, next nr to query)
|
||||||
|
"""
|
||||||
|
_nr = c_int(nr)
|
||||||
|
|
||||||
|
if match is not None:
|
||||||
|
match = byref(match)
|
||||||
|
|
||||||
|
chip = _hdl.sensors_get_detected_chips(match, byref(_nr))
|
||||||
|
chip = chip.contents if bool(chip) else None
|
||||||
|
return chip, _nr.value
|
||||||
|
|
||||||
|
def chip_snprintf_name(chip, buffer_size=200):
|
||||||
|
"""
|
||||||
|
@param buffer_size defaults to the size used in the sensors utility
|
||||||
|
"""
|
||||||
|
ret = create_string_buffer(buffer_size)
|
||||||
|
err = _hdl.sensors_snprintf_chip_name(ret, buffer_size, byref(chip))
|
||||||
|
|
||||||
|
if err < 0:
|
||||||
|
raise Exception(strerror(err))
|
||||||
|
|
||||||
|
return ret.value.decode("utf-8")
|
||||||
|
|
||||||
|
def do_chip_sets(chip):
|
||||||
|
"""
|
||||||
|
@attention this function was not tested
|
||||||
|
"""
|
||||||
|
err = _hdl.sensors_do_chip_sets(byref(chip))
|
||||||
|
if err < 0:
|
||||||
|
raise Exception(strerror(err))
|
||||||
|
|
||||||
|
def get_adapter_name(bus):
|
||||||
|
return _hdl.sensors_get_adapter_name(byref(bus)).decode("utf-8")
|
||||||
|
|
||||||
|
def get_features(chip, nr):
|
||||||
|
"""
|
||||||
|
@return: (feature, next nr to query)
|
||||||
|
"""
|
||||||
|
_nr = c_int(nr)
|
||||||
|
feature = _hdl.sensors_get_features(byref(chip), byref(_nr))
|
||||||
|
feature = feature.contents if bool(feature) else None
|
||||||
|
return feature, _nr.value
|
||||||
|
|
||||||
|
def get_label(chip, feature):
|
||||||
|
ptr = _hdl.sensors_get_label(byref(chip), byref(feature))
|
||||||
|
val = cast(ptr, c_char_p).value.decode("utf-8")
|
||||||
|
_libc.free(ptr)
|
||||||
|
return val
|
||||||
|
|
||||||
|
def get_all_subfeatures(chip, feature, nr):
|
||||||
|
"""
|
||||||
|
@return: (subfeature, next nr to query)
|
||||||
|
"""
|
||||||
|
_nr = c_int(nr)
|
||||||
|
subfeature = _hdl.sensors_get_all_subfeatures(byref(chip), byref(feature), byref(_nr))
|
||||||
|
subfeature = subfeature.contents if bool(subfeature) else None
|
||||||
|
return subfeature, _nr.value
|
||||||
|
|
||||||
|
def get_value(chip, subfeature_nr):
|
||||||
|
val = c_double()
|
||||||
|
err = _hdl.sensors_get_value(byref(chip), subfeature_nr, byref(val))
|
||||||
|
if err < 0:
|
||||||
|
raise Exception(strerror(err))
|
||||||
|
return val.value
|
||||||
|
|
||||||
|
def set_value(chip, subfeature_nr, value):
|
||||||
|
"""
|
||||||
|
@attention this function was not tested
|
||||||
|
"""
|
||||||
|
val = c_double(value)
|
||||||
|
err = _hdl.sensors_set_value(byref(chip), subfeature_nr, byref(val))
|
||||||
|
if err < 0:
|
||||||
|
raise Exception(strerror(err))
|
||||||
|
|
||||||
|
### Convenience API ###
|
||||||
|
class ChipIterator:
|
||||||
|
def __init__(self, match = None):
|
||||||
|
self.match = parse_chip_name(match) if match is not None else None
|
||||||
|
self.nr = 0
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
chip, self.nr = get_detected_chips(self.match, self.nr)
|
||||||
|
|
||||||
|
if chip is None:
|
||||||
|
raise StopIteration
|
||||||
|
|
||||||
|
return chip
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if self.match is not None:
|
||||||
|
free_chip_name(self.match)
|
||||||
|
|
||||||
|
def next(self): # python2 compability
|
||||||
|
return self.__next__()
|
||||||
|
|
||||||
|
class FeatureIterator:
|
||||||
|
def __init__(self, chip):
|
||||||
|
self.chip = chip
|
||||||
|
self.nr = 0
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
feature, self.nr = get_features(self.chip, self.nr)
|
||||||
|
|
||||||
|
if feature is None:
|
||||||
|
raise StopIteration
|
||||||
|
|
||||||
|
return feature
|
||||||
|
|
||||||
|
def next(self): # python2 compability
|
||||||
|
return self.__next__()
|
||||||
|
|
||||||
|
class SubFeatureIterator:
|
||||||
|
def __init__(self, chip, feature):
|
||||||
|
self.chip = chip
|
||||||
|
self.feature = feature
|
||||||
|
self.nr = 0
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
subfeature, self.nr = get_all_subfeatures(self.chip, self.feature, self.nr)
|
||||||
|
|
||||||
|
if subfeature is None:
|
||||||
|
raise StopIteration
|
||||||
|
|
||||||
|
return subfeature
|
||||||
|
|
||||||
|
def next(self): # python2 compability
|
||||||
|
return self.__next__()
|
||||||
147
.bin/Scripts/hw-sensors
Executable file
147
.bin/Scripts/hw-sensors
Executable file
|
|
@ -0,0 +1,147 @@
|
||||||
|
#!/bin/python3
|
||||||
|
#
|
||||||
|
## Wizard Kit: Sensor monitoring tool
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Init
|
||||||
|
os.chdir(os.path.dirname(os.path.realpath(__file__)))
|
||||||
|
sys.path.append(os.getcwd())
|
||||||
|
from functions.common import *
|
||||||
|
from borrowed import sensors
|
||||||
|
|
||||||
|
# STATIC VARIABLES
|
||||||
|
COLORS = {
|
||||||
|
'CLEAR': '\033[0m',
|
||||||
|
'RED': '\033[31m',
|
||||||
|
'GREEN': '\033[32m',
|
||||||
|
'YELLOW': '\033[33m',
|
||||||
|
'ORANGE': '\033[31;1m',
|
||||||
|
'BLUE': '\033[34m'
|
||||||
|
}
|
||||||
|
TEMP_LIMITS = {
|
||||||
|
'GREEN': 60,
|
||||||
|
'YELLOW': 70,
|
||||||
|
'ORANGE': 80,
|
||||||
|
'RED': 90,
|
||||||
|
}
|
||||||
|
|
||||||
|
# REGEX
|
||||||
|
REGEX_COLORS = re.compile(r'\033\[\d+;?1?m')
|
||||||
|
|
||||||
|
def color_temp(temp):
|
||||||
|
try:
|
||||||
|
temp = float(temp)
|
||||||
|
except ValueError:
|
||||||
|
return '{YELLOW}{temp}{CLEAR}'.format(temp=temp, **COLORS)
|
||||||
|
if temp > TEMP_LIMITS['RED']:
|
||||||
|
color = COLORS['RED']
|
||||||
|
elif temp > TEMP_LIMITS['ORANGE']:
|
||||||
|
color = COLORS['ORANGE']
|
||||||
|
elif temp > TEMP_LIMITS['YELLOW']:
|
||||||
|
color = COLORS['YELLOW']
|
||||||
|
elif temp > TEMP_LIMITS['GREEN']:
|
||||||
|
color = COLORS['GREEN']
|
||||||
|
elif temp > 0:
|
||||||
|
color = COLORS['BLUE']
|
||||||
|
else:
|
||||||
|
color = COLORS['CLEAR']
|
||||||
|
return '{color}{prefix}{temp:2}°C{CLEAR}'.format(
|
||||||
|
color = color,
|
||||||
|
prefix = '+' if temp>0 else '-',
|
||||||
|
temp = int(temp),
|
||||||
|
**COLORS)
|
||||||
|
|
||||||
|
def get_feature_string(chip, feature):
|
||||||
|
sfs = list(sensors.SubFeatureIterator(chip, feature)) # get a list of all subfeatures
|
||||||
|
label = sensors.get_label(chip, feature)
|
||||||
|
skipname = len(feature.name)+1 # skip common prefix
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
if feature.type == sensors.feature.INTRUSION:
|
||||||
|
vals = [sensors.get_value(chip, sf.number) for sf in sfs]
|
||||||
|
# short path for INTRUSION to demonstrate type usage
|
||||||
|
status = "alarm" if int(vals[0]) == 1 else "normal"
|
||||||
|
print_standard(' {:18} {}'.format(label, status))
|
||||||
|
return
|
||||||
|
|
||||||
|
for sf in sfs:
|
||||||
|
name = sf.name[skipname:].decode("utf-8").strip()
|
||||||
|
val = sensors.get_value(chip, sf.number)
|
||||||
|
if 'alarm' in name:
|
||||||
|
# Skip
|
||||||
|
continue
|
||||||
|
data[name] = color_temp(val)
|
||||||
|
|
||||||
|
main_temp = data.pop('input', None)
|
||||||
|
if main_temp:
|
||||||
|
list_data = []
|
||||||
|
for item in ['max', 'crit']:
|
||||||
|
if item in data:
|
||||||
|
list_data.append('{}: {}'.format(item, data.pop(item)))
|
||||||
|
list_data.extend(
|
||||||
|
['{}: {}'.format(k, v) for k, v in sorted(data.items())])
|
||||||
|
data_str = '{:18} {} ({})'.format(
|
||||||
|
label, main_temp, ', '.join(list_data))
|
||||||
|
else:
|
||||||
|
list_data.extend(sorted(data.items()))
|
||||||
|
list_data = ['{}: {}'.format(item[0], item[1]) for item in list_data]
|
||||||
|
data_str = '{:18} {}'.format(label, ', '.join(list_data))
|
||||||
|
return data_str
|
||||||
|
|
||||||
|
def join_columns(column1, column2, width=55):
|
||||||
|
return '{:<{}}{}'.format(
|
||||||
|
column1,
|
||||||
|
55+len(column1)-len(REGEX_COLORS.sub('', column1)),
|
||||||
|
column2)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
# Prep
|
||||||
|
clear_screen()
|
||||||
|
sensors.init()
|
||||||
|
|
||||||
|
# Get sensor data
|
||||||
|
chip_temps = {}
|
||||||
|
for chip in sensors.ChipIterator():
|
||||||
|
chip_name = '{} ({})'.format(
|
||||||
|
sensors.chip_snprintf_name(chip),
|
||||||
|
sensors.get_adapter_name(chip.bus))
|
||||||
|
chip_temps[chip_name] = [chip_name]
|
||||||
|
for feature in sensors.FeatureIterator(chip):
|
||||||
|
chip_temps[chip_name].append(get_feature_string(chip, feature))
|
||||||
|
chip_temps[chip_name].append('')
|
||||||
|
|
||||||
|
# Sort chips
|
||||||
|
sensor_temps = []
|
||||||
|
for chip in [k for k in sorted(chip_temps.keys()) if 'coretemp' in k]:
|
||||||
|
sensor_temps.extend(chip_temps[chip])
|
||||||
|
for chip in sorted(chip_temps.keys()):
|
||||||
|
if 'coretemp' not in chip:
|
||||||
|
sensor_temps.extend(chip_temps[chip])
|
||||||
|
|
||||||
|
# Wrap columns as needed
|
||||||
|
screen_size = shutil.get_terminal_size()
|
||||||
|
rows = screen_size.lines - 1
|
||||||
|
if len(sensor_temps) > rows and screen_size.columns > 55*2:
|
||||||
|
sensor_temps = list(itertools.zip_longest(
|
||||||
|
sensor_temps[:rows], sensor_temps[rows:], fillvalue=''))
|
||||||
|
sensor_temps = [join_columns(a, b) for a, b in sensor_temps]
|
||||||
|
|
||||||
|
# Print data
|
||||||
|
for line in sensor_temps:
|
||||||
|
print_standard(line)
|
||||||
|
|
||||||
|
# Done
|
||||||
|
sensors.cleanup()
|
||||||
|
exit_script()
|
||||||
|
except SystemExit:
|
||||||
|
sensors.cleanup()
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
sensors.cleanup()
|
||||||
|
major_exception()
|
||||||
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
#
|
|
||||||
## Wizard Kit: HW Diagnostics - Sensors
|
|
||||||
|
|
||||||
LOG_DIR="$1"
|
|
||||||
|
|
||||||
function usage {
|
|
||||||
echo "Usage: $0 log-dir"
|
|
||||||
echo " e.g. $0 /tmp/tmp.7Mh5f1RhSL9001"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create directory if necessary
|
|
||||||
if [ ! -d "$LOG_DIR" ]; then
|
|
||||||
LOG_DIR="$(mktemp -d)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Run Sensor loop
|
|
||||||
if sensors >/dev/null 2>&1; then
|
|
||||||
while :; do
|
|
||||||
sensors -A | grep -E -i -v '(N/A|RPM|\d+\s+V\s+|^\s*$)' > "$LOG_DIR/sensors.out" 2>/dev/null
|
|
||||||
|
|
||||||
# Colorize
|
|
||||||
# Blue: All temps (superseeded by other colors below)
|
|
||||||
sed -i -r 's#(\+[0-9]+\.[0-9].C)#\\e[34m\1\\e[0m#g' "$LOG_DIR/sensors.out" >/dev/null 2>&1
|
|
||||||
# Green >= 60* C
|
|
||||||
sed -i -r 's#(\+6[0-9]\.[0-9].C)#\\e[32m\1\\e[0m#g' "$LOG_DIR/sensors.out" >/dev/null 2>&1
|
|
||||||
# Yellow >= 70* C
|
|
||||||
sed -i -r 's#(\+7[0-9]\.[0-9].C)#\\e[33m\1\\e[0m#g' "$LOG_DIR/sensors.out" >/dev/null 2>&1
|
|
||||||
# Orange >= 80* C
|
|
||||||
sed -i -r 's#(\+(8[0-9]|9[0-4])\.[0-9].C)#\\e[31\;1m\1\\e[0m#g' "$LOG_DIR/sensors.out" >/dev/null 2>&1
|
|
||||||
# Red >= 95* C
|
|
||||||
sed -i -r 's#(\+(9[5-9]|1[0-9][0-9])\.[0-9].C)#\\e[31m\1\\e[0m#g' "$LOG_DIR/sensors.out" >/dev/null 2>&1
|
|
||||||
|
|
||||||
# Output data
|
|
||||||
clear
|
|
||||||
echo -e "$(cat "$LOG_DIR/sensors.out")"
|
|
||||||
sleep 1s
|
|
||||||
done
|
|
||||||
else
|
|
||||||
echo -e "\e[33mNo sensors found!\nPlease monitor temperatures manually\e[0m"
|
|
||||||
sleep 1h
|
|
||||||
fi
|
|
||||||
|
|
@ -74,7 +74,7 @@ bindsym $mod+i exec "hardinfo"
|
||||||
bindsym $mod+m exec "urxvt -title 'Mount All Volumes' -e mount-all-volumes foh"
|
bindsym $mod+m exec "urxvt -title 'Mount All Volumes' -e mount-all-volumes foh"
|
||||||
bindsym $mod+s exec "urxvt -title 'Hardware Diagnostics' -e hw-diags foh"
|
bindsym $mod+s exec "urxvt -title 'Hardware Diagnostics' -e hw-diags foh"
|
||||||
bindsym $mod+t exec "urxvt"
|
bindsym $mod+t exec "urxvt"
|
||||||
bindsym $mod+v exec "urxvt -title 'Hardware Sensors' -e hw-diags-sensors"
|
bindsym $mod+v exec "urxvt -title 'Hardware Sensors' -e watch -c -n1 -t hw-sensors"
|
||||||
bindsym $mod+w exec "firefox"
|
bindsym $mod+w exec "firefox"
|
||||||
|
|
||||||
focus_follows_mouse no
|
focus_follows_mouse no
|
||||||
|
|
|
||||||
|
|
@ -334,7 +334,7 @@
|
||||||
</keybind>
|
</keybind>
|
||||||
<keybind key="W-v">
|
<keybind key="W-v">
|
||||||
<action name="Execute">
|
<action name="Execute">
|
||||||
<command>urxvt -title "Hardware Sensors" -e hw-diags-sensors</command>
|
<command>urxvt -title "Hardware Sensors" -e watch -c -n1 -t hw-sensors</command>
|
||||||
</action>
|
</action>
|
||||||
</keybind>
|
</keybind>
|
||||||
<keybind key="W-w">
|
<keybind key="W-w">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue