Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,132 changes: 1,103 additions & 29 deletions Cargo.lock

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@ name = "lungo"
version = "0.0.1"
authors = ["Robin Candau <robincandau@protonmail.com>"]
edition = "2024"
description = "A simple systray applet to prevent your system from going idle or sleeping on demand"
description = "A simple systray applet to prevent the system from going idle or suspending on demand"
license = "GPL-3.0-or-later"

[dependencies]
clap = { version = "4.6.1", features = ["derive"] }
dirs = "6.0.0"
env_logger = "0.11.11"
fs2 = "0.4.3"
ksni = "0.3.6"
log = "0.4.33"
tokio = "1.53.1"
zbus = "5.18.0"

[dev-dependencies]
assert_cmd = "2.0.0"
Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@

## Description

A simple systray applet to prevent your system from going idle or sleeping on demand.
A simple systray applet to prevent the system from going idle or suspending on demand (including on laptop lid closure).

Run `lungo` to start the systray applet.

Simply click on the systray applet to toggle it on and off.
The systray icon automatically changes to reflect the current state ("empty cup" icon if off, "hot cup" icon if on).
The systray icon automatically changes to reflect the current state ("empty cup" icon if off, "hot cup" icon if on).
The state can also be consulted and toggled from the systray applet menu.

Lungo is:

Expand Down Expand Up @@ -69,7 +70,9 @@ cd lungo
cargo build --release
```

The built binary will be located at `./target/release/lungo`.
The built binary will be located at `./target/release/lungo`, place it somewhere in your `$PATH`.

Place the icons from the [`res/icons/`](https://github.com/Antiz96/lungo/tree/main/res/icons) directory in an "icon compliant" folder on the system (such as `~/.icons/lungo/`, `~/.local/share/icons/lungo` or `/usr/share/icons/lungo/`, create it if needed). For packaging, using the `hicolor-icon-theme` folder (e.g. `/usr/share/icons/hicolor/scalable/apps/`) is an option.

A `.desktop` file is available in the [`res/desktop/`](https://github.com/Antiz96/lungo/tree/main/res/desktop) directory, allowing to automatically start the systray applet at boot via [XDG Autostart](https://wiki.archlinux.org/title/XDG_Autostart) (by placing it either in `~/.config/autostart/` or `/etc/xdg/autostart/`).

Expand All @@ -88,7 +91,8 @@ Run `lungo` to start the systray applet.
To start it automatically at boot, either place the [`.desktop file`](https://github.com/Antiz96/lungo/blob/main/res/desktop/lungo.desktop) in `~/.config/autostart/` or `/etc/xdg/autostart/` (relies on [XDG Autostart](https://wiki.archlinux.org/title/XDG_Autostart)). Alternatively (if your graphical environment doesn't support XDG Autostart for instance), add the `lungo` command to your environment's auto-start method.

Simply click on the systray applet to toggle it on and off.
The icon automatically changes to reflect the current state ("empty cup" icon if off, "hot cup" icon if on).
The icon automatically changes to reflect the current state ("empty cup" icon if off, "hot cup" icon if on).
The state can also be consulted and toggled from the systray applet menu.

## Documentation

Expand Down
3 changes: 2 additions & 1 deletion doc/man/lungo.1.scd
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ lungo(1) ["Lungo 0.0.1" ["Lungo Manual"]]

# NAME

Lungo - A simple systray applet to prevent your system from going idle or sleeping on demand.
Lungo - A simple systray applet to prevent the system from going idle or suspending on demand (including on laptop lid closure).

# DESCRIPTION

Run `lungo` to start the systray applet.

Simply click on the systray applet to toggle it on and off.
The icon automatically changes to reflect the current state ("empty cup" icon if off, "hot cup" icon if on).
The state can also be consulted and toggled from the systray applet menu.

# SYNOPSIS

Expand Down
2 changes: 1 addition & 1 deletion res/desktop/lungo.desktop
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[Desktop Entry]
Name=Lungo
Comment=A simple systray applet to prevent your system from going idle or sleeping on demand
Comment=A simple systray applet to prevent the system from going idle or suspending on demand
Icon=lungo
Type=Application
Exec=lungo
Expand Down
8 changes: 8 additions & 0 deletions res/icons/lungo-off.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions res/icons/lungo-on.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

pub fn show_help() {
println!(
"Lungo - A simple systray applet to prevent your system from going idle or sleeping on demand."
"Lungo - A simple systray applet to prevent the system from going idle or suspending on demand (including on laptop lid closure)."
);
println!();
println!("Run `lungo` to start the systray applet.");
Expand All @@ -11,6 +11,7 @@ pub fn show_help() {
println!(
"The icon automatically changes to reflect the current state (\"empty cup\" icon if off, \"hot cup\" icon if on)."
);
println!("The state can also be consulted and toggled from the systray applet menu.");
println!();
println!("Options:");
println!(" -h, --help Display this message");
Expand Down
27 changes: 27 additions & 0 deletions src/inhibit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//! Acquire inhibit file descriptor lock through logind

use std::os::fd::OwnedFd;
use zbus::blocking::Connection;
use zbus::zvariant::OwnedFd as ZbusFd;

pub fn acquire_inhibit() -> Result<OwnedFd, Box<dyn std::error::Error>> {
let conn = Connection::system()?;

let reply: ZbusFd = conn
.call_method(
Some("org.freedesktop.login1"),
"/org/freedesktop/login1",
Some("org.freedesktop.login1.Manager"),
"Inhibit",
&(
"idle:sleep:handle-lid-switch",
"Lungo",
"Keeping the system awake",
"block",
),
)?
.body()
.deserialize()?;

Ok(reply.into())
}
32 changes: 32 additions & 0 deletions src/lockfile.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//! Create (if needed) and acquire lockfile
//! in order to prevent multiple instances to run at the same time

use fs2::FileExt;
use std::fs::{self, File, OpenOptions};
use std::io::{self, Error, ErrorKind};

pub fn acquire_lockfile() -> io::Result<File> {
// Create lungo cachedir (if it doesn't exist)
let cachedir = dirs::cache_dir()
.ok_or_else(|| Error::new(ErrorKind::NotFound, "Could not determine cache directory"))?
.join("lungo");
fs::create_dir_all(&cachedir)?;

let lockfile_path = cachedir.join("lungo.lock");

let lockfile = OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(lockfile_path)?;

lockfile.try_lock_exclusive().map_err(|error| {
if error.kind() == ErrorKind::WouldBlock {
Error::from(ErrorKind::AlreadyExists)
} else {
error
}
})?;

Ok(lockfile)
}
32 changes: 29 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
//! Lungo - A simple systray applet to prevent your system from going idle or sleeping on demand.
//! Lungo - A simple systray applet to prevent the system from going idle or suspending on demand (including on laptop lid closure).

use clap::Parser;
use log::error;
use std::io::ErrorKind;
use std::process;

mod help;
mod inhibit;
mod lockfile;
mod tray;
mod version;

// Argument parser
Expand All @@ -17,7 +23,11 @@ struct Args {
version: bool,
}

fn main() {
#[tokio::main]
async fn main() {
// Initialize logger
env_logger::init();

// Parse arguments
let args = Args::parse();

Expand All @@ -30,6 +40,22 @@ fn main() {
// Show name and version if the -V / --version arg is passed
if args.version {
version::show_version();
//return;
return;
}

// Create (if needed) and acquire lockfile
// Exit if there's already an instance running
// or if there was an issue creating or acquiring the lockfile (e.g. permission issue)
let _lock = lockfile::acquire_lockfile().unwrap_or_else(|error| {
if error.kind() == ErrorKind::AlreadyExists {
error!("Another instance of lungo is already running");
} else {
error!("Failed to acquire lockfile: {error}");
}

process::exit(1);
});

// Start systray applet
tray::run().await;
}
115 changes: 115 additions & 0 deletions src/tray.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//! Systray applet implementation
//! Built with ksni:
//! https://github.com/iovxw/ksni
//! https://crates.io/crates/ksni

use ksni::TrayMethods;
use ksni::menu::*;
use log::{debug, info};
use std::future;
use std::os::fd::OwnedFd;
use std::process;
use tokio::task;

use crate::inhibit;

pub struct LungoTray {
inhibit_fd: Option<OwnedFd>,
}

// Systray Applet implementation
impl ksni::Tray for LungoTray {
// Set id
fn id(&self) -> String {
"Lungo".into()
}

// Set icon
fn icon_name(&self) -> String {
let icon = if self.inhibit_fd.is_some() {
"lungo-on"
} else {
"lungo-off"
};

debug!("Icon set or updated: {icon}");
icon.into()
}

// Set title
fn title(&self) -> String {
"Lungo".into()
}

// Set tooltip
fn tool_tip(&self) -> ksni::ToolTip {
ksni::ToolTip {
title: "Lungo".into(),
..Default::default()
}
}

// Toggle checkable menu entry on click (triggers inhibitor lock and changes icon accordingly)
fn activate(&mut self, _x: i32, _y: i32) {
self.toggle();
}

// Build menu
fn menu(&self) -> Vec<ksni::MenuItem<Self>> {
vec![
// Checkable menu entry, reporting the current state and allowing to toggle the creation
// or release of the inhibit file descriptor lock
CheckmarkItem {
label: "Enable".into(),
checked: self.inhibit_fd.is_some(),
activate: Box::new(|this: &mut Self| this.toggle()),
..Default::default()
}
.into(),
// Exit button
StandardItem {
label: "Exit".into(),
activate: Box::new(|_| {
info!("Exited on user request");
process::exit(0);
}),
..Default::default()
}
.into(),
]
}
}

// Helper to toggle the creation and releasing of the inhibit file descriptor lock
impl LungoTray {
fn toggle(&mut self) {
if self.inhibit_fd.is_some() {
self.inhibit_fd = None;
info!("Inhibitor disabled");
} else {
match task::block_in_place(inhibit::acquire_inhibit) {
Ok(fd) => {
self.inhibit_fd = Some(fd);
info!("Inhibitor enabled");
}
Err(error) => {
log::error!("Unable to acquire inhibitor: {error}");
}
}
}
}
}

// Starting the Systray Applet
pub async fn run() {
let tray = LungoTray { inhibit_fd: None };

tray.spawn()
.await
.expect("Unable to start the systray applet");

info!("Systray applet started");

// Run forever
future::pending().await
}