What makes us different from other similar websites? › Forums › Tech › Logitech Litra Glow (Linux Drivers) › Reply To: Logitech Litra Glow (Linux Drivers)
💡 Litra Light Controller GUI Setup Guide for Linux (Xubuntu/Debian)
This guide provides every single step needed to install the lcli command-line tool, set up the Python GUI, and create a custom menu entry for controlling your Logitech Litra light.
1. Prerequisites and Installing the CLI Tool (lcli)
This application requires Python 3, Tkinter, and the external lcli tool.
1.1 Download the Litra CLI Tool
1. Open your web browser and go to the official lcli GitHub repository Releases page:
https://github.com/charles-dyer/lcli/releases
2. Download the latest Linux x86_64 release (it will be named something like lcli_x86_64.tar.gz).
3. Open your terminal and navigate to your Downloads directory:
cd ~/Downloads
4. Extract the contents (replace [FILENAME] with the actual file name you downloaded, e.g., lcli_x86_64.tar.gz):
tar -xzf [FILENAME]
5. Move the executable to a standard binary location so you know its path. Assuming the extracted executable is named lcli:
sudo mv ./lcli /usr/local/bin/lcli
If this command fails because the file is in a subdirectory, locate the lcli executable inside the extracted folder and move it to /usr/local/bin/.
1.2 Identify the Absolute Path
The absolute path for the lcli executable is now: /usr/local/bin/lcli. You will use this path in the Python script in the next step.
1.3 Python and Tkinter Check
Ensure you have Python 3 and Tkinter installed. If you are on a Debian/Ubuntu-based system and the GUI doesn’t work, install Tkinter:
sudo apt update
sudo apt install python3-tk
2. Setup the Application Files
We will create a specific directory to hold the Python script. Open your terminal and run:
mkdir -p ~/.local/share/litra-controller
2.A Create the GUI Python Script (litra_controller.py)
This is the main application code. Open your preferred text editor (mousepad) and save the following content as ~/.local/share/litra-controller/litra_controller.py.
CRITICAL: The DEFAULT_CLI_PATH in the code below is set to /usr/local/bin/lcli. Only change it if you moved the executable elsewhere.
import tkinter as tk
from tkinter import ttk, messagebox
import subprocess
import webbrowser
# --- Configuration Constants ---
# ABSOLUTE PATH TO THE EXECUTABLE.
DEFAULT_CLI_PATH = '/usr/local/bin/lcli'
MIN_BRIGHTNESS = 1
MAX_BRIGHTNESS = 100
MIN_TEMP = 2700
MAX_TEMP = 6500
HIGHLIGHT_COLOR = '#ff66cc'
class LitraControllerApp(tk.Tk):
def __init__(self):
super().__init__()
self.title("Litra Light Controller (Python CLI Wrapper)")
self.geometry("520x450")
self.resizable(False, False)
try:
ttk.Style().theme_use('clam')
except:
pass
self.cli_path = tk.StringVar(value=DEFAULT_CLI_PATH)
self.is_power_on = tk.BooleanVar(value=True)
self.generated_command = tk.StringVar(value="Adjust controls or click a button to generate command.")
self.create_menu()
self.create_widgets()
self.update_command(None)
def create_menu(self):
menubar = tk.Menu(self)
self.config(menu=menubar)
help_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Help", menu=help_menu)
help_menu.add_command(label="About", command=self.show_about_dialog)
def open_url(self, url):
webbrowser.open_new_tab(url)
def show_about_dialog(self):
about_window = tk.Toplevel(self)
about_window.title("About Litra Controller")
about_window.resizable(False, False)
about_window.transient(self)
about_window.grab_set()
frame = ttk.Frame(about_window, padding="15")
frame.pack(fill="both", expand=True)
ttk.Label(frame, text="Litra Light Controller", font=('Arial', 14, 'bold')).pack(pady=(0, 5))
ttk.Label(frame, text="Version 1.0 (Python CLI Wrapper)", font=('Arial', 10)).pack(pady=(0, 10))
description = (
"This GUI allows control of your Logitech Litra light via the "
"external Litra CLI (lcli) tool, requiring sudo permissions."
)
ttk.Label(frame, text=description, wraplength=350, justify=tk.CENTER).pack(pady=(0, 20))
ttk.Label(frame, text="GUI Development:", font=('Arial', 10, 'bold')).pack()
link_text = "thumbtak on taksshack.com"
url = "[https://taksshack.com/members/thumbtak/](https://taksshack.com/members/thumbtak/)"
link_label = ttk.Label(frame, text=link_text,
foreground="blue",
cursor="hand2",
font=('Arial', 10, 'underline'))
link_label.pack(pady=(0, 5))
link_label.bind("<Button-1>", lambda e: self.open_url(url))
ttk.Button(frame, text="Close", command=about_window.destroy, style='Pink.TButton').pack(pady=(15, 0))
about_window.update_idletasks()
main_x = self.winfo_x(); main_y = self.winfo_y()
main_width = self.winfo_width(); main_height = self.winfo_height()
win_width = about_window.winfo_reqwidth(); win_height = about_window.winfo_reqheight()
x = main_x + (main_width // 2) - (win_width // 2)
y = main_y + (main_height // 2) - (win_height // 2)
about_window.geometry(f'+{x}+{y}')
self.wait_window(about_window)
def create_widgets(self):
# Widget creation logic (omitted for brevity, see code for full implementation)
main_frame = ttk.Frame(self, padding="15")
main_frame.pack(fill='both', expand=True)
ttk.Label(main_frame, text="Litra CLI Path (lcli):", font=('Arial', 10, 'bold')).pack(pady=(0, 2), anchor='w')
ttk.Entry(main_frame, textvariable=self.cli_path).pack(fill='x', padx=5, pady=(0, 10))
control_frame = ttk.LabelFrame(main_frame, text="Light Controls", padding="10")
control_frame.pack(fill='x', pady=10)
power_frame = ttk.Frame(control_frame)
power_frame.pack(fill='x', pady=5)
ttk.Label(power_frame, text="Light Power:", font=('Arial', 11)).pack(side='left')
self.power_button = ttk.Button(power_frame, text="", command=self.toggle_power)
self.power_button.pack(side='right')
ttk.Label(control_frame, text="Brightness:", font=('Arial', 11)).pack(pady=(10, 0), anchor='w')
self.brightness_scale = tk.Scale(control_frame, from_=MIN_BRIGHTNESS, to=MAX_BRIGHTNESS,
orient=tk.HORIZONTAL, command=self.update_command,
length=400, resolution=1, relief=tk.FLAT, bd=0,
troughcolor="#E0E7FF", showvalue=True, highlightthickness=0)
self.brightness_scale.set(50)
self.brightness_scale.pack(fill='x', padx=5)
ttk.Label(control_frame, text="Color Temperature (K):", font=('Arial', 11)).pack(pady=(10, 0), anchor='w')
self.temp_scale = tk.Scale(control_frame, from_=MIN_TEMP, to=MAX_TEMP,
orient=tk.HORIZONTAL, command=self.update_command,
length=400, resolution=50, relief=tk.FLAT, bd=0,
troughcolor="#E0E7FF", showvalue=True, highlightthickness=0)
self.temp_scale.set(4600)
self.temp_scale.pack(fill='x', padx=5)
ttk.Label(main_frame, text="Generated Command:", font=('Arial', 10, 'bold')).pack(pady=(15, 2), anchor='w')
command_output_frame = ttk.Frame(main_frame)
command_output_frame.pack(fill='x')
command_output_frame.columnconfigure(0, weight=1)
self.command_text = tk.Text(command_output_frame, height=2, width=50, state='disabled',
bg="#2D3748", fg=HIGHLIGHT_COLOR, font=('Courier', 10), wrap='word', bd=1, relief=tk.SUNKEN)
self.command_text.grid(row=0, column=0, sticky="nsew", padx=(0, 5), pady=0)
copy_button = ttk.Button(command_output_frame, text="Copy", command=self.copy_command)
copy_button.grid(row=0, column=1, sticky="ns", pady=0)
self.update_power_button_ui()
def update_power_button_ui(self):
if self.is_power_on.get():
self.power_button.config(text=" ON ", style='Pink.TButton')
else:
self.power_button.config(text=" OFF ", style='Red.TButton')
s = ttk.Style()
s.configure('Pink.TButton', background=HIGHLIGHT_COLOR, foreground='black', font=('Arial', 10, 'bold'))
s.map('Pink.TButton', background=[('active', '#e65cb8')])
s.configure('Red.TButton', background='#F56565', foreground='black', font=('Arial', 10, 'bold'))
s.map('Red.TButton', background=[('active', '#E53E3E')])
def toggle_power(self):
self.is_power_on.set(not self.is_power_on.get())
self.update_power_button_ui()
self.update_command(None)
def update_command(self, event):
cli = self.cli_path.get().strip()
brightness = self.brightness_scale.get()
temperature = self.temp_scale.get()
command_parts = []
sudo_cli = f"sudo {cli}"
if self.is_power_on.get():
command_parts.append(f"{sudo_cli} on")
command_parts.append(f"{sudo_cli} bright {brightness}")
command_parts.append(f"{sudo_cli} temp {temperature}")
command_string = " && ".join(command_parts)
else:
command_string = f"{sudo_cli} off"
self.command_text.config(state='normal')
self.command_text.delete(1.0, tk.END)
self.command_text.insert(tk.END, command_string)
self.command_text.config(state='disabled')
self.execute_command(auto_exec=True)
def copy_command(self):
command = self.command_text.get(1.0, tk.END).strip()
if command:
self.clipboard_clear()
self.clipboard_append(command)
messagebox.showinfo("Copied", "Command copied to clipboard!")
else:
messagebox.showwarning("Empty", "No command to copy.")
def execute_command(self, auto_exec=False):
command = self.command_text.get(1.0, tk.END).strip()
if not command: return
try:
if not auto_exec:
messagebox.showinfo("Executing Command",
"Running command in shell. You will likely be prompted for your sudo password in the terminal window where you launched this GUI.")
result = subprocess.run(command, shell=True, check=True,
capture_output=True, text=True)
if auto_exec:
print(f"Executed: {command}")
else:
output = result.stdout.strip() if result.stdout else "Command executed successfully with no output."
messagebox.showinfo("Execution Success", f"SUCCESS:\n{output}")
except subprocess.CalledProcessError as e:
error_output = e.stderr.strip() if e.stderr else "Unknown error."
messagebox.showerror("Execution Failed", f"Command failed with return code {e.returncode}. You may need to enter your sudo password in the terminal:\n{error_output}")
except FileNotFoundError:
messagebox.showerror("Execution Failed", f"The executable specified was not found. Check the CLI Path: '{self.cli_path.get()}'")
except Exception as e:
messagebox.showerror("Execution Failed", f"An unexpected error occurred:\n{e}")
if __name__ == "__main__":
app = LitraControllerApp()
app.mainloop()
2.B Create the Runner Script (run_litra.sh)
This shell script launches the Python GUI using a terminal, which is necessary to handle the sudo password prompt.
1. Create the script file using mousepad:
mkdir -p ~/.local/bin/
mousepad ~/.local/bin/run_litra.sh
2. Paste the following content into the file:
#!/bin/bash
# Define the absolute path to the Python script using the user's HOME directory
SCRIPT_PATH="$HOME/.local/share/litra-controller/litra_controller.py"
# Execute the script using the default terminal emulator (e.g., xfce4-terminal).
# The --hold option keeps the terminal open to capture the sudo password and see errors.
xfce4-terminal --hold --title="Litra Controller Runner" --command="python3 $SCRIPT_PATH"
3. Set Permissions: Make the runner script executable:
chmod +x ~/.local/bin/run_litra.sh
3. Create the Desktop Launcher (litra-controller.desktop)
This file integrates the application into your desktop environment’s application menu (e.g., the Start Menu).
1. Create the file using mousepad:
mousepad ~/.local/share/applications/litra-controller.desktop
2. Paste the following content into the file:
[Desktop Entry]
Name=Litra Light Controller
Comment=GUI to control Logitech Litra light via lcli
Exec=$HOME/.local/bin/run_litra.sh
Icon=system-run
Terminal=false
Type=Application
Categories=Utility;
3. Save and close mousepad.
4. Final Steps and Usage
1. Refresh Menu: Log out and log back into your desktop session to ensure the application menu refreshes and shows the new entry.
2. Launch: Find “Litra Light Controller” in your application menu and click it. A terminal window will open, and the separate GUI window will launch.
3. Sudo Prompt: The first time you adjust a control (like brightness or color temperature), the terminal window will prompt you for your user password with:
[sudo] password for yourusername:
4. Enter Password: Type your user password into the terminal and press Enter.
5. Enjoy: Once authenticated, the application will run all commands successfully until the sudo timeout expires.
