ThinkPad X280 running Ubuntu 18.04 allows a brightness level from 0 to 1515. However, the Fn+F5/F6 hotkey only allows 21 levels of control. This post provides a solution and easy-to-use script for brightness control, best use with Guake.
Features and how it looks like
- Use arrow keys to change the brightness (while the script is running), by 1, 10 or 100.
- Does not interfere with the Fn+F5/F6 hotkey.
- While the script was running and Fn+F5/F6 hotkey has been triggered, the script will display the up-to-date brightness value.
$ x280-brightness up/a:+1 right/s:+10 PageDown/d:+100 down/z:-1 left/x:-10 PageUp/c:-100 esc/q: quit Brightness: 160 / 1515 (10.56%)
Setup
To allow our script to write to /sys/class/backlight/intel_backlight/brightness
without entering sudoer password, we need to edit sudoers file
sudo --preserve-env=EDITOR visudo -f /etc/sudoers.d/nopasswd
%sudo ALL=NOPASSWD:/bin/chgrp sudo /sys/class/backlight/intel_backlight/brightness %sudo ALL=NOPASSWD:/bin/chmod g+w /sys/class/backlight/intel_backlight/brightness
Install Ruby gem rb-inotify
, to be able to monitor the sysfs without polling.gem install rb-inotify --user-install
And create the script x280-brightness
(or download from gist)
#!/usr/bin/env ruby # encoding: utf-8 require "io/console" require "rb-inotify" CURSOR_ON = "\e[?25h" CURSOR_OFF = "\e[?25l" BRIGHTNESS_FILE = "/sys/class/backlight/intel_backlight/brightness" MAX_BRIGHTNESS_FILE = "/sys/class/backlight/intel_backlight/max_brightness" class StdoutSingleLineUpdate def initialize @length = 0 @mutex = Mutex.new end private def self.get_str_console_length(str) length = 0 str.each_char{|char| if char.bytesize == 3 length += 2 else length += 1 end } str.scan(/\x1B\[[0-9;]+m/).each{|colour_msg| length -= colour_msg.size } return length end public def update_message(msg) @mutex.synchronize{ padding_length = @length - self.class.get_str_console_length(msg) padding = "" if padding_length > 0 padding = " " * padding_length end new_msg = msg + padding $stdout.write"\r#{new_msg}" @length = self.class.get_str_console_length(new_msg) } return end end class Brightness attr_reader :value attr_reader :max_value def initialize(filename, max_filename, &callback) @filename = filename File.open(max_filename, "rb"){|file| @max_value = file.read().to_i } read_sysfs() @callback = nil if callback != nil @callback = callback @thread = Thread.new{ run_inotify() } end end def close if @callback != nil @notifier.stop @notifier.close @thread.terminate @thread.join end end def run_inotify @notifier = INotify::Notifier.new @notifier.watch(File.dirname(BRIGHTNESS_FILE), :modify){|event| if event.name == File.basename(BRIGHTNESS_FILE) read_sysfs() @callback.call(self) end } @notifier.run end def read_sysfs File.open(@filename, "rb"){|file| @value = file.read().to_i } end def write_sysfs File.open(@filename, "wb"){|file| file.write(@value.to_s) } end def read return @value end def write(value) if (Integer === value) != true raise ArgumentError, "Invalid value" end if value < 0 return elsif value > @max_value return end @value = value write_sysfs() end def increase(brightness_to_change) write(@value + brightness_to_change) end end class ReadKey @@key_map = {} (1..26).each{|x| @@key_map[x.chr] = "ctrl_#{(96+x).chr}".to_sym } @@key_map["\e[A"] = :up @@key_map["\e[B"] = :down @@key_map["\e[D"] = :left @@key_map["\e[C"] = :right @@key_map["\x7f"] = :backspace @@key_map["\e"] = :esc @@key_map["\t"] = :tab @@key_map["\r"] = :enter @@key_map["\e[2~"] = :insert @@key_map["\e[3~"] = :delete @@key_map["\e[H"] = :home @@key_map["\e[F"] = :end @@key_map["\e[5~"] = :pageup @@key_map["\e[6~"] = :pagedown @@key_map["\eOP"] = :f1 @@key_map["\eOQ"] = :f2 @@key_map["\eOR"] = :f3 @@key_map["\eOS"] = :f4 @@key_map["\e[15~"] = :f5 @@key_map["\e[17~"] = :f6 @@key_map["\e[18~"] = :f7 @@key_map["\e[19~"] = :f8 @@key_map["\e[20~"] = :f9 @@key_map["\e[21~"] = :f10 @@key_map["\e[24~"] = :f11 @@key_map["\e[24~"] = :f12 def self.read key = $stdin.getch while true temp = nil temp = $stdin.read_nonblock(1) rescue nil if temp == nil break end key << temp end symbol = @@key_map[key] if symbol != nil return symbol end return key end end def brightness_to_str(brightness) return "Brightness: #{brightness.value} / #{brightness.max_value} (#{(brightness.value.to_f / brightness.max_value * 100).round(2)}%)" end def main if File.writable?(BRIGHTNESS_FILE) != true system("sudo chgrp sudo /sys/class/backlight/intel_backlight/brightness") system("sudo chmod g+w /sys/class/backlight/intel_backlight/brightness") end begin line_update = StdoutSingleLineUpdate.new brightness = Brightness.new(BRIGHTNESS_FILE, MAX_BRIGHTNESS_FILE){|brightness_self| line_update.update_message(brightness_to_str(brightness_self)) } $stdout.puts(" up/a:+1 right/s:+10 PageDown/d:+100") $stdout.puts("down/z:-1 left/x:-10 PageUp/c:-100") $stdout.puts("esc/q: quit") $stdin.echo = false begin $stdin.cursor = false rescue NotImplementedError $stdout.write(CURSOR_OFF) end line_update.update_message(brightness_to_str(brightness)) while true brightness_updated = true brightness_to_change = 0 key = ReadKey.read() if String === key key.downcase! end case key when "a", :up brightness_to_change = 1 when "s", :right brightness_to_change = 10 when "d", :pagedown brightness_to_change = 100 when "z", :down brightness_to_change = -1 when "x", :left brightness_to_change = -10 when "c", :pageup brightness_to_change = -100 when "q", :esc, :ctrl_c brightness.close break else brightness_updated = false line_update.update_message("Unsupported key #{key.inspect}") end if brightness_updated == true brightness.increase(brightness_to_change) line_update.update_message(brightness_to_str(brightness)) end end rescue Exception => e raise e ensure $stdin.echo = true begin $stdin.cursor = true rescue NotImplementedError $stdout.write(CURSOR_ON) end $stdout.puts("") end end main()