For the I3 window manager (the one and only), some functionalities might need some extra configuration. Basically, you can screw and adjust this thing as you like. On the downside, the learning curve is quite steep.

In day to day work life, I3 is quite the interface to use and with some minor adjustments, even in the office area quite easy to use.

One litte script, that became the ultimative service tool for me is a script, that detect external monitors and enables or disables them. In an office environment, this is quite usefull to connect to a projector or an external screen in a meeting room.


Fist, within ~/.i3/config the mod+tab key is connected to a custom script:

bindcode $mod+23 exec "jtt screen toggle;"

This runs a custom script I use jtt, with the parameter screen and the option toggle.

I will not add the whole script here, since it does some other stuff (which is totally unrelated) as well, but by just looking at the core sections, you will get the idea and be able to adjust the ruby code to your environment.

The first script just controls a ruby module jtt.mod.screen that does the actual job. That module follows below.

    # Manage screen control via I3
    desc 'screen', 'Manage screen functions'
    def screen(*parameter)
      require_relative  './jtt.mod.screen'

      # Fail check with syntax output
      if parameter.size != 1
        puts "#{File.basename(__FILE__)} screen on|off|toggle|10-100"
        exit 0
      end

      case parameter[0]
      when 'off'
        Screen.disable
      when 'on'
        Screen.enable
      when 'toggle'
        puts Screen.get_status
        if Screen.get_status == 1
          Screen.enable
        elsif Screen.get_status.nil?
          puts 'No additional screen found.'
          `xmessage -center -timeout 2 'No additional screen found'`
          exit 0
        else
          Screen.disable
        end
      when /^[+-]?[1-9][0-9](0?)$/
        Screen.set_screen_light parameter[0]
      else
        puts "${__FILE__} screen on|off|toggle|10-100"
        exit 1
      end
    end

Here now the module:

#!/usr/bin/env ruby
#
# Module to manage screen functions via i3

module Screen

  require 'json'

  # Enable an external screen with xrand
  def Screen.enable
    puts 'Enabling screen'
    command = 'xrandr --output ' + get_external_output.keys.first + ' --right-of ' + get_main_output + ' --auto'
    `#{command}`

    # Change the location of workspace 1 if it's not on the external screen yet.
    current_position_of_ws_1 = get_output_for_workspace(1)
    if current_position_of_ws_1 != get_external_output.keys.first 
      # Focus workspace 1
      `i3-msg workspace 1`
      # Move container 1 to new screen
      `i3-msg move workspace to output right`
    end
    # Focus workspace 1
    `i3-msg workspace 1`
  end


  # Return the status of a screen
  # 1: Connected Screen is not enabled
  # 0: connected Screen is enabled.
  def Screen.get_status
    second_screen = get_external_output
    if second_screen and second_screen[second_screen.keys.last][:enabled] == false
      return 1
    elsif second_screen and second_screen[second_screen.keys.last][:enabled] == true
      return 0
    end
  end


  # Determine the main output
  def Screen.get_main_output
    `xrandr`.each_line do |line|
      if line =~ /connected/i
        return line.split(' ')[0]
      end
    end
    exit 1
  end


  # Return the current workspace of an output
  # Read the current active workspace distribution and return the output the given workspace
  # is running on, otherwise nil
  def Screen.get_output_for_workspace(workspace_id)
    current_output_config=JSON.parse(`i3-msg -t get_outputs`)
    current_output_config.each do |single_output_config|
      if single_output_config['current_workspace'] == workspace_id.to_s
        return single_output_config['name']
      end
    end
    return nil
  end

  # Identify the current active workspace return the workspace_id
  def Screen.get_current_workspace_id
    current_workspace_config=JSON.parse(`i3-msg -t get_workspaces`)
    current_workspace_config.each do |workspace|
      if workspace['focused']
        return workspace['num']
      end
    end
  end


  # Disabled the fist additionaly screen
  def Screen.disable
    puts 'Disabling screen'
    current_screen = get_current_workspace_id
    command = 'xrandr --output ' + get_external_output.keys.first + ' --off'
    `#{command}`

    # Switch back to the previously active screen.
    `i3-msg focus output #{get_main_output}`
    `i3-msg workspace #{current_screen}`
  end

  # Find the external  output, parse som data and return the information
  # Return
  #   hash: external output found, this is the data
  #   false: no external output could be identified.
  def Screen.get_external_output
    output_status = Hash.new
    `xrandr`.each_line do |line|

      pattern = /(^hdmi\-|^dp\-|^edp\-|^vga\-)/i

      if line =~ pattern
        fields = line.split(' ')
        if fields[1] =~ /^connected$/i
          if fields[fields.size-1] =~ /.*mm$/
            screen_enabled = true
          else
            screen_enabled = false
          end
          output_status[fields[0]] = { :status => fields[1], :resolution => fields[2], :enabled => screen_enabled }
        end
      end

    end

    # Return the last connected output in addition to the first one found
    # ( which is the local one), otherwise "false"
    if output_status.keys.size > 1
      return { output_status.keys.last => output_status.values.last }
    else
      return false
    end
  end


  # Set the light on the Laptop display
  #
  def Screen.set_screen_light(light_value)

    if not system('which light 2>/dev/null 1>/dev/null')
      puts 'This system is not supported.'
      `xmessage -center -timeout 2 'System is not supported for changing display light.'`
      exit 0
    end

    case light_value
    when /^\+\d{1,2}$/
      `light -A #{light_value.delete('+')}`
    when /^\-\d{1,2}$/
      `light -U #{light_value.delete('-')}`
    when /^\d{1,3}$/
      `light -S #{light_value}`
    else
      puts 'Value error!'
      exit 1
    end
  end

end

When set together, a simple mod+tab will result in one of three results:

  1. Any connected screen will be enabled and the workspace 1 will be moved over there.
  2. Any connected screen will be disabled if enabled and the workspaces moved back to the local screen.
  3. If now screen is connected a small Popup window will let you know, that you have not connected any screen.