Precision brightness control on X280 running Ubuntu 18.04 (bionic)

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()

Leave a Reply

Your email address will not be published. Required fields are marked *