added INI config file support
This commit was merged in pull request #12.
This commit is contained in:
+10
-1
@@ -7,6 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
This is a Rust rewrite of Redshift, a screen color temperature adjustment tool. The goal is to modernize the codebase while maintaining feature parity with the original C implementation located in `../legacy/`.
|
||||
|
||||
The original Redshift project:
|
||||
|
||||
- Adjusts screen color temperature based on time of day using gamma ramps
|
||||
- Consists of a C daemon (`redshift`) and Python GUI (`redshift-gtk`)
|
||||
- Uses GNU Autotools build system
|
||||
@@ -49,6 +50,7 @@ cargo clippy
|
||||
**Current state:** Initial project setup with minimal "Hello, world!" implementation.
|
||||
|
||||
The rewrite is in early stages. The original C codebase in `../legacy/` should be referenced for:
|
||||
|
||||
- Architecture patterns (adjustment methods, location providers, solar calculations)
|
||||
- Platform-specific implementations
|
||||
- Configuration file format
|
||||
@@ -59,6 +61,7 @@ The rewrite is in early stages. The original C codebase in `../legacy/` should b
|
||||
The Rust rewrite should maintain the modular architecture of the original:
|
||||
|
||||
**Core modules to implement:**
|
||||
|
||||
- Gamma adjustment backends (DRM, RANDR, VidMode)
|
||||
- Location providers (manual, GeoClue2)
|
||||
- Solar position calculations
|
||||
@@ -67,6 +70,7 @@ The Rust rewrite should maintain the modular architecture of the original:
|
||||
- Main event loop and signal handling
|
||||
|
||||
**Key architectural decisions from original:**
|
||||
|
||||
- Adjustment methods are pluggable (different display systems)
|
||||
- Location providers are queried at startup only
|
||||
- Main loop sets gamma every few seconds/minutes
|
||||
@@ -89,6 +93,7 @@ The legacy C implementation is located in `../legacy/src/`:
|
||||
Currently the project has no dependencies. As features are implemented, consider:
|
||||
|
||||
**Potential crates:**
|
||||
|
||||
- `clap` or `structopt` for CLI argument parsing
|
||||
- `serde` and `toml`/`ini` for configuration file parsing
|
||||
- `chrono` for time calculations
|
||||
@@ -98,6 +103,7 @@ Currently the project has no dependencies. As features are implemented, consider
|
||||
## Coding Style
|
||||
|
||||
Follow standard Rust conventions:
|
||||
|
||||
- Use `cargo fmt` for formatting
|
||||
- Use `cargo clippy` for linting
|
||||
- Prefer idiomatic Rust patterns over direct C translations
|
||||
@@ -106,6 +112,7 @@ Follow standard Rust conventions:
|
||||
## Platform Support
|
||||
|
||||
This rewrite targets Linux only:
|
||||
|
||||
- **DRM:** Direct Rendering Manager (TTY/framebuffer)
|
||||
- **RANDR:** X11 RandR extension (multi-output support, preferred)
|
||||
- **VidMode:** X11 VidMode extension (legacy, single output)
|
||||
@@ -115,6 +122,7 @@ Each method requires different system APIs for gamma manipulation. Conditional c
|
||||
## Configuration
|
||||
|
||||
Configuration files should be in INI format at:
|
||||
|
||||
- `~/.config/redshift.conf` (XDG standard location)
|
||||
|
||||
Maintain this format for compatibility with the original.
|
||||
@@ -122,7 +130,8 @@ Maintain this format for compatibility with the original.
|
||||
## Testing Strategy
|
||||
|
||||
The original includes a dummy adjustment method for testing without actual display changes. Implement similar testing infrastructure:
|
||||
|
||||
- Unit tests for solar calculations
|
||||
- Unit tests for color temperature conversion
|
||||
- Integration tests with dummy/mock adjustment methods
|
||||
- Platform-specific tests gated by `cfg`
|
||||
- Always generate a coverage report at the end of a task and write tests for any uncovered lines or branches that were not previously ignored or skipped.
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
# Configuration File Support
|
||||
|
||||
Redshift supports configuration files in INI format, compatible with the original C version.
|
||||
|
||||
## Configuration File Locations
|
||||
|
||||
Redshift searches for configuration files in the following locations (in order of priority):
|
||||
|
||||
1. `$XDG_CONFIG_HOME/redshift/redshift.conf`
|
||||
2. `$XDG_CONFIG_HOME/redshift.conf` (fallback)
|
||||
3. `$HOME/.config/redshift/redshift.conf`
|
||||
4. `$HOME/.config/redshift.conf` (fallback)
|
||||
5. `/etc/redshift/redshift.conf` (system-wide)
|
||||
6. `/etc/redshift.conf` (system-wide fallback)
|
||||
|
||||
## Configuration Sections
|
||||
|
||||
### `[redshift]` - Main Settings
|
||||
|
||||
**Temperature settings:**
|
||||
- `temp-day` - Day color temperature in Kelvin (default: 6500)
|
||||
- `temp-night` - Night color temperature in Kelvin (default: 3500)
|
||||
|
||||
**Transition settings:**
|
||||
- `fade` or `transition` - Smooth transition (0=off, 1=on, default: 1)
|
||||
- `elevation-high` - Solar elevation for day in degrees (default: 3.0)
|
||||
- `elevation-low` - Solar elevation for night in degrees (default: -6.0)
|
||||
|
||||
**Time-based transitions (alternative to elevation):**
|
||||
- `dawn-time` - Dawn time range, e.g., `6:00-7:45`
|
||||
- `dusk-time` - Dusk time range, e.g., `18:35-20:15`
|
||||
|
||||
**Brightness settings:**
|
||||
- `brightness` - Single value for both day and night (0.1-1.0)
|
||||
- `brightness-day` - Day brightness (0.1-1.0)
|
||||
- `brightness-night` - Night brightness (0.1-1.0)
|
||||
|
||||
**Gamma settings:**
|
||||
- `gamma` - Single value for all RGB channels, or R:G:B format
|
||||
- Example: `gamma=0.8` (applies 0.8 to all channels)
|
||||
- Example: `gamma=0.8:0.7:0.9` (R=0.8, G=0.7, B=0.9)
|
||||
- `gamma-day` - Day gamma value(s)
|
||||
- `gamma-night` - Night gamma value(s)
|
||||
|
||||
**Provider/Method settings:**
|
||||
- `location-provider` - Location provider (manual, geoclue2)
|
||||
- `adjustment-method` - Gamma adjustment method (randr, dummy)
|
||||
|
||||
### `[manual]` - Manual Location
|
||||
|
||||
- `lat` - Latitude (-90 to 90)
|
||||
- `lon` - Longitude (-180 to 180)
|
||||
|
||||
**Note:** Longitudes west of Greenwich (e.g., Americas) are negative.
|
||||
|
||||
### `[randr]` - RandR Method Settings
|
||||
|
||||
- `screen` - X11 screen number to adjust (default: all screens)
|
||||
- `crtc` - Specific CRTC to adjust (optional)
|
||||
|
||||
## Priority Order
|
||||
|
||||
Settings are applied in the following priority order (highest to lowest):
|
||||
|
||||
1. **Command-line arguments** (highest priority)
|
||||
2. **INI configuration file**
|
||||
3. **Default values** (lowest priority)
|
||||
|
||||
### Examples
|
||||
|
||||
If you specify `-t 6000` on the command line and `temp-day=5700` in the config file, the command-line value (6000K) will be used.
|
||||
|
||||
## Sample Configuration
|
||||
|
||||
```ini
|
||||
[redshift]
|
||||
; Color temperature
|
||||
temp-day=5700
|
||||
temp-night=3500
|
||||
|
||||
; Enable smooth transitions
|
||||
fade=1
|
||||
|
||||
; Brightness (optional)
|
||||
brightness-day=0.9
|
||||
brightness-night=0.7
|
||||
|
||||
; Gamma correction (optional)
|
||||
gamma=0.8
|
||||
; Or per-channel: gamma=0.8:0.7:0.9
|
||||
|
||||
; Solar elevation thresholds (optional)
|
||||
;elevation-high=3
|
||||
;elevation-low=-6
|
||||
|
||||
; Or use time-based transitions (optional)
|
||||
;dawn-time=6:00-7:45
|
||||
;dusk-time=18:35-20:15
|
||||
|
||||
; Location and adjustment method
|
||||
location-provider=manual
|
||||
adjustment-method=randr
|
||||
|
||||
[manual]
|
||||
lat=40.7
|
||||
lon=-74.0
|
||||
|
||||
[randr]
|
||||
screen=0
|
||||
```
|
||||
|
||||
## Location Priority
|
||||
|
||||
Locations are determined in the following order:
|
||||
|
||||
1. **Command-line location** (`-l LAT:LON`)
|
||||
2. **INI config manual location** (`[manual]` section)
|
||||
3. **Saved TOML location** (from previous runs)
|
||||
4. **GeoClue2 automatic detection** (if available)
|
||||
5. **Interactive selection** (fallback)
|
||||
|
||||
## Command-Line Options
|
||||
|
||||
You can override any config file setting with command-line arguments:
|
||||
|
||||
```bash
|
||||
# Override temperature from config
|
||||
redshift -t 6000 --temp-night 4000
|
||||
|
||||
# Override brightness
|
||||
redshift -b 0.8
|
||||
|
||||
# Override gamma
|
||||
redshift -g 0.9
|
||||
|
||||
# Override location
|
||||
redshift -l 40.7:-74.0
|
||||
|
||||
# Print current settings
|
||||
redshift -p
|
||||
|
||||
# Verbose output (shows where settings come from)
|
||||
redshift -v
|
||||
```
|
||||
|
||||
## Compatibility
|
||||
|
||||
This implementation is compatible with the original Redshift configuration file format. Existing `redshift.conf` files should work without modification.
|
||||
@@ -18,9 +18,11 @@ dirs = "5.0"
|
||||
lazy_static = "1.5"
|
||||
dialoguer = "0.11"
|
||||
signal-hook = "0.3"
|
||||
rust-ini = "0.21"
|
||||
|
||||
[dev-dependencies]
|
||||
libc = "0.2"
|
||||
wait-timeout = "0.2"
|
||||
serial_test = "3.0"
|
||||
ctor = "0.2"
|
||||
tempfile = "3.8"
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
# INI Configuration File Implementation
|
||||
|
||||
This document describes the INI configuration file parsing implementation for Redshift.
|
||||
|
||||
## Overview
|
||||
|
||||
The Rust rewrite now supports reading configuration files in the same INI format as the original C version. This provides backward compatibility and allows users to configure Redshift without command-line arguments.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Files Added/Modified
|
||||
|
||||
1. **`src/config_ini.rs`** - New module for INI parsing
|
||||
- `RedshiftConfig` struct to hold all INI settings
|
||||
- Config file search logic (XDG directories)
|
||||
- Parsing functions for special formats (brightness, gamma, time ranges)
|
||||
|
||||
2. **`src/main.rs`** - Updated main program
|
||||
- Load INI config early in startup
|
||||
- Merge INI settings with CLI args (CLI takes priority)
|
||||
- Build transition scheme from merged settings
|
||||
- Support new CLI options: `-b/--brightness` and `-g/--gamma`
|
||||
|
||||
3. **`Cargo.toml`** - Added dependency
|
||||
- `rust-ini = "0.21"` for INI file parsing
|
||||
|
||||
4. **Test files:**
|
||||
- `tests/config_ini_tests.rs` - Unit tests for INI parsing
|
||||
- `tests/config_merging_tests.rs` - Integration tests for config merging
|
||||
|
||||
5. **Documentation:**
|
||||
- `CONFIG.md` - User-facing configuration guide
|
||||
- `redshift.conf.sample` - Sample configuration file
|
||||
|
||||
### Dependencies
|
||||
|
||||
- **rust-ini** (0.21): Mature, well-maintained INI parsing library
|
||||
- Simple API for reading INI files
|
||||
- Section-based organization matching our config format
|
||||
- Handles edge cases and malformed files gracefully
|
||||
|
||||
### Config File Search Order
|
||||
|
||||
The implementation searches for config files in these locations:
|
||||
|
||||
1. `$XDG_CONFIG_HOME/redshift/redshift.conf`
|
||||
2. `$XDG_CONFIG_HOME/redshift.conf`
|
||||
3. `$HOME/.config/redshift/redshift.conf`
|
||||
4. `$HOME/.config/redshift.conf`
|
||||
5. `/etc/redshift/redshift.conf`
|
||||
6. `/etc/redshift.conf`
|
||||
|
||||
This matches the C version's behavior for maximum compatibility.
|
||||
|
||||
### Supported Settings
|
||||
|
||||
#### `[redshift]` Section
|
||||
|
||||
| Setting | Format | Example | Description |
|
||||
|---------|--------|---------|-------------|
|
||||
| `temp-day` | Integer | `5700` | Day temperature in Kelvin |
|
||||
| `temp-night` | Integer | `3500` | Night temperature in Kelvin |
|
||||
| `fade` / `transition` | 0 or 1 | `1` | Enable smooth transitions |
|
||||
| `brightness` | Float or Float:Float | `0.9` or `0.7:0.4` | Brightness (day:night) |
|
||||
| `brightness-day` | Float | `0.9` | Day brightness (0.1-1.0) |
|
||||
| `brightness-night` | Float | `0.7` | Night brightness (0.1-1.0) |
|
||||
| `gamma` | Float or R:G:B | `0.8` or `0.8:0.7:0.9` | Gamma for all or RGB |
|
||||
| `gamma-day` | Float or R:G:B | `0.8:0.7:0.9` | Day gamma |
|
||||
| `gamma-night` | Float or R:G:B | `0.6` | Night gamma |
|
||||
| `elevation-high` | Float | `3.0` | Solar elevation for day (degrees) |
|
||||
| `elevation-low` | Float | `-6.0` | Solar elevation for night (degrees) |
|
||||
| `dawn-time` | HH:MM[-HH:MM] | `6:00-7:45` | Dawn time range |
|
||||
| `dusk-time` | HH:MM[-HH:MM] | `18:35-20:15` | Dusk time range |
|
||||
| `location-provider` | String | `manual` | Location provider |
|
||||
| `adjustment-method` | String | `randr` | Gamma method |
|
||||
|
||||
#### `[manual]` Section
|
||||
|
||||
| Setting | Format | Example | Description |
|
||||
|---------|--------|---------|-------------|
|
||||
| `lat` | Float | `40.7` | Latitude (-90 to 90) |
|
||||
| `lon` | Float | `-74.0` | Longitude (-180 to 180) |
|
||||
|
||||
#### `[randr]` Section
|
||||
|
||||
| Setting | Format | Example | Description |
|
||||
|---------|--------|---------|-------------|
|
||||
| `screen` | Integer | `0` | X11 screen number |
|
||||
| `crtc` | Integer | `0` | Specific CRTC ID |
|
||||
|
||||
### Parsing Functions
|
||||
|
||||
#### Brightness Parsing
|
||||
```rust
|
||||
parse_brightness_string(s: &str) -> Result<(f32, f32), String>
|
||||
```
|
||||
- Single value: `"0.9"` → `(0.9, 0.9)`
|
||||
- Separate values: `"0.7:0.4"` → `(0.7, 0.4)`
|
||||
|
||||
#### Gamma Parsing
|
||||
```rust
|
||||
parse_gamma_string(s: &str) -> Result<[f32; 3], String>
|
||||
```
|
||||
- Single value: `"0.8"` → `[0.8, 0.8, 0.8]`
|
||||
- RGB values: `"0.8:0.7:0.9"` → `[0.8, 0.7, 0.9]`
|
||||
|
||||
#### Time Range Parsing
|
||||
```rust
|
||||
parse_time_range(s: &str) -> Result<TimeRange, String>
|
||||
```
|
||||
- Single time: `"6:00"` → `TimeRange { start: 21600, end: 21600 }`
|
||||
- Range: `"6:00-7:45"` → `TimeRange { start: 21600, end: 27900 }`
|
||||
|
||||
Times are stored as seconds since midnight.
|
||||
|
||||
### Merging Strategy
|
||||
|
||||
Settings are applied with this priority:
|
||||
|
||||
1. **Command-line arguments** (highest)
|
||||
2. **INI configuration file**
|
||||
3. **Built-in defaults** (lowest)
|
||||
|
||||
Example:
|
||||
```bash
|
||||
# Config file has temp-day=5700
|
||||
# Running with: redshift -t 6000
|
||||
# Result: Uses 6000K (CLI override)
|
||||
```
|
||||
|
||||
Location is also merged with priority:
|
||||
|
||||
1. CLI location (`-l LAT:LON`)
|
||||
2. INI manual location (`[manual]` section)
|
||||
3. Saved TOML location (from previous runs)
|
||||
4. GeoClue2 automatic detection
|
||||
5. Interactive selection
|
||||
|
||||
### Validation
|
||||
|
||||
All settings are validated against bounds:
|
||||
|
||||
- Temperature: 1000-25000 K
|
||||
- Brightness: 0.1-1.0
|
||||
- Gamma: 0.1-10.0
|
||||
- Latitude: -90 to 90
|
||||
- Longitude: -180 to 180
|
||||
|
||||
Invalid values result in error messages and the program exits.
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Located in `src/config_ini.rs`:
|
||||
- `test_parse_brightness_single`
|
||||
- `test_parse_brightness_separate`
|
||||
- `test_parse_gamma_single`
|
||||
- `test_parse_gamma_rgb`
|
||||
- `test_parse_time`
|
||||
- `test_parse_time_range`
|
||||
- `test_parse_time_range_single`
|
||||
|
||||
### Integration Tests
|
||||
|
||||
**`tests/config_ini_tests.rs`**:
|
||||
- Full config file parsing
|
||||
- Individual section parsing
|
||||
- Error handling for malformed configs
|
||||
- Path search logic
|
||||
- Config file not found scenarios
|
||||
|
||||
**`tests/config_merging_tests.rs`**:
|
||||
- Temperature loading
|
||||
- Brightness loading (separate day/night)
|
||||
- Gamma loading (separate day/night)
|
||||
- Elevation settings
|
||||
- Time-based transitions
|
||||
- All sections together
|
||||
|
||||
All tests pass successfully.
|
||||
|
||||
### Manual Testing
|
||||
|
||||
Verified with sample config:
|
||||
```bash
|
||||
env HOME=/tmp/redshift-test ./target/release/redshift-rebooted -pv
|
||||
```
|
||||
|
||||
Output correctly shows:
|
||||
- Location from INI config
|
||||
- Temperature from config
|
||||
- Brightness from config
|
||||
- Gamma from config
|
||||
- CLI overrides work correctly
|
||||
|
||||
## Backward Compatibility
|
||||
|
||||
The implementation is fully compatible with the C version:
|
||||
|
||||
1. **Same file format**: Standard INI with sections
|
||||
2. **Same search paths**: XDG directories, fallbacks, etc.
|
||||
3. **Same section names**: `[redshift]`, `[manual]`, `[randr]`
|
||||
4. **Same setting names**: All original settings supported
|
||||
5. **Same aliases**: Both `fade` and `transition` work
|
||||
|
||||
Existing `redshift.conf` files work without modification.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Configuration
|
||||
|
||||
```ini
|
||||
[redshift]
|
||||
temp-day=5700
|
||||
temp-night=3500
|
||||
|
||||
[manual]
|
||||
lat=40.7
|
||||
lon=-74.0
|
||||
```
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
```ini
|
||||
[redshift]
|
||||
temp-day=5500
|
||||
temp-night=3200
|
||||
brightness-day=1.0
|
||||
brightness-night=0.75
|
||||
gamma-day=0.9:0.85:0.95
|
||||
gamma-night=0.7
|
||||
elevation-high=5
|
||||
elevation-low=-8
|
||||
fade=1
|
||||
|
||||
[manual]
|
||||
lat=51.5074
|
||||
lon=-0.1278
|
||||
```
|
||||
|
||||
### Time-Based Transitions
|
||||
|
||||
```ini
|
||||
[redshift]
|
||||
temp-day=6500
|
||||
temp-night=3500
|
||||
dawn-time=6:00-7:45
|
||||
dusk-time=18:35-20:15
|
||||
|
||||
[manual]
|
||||
lat=48.1
|
||||
lon=11.6
|
||||
```
|
||||
|
||||
## Command-Line Overrides
|
||||
|
||||
All config settings can be overridden:
|
||||
|
||||
```bash
|
||||
# Override temperature
|
||||
redshift -t 6000 --temp-night 4000
|
||||
|
||||
# Override brightness
|
||||
redshift -b 0.8:0.6
|
||||
|
||||
# Override gamma
|
||||
redshift -g 0.9
|
||||
|
||||
# Override with RGB gamma
|
||||
redshift -g 1.0:0.9:1.0
|
||||
|
||||
# Override location
|
||||
redshift -l 40.7:-74.0
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements for future versions:
|
||||
|
||||
1. Support for additional gamma methods (DRM, VidMode, etc.)
|
||||
2. Per-screen/CRTC brightness and gamma settings
|
||||
3. Hook scripts configuration
|
||||
4. Multiple location providers in config
|
||||
5. Configuration validation with detailed error messages
|
||||
6. Config file generation from CLI settings
|
||||
|
||||
## References
|
||||
|
||||
- Original C implementation: `legacy/src/config-ini.c`
|
||||
- Sample config: `legacy/redshift.conf.sample`
|
||||
- rust-ini crate: https://crates.io/crates/rust-ini
|
||||
@@ -0,0 +1,65 @@
|
||||
; Sample Redshift configuration file
|
||||
|
||||
; Global settings for redshift
|
||||
[redshift]
|
||||
; Set the day and night screen temperatures (in Kelvin)
|
||||
temp-day=5700
|
||||
temp-night=3500
|
||||
|
||||
; Disable the smooth fade between temperatures when Redshift starts and stops.
|
||||
; 0 will cause an immediate change between screen temperatures.
|
||||
; 1 will gradually apply the new screen temperature over a couple of seconds.
|
||||
fade=1
|
||||
|
||||
; Solar elevation thresholds.
|
||||
; By default, Redshift will use the current elevation of the sun to determine
|
||||
; whether it is daytime, night or in transition (dawn/dusk). When the sun is
|
||||
; above the degrees specified with elevation-high it is considered daytime and
|
||||
; below elevation-low it is considered night.
|
||||
;elevation-high=3
|
||||
;elevation-low=-6
|
||||
|
||||
; Custom dawn/dusk intervals.
|
||||
; Instead of using the solar elevation, the time intervals of dawn and dusk
|
||||
; can be specified manually. The times must be specified as HH:MM in 24-hour
|
||||
; format. You can specify a time range like 6:00-7:45.
|
||||
;dawn-time=6:00-7:45
|
||||
;dusk-time=18:35-20:15
|
||||
|
||||
; Set the screen brightness. Default is 1.0.
|
||||
;brightness=0.9
|
||||
; It is also possible to use different settings for day and night
|
||||
;brightness-day=0.7
|
||||
;brightness-night=0.4
|
||||
|
||||
; Set the screen gamma (for all colors, or each color channel individually)
|
||||
; Single value applies to all RGB channels
|
||||
gamma=0.8
|
||||
; Or specify R:G:B separately
|
||||
;gamma=0.8:0.7:0.8
|
||||
; This can also be set individually for day and night
|
||||
;gamma-day=0.8:0.7:0.8
|
||||
;gamma-night=0.6
|
||||
|
||||
; Set the location-provider: 'geoclue2', 'manual'
|
||||
; The location provider settings are in a different section.
|
||||
location-provider=manual
|
||||
|
||||
; Set the adjustment-method: 'randr', 'dummy'
|
||||
; 'randr' is the preferred method for X11
|
||||
; The adjustment method settings are in a different section.
|
||||
adjustment-method=randr
|
||||
|
||||
; Configuration of the location-provider:
|
||||
; Keep in mind that longitudes west of Greenwich (e.g. the Americas)
|
||||
; are negative numbers.
|
||||
[manual]
|
||||
lat=48.1
|
||||
lon=11.6
|
||||
|
||||
; Configuration of the adjustment-method
|
||||
; In this example, randr is configured to adjust only screen 0.
|
||||
; Note that the numbering starts from 0, so this is actually the first screen.
|
||||
; If this option is not specified, Redshift will try to adjust all screens.
|
||||
[randr]
|
||||
screen=0
|
||||
@@ -0,0 +1,340 @@
|
||||
/// INI Configuration file support for Redshift
|
||||
/// Parses redshift.conf files in INI format (matching the C version)
|
||||
|
||||
use crate::types::*;
|
||||
use ini::Ini;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Configuration loaded from INI file
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct RedshiftConfig {
|
||||
/* Redshift section settings */
|
||||
pub temp_day: Option<i32>,
|
||||
pub temp_night: Option<i32>,
|
||||
pub fade: Option<bool>,
|
||||
pub brightness_day: Option<f32>,
|
||||
pub brightness_night: Option<f32>,
|
||||
pub gamma_day: Option<[f32; 3]>,
|
||||
pub gamma_night: Option<[f32; 3]>,
|
||||
pub elevation_high: Option<f64>,
|
||||
pub elevation_low: Option<f64>,
|
||||
pub dawn_time: Option<TimeRange>,
|
||||
pub dusk_time: Option<TimeRange>,
|
||||
pub location_provider: Option<String>,
|
||||
pub adjustment_method: Option<String>,
|
||||
|
||||
/* Manual location section */
|
||||
pub manual_lat: Option<f32>,
|
||||
pub manual_lon: Option<f32>,
|
||||
|
||||
/* Gamma method settings */
|
||||
pub randr_screen: Option<i32>,
|
||||
pub randr_crtc: Option<i32>,
|
||||
}
|
||||
|
||||
impl RedshiftConfig {
|
||||
/// Find and load the INI config file from standard locations
|
||||
pub fn load() -> Result<Self, String> {
|
||||
if let Some(path) = Self::find_config_file() {
|
||||
Self::load_from_file(&path)
|
||||
} else {
|
||||
Ok(Self::default())
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the config file in standard XDG locations
|
||||
pub fn find_config_file() -> Option<PathBuf> {
|
||||
let paths = Self::get_config_search_paths();
|
||||
|
||||
for path in paths {
|
||||
if path.exists() {
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Get list of paths to search for config file (in priority order)
|
||||
pub fn get_config_search_paths() -> Vec<PathBuf> {
|
||||
let mut paths = Vec::new();
|
||||
|
||||
/* Priority 1: XDG_CONFIG_HOME/redshift/redshift.conf */
|
||||
if let Ok(xdg_config) = std::env::var("XDG_CONFIG_HOME") {
|
||||
if !xdg_config.is_empty() {
|
||||
paths.push(PathBuf::from(&xdg_config).join("redshift").join("redshift.conf"));
|
||||
/* Fallback: XDG_CONFIG_HOME/redshift.conf */
|
||||
paths.push(PathBuf::from(&xdg_config).join("redshift.conf"));
|
||||
}
|
||||
}
|
||||
|
||||
/* Priority 2: HOME/.config/redshift/redshift.conf */
|
||||
if let Some(home_dir) = dirs::home_dir() {
|
||||
paths.push(home_dir.join(".config").join("redshift").join("redshift.conf"));
|
||||
/* Fallback: HOME/.config/redshift.conf */
|
||||
paths.push(home_dir.join(".config").join("redshift.conf"));
|
||||
}
|
||||
|
||||
/* Priority 3: System-wide configs */
|
||||
paths.push(PathBuf::from("/etc/redshift/redshift.conf"));
|
||||
paths.push(PathBuf::from("/etc/redshift.conf"));
|
||||
|
||||
paths
|
||||
}
|
||||
|
||||
/// Load config from a specific file
|
||||
pub fn load_from_file(path: &PathBuf) -> Result<Self, String> {
|
||||
let ini = Ini::load_from_file(path)
|
||||
.map_err(|e| format!("Failed to load INI file: {}", e))?;
|
||||
|
||||
let mut config = Self::default();
|
||||
|
||||
/* Parse [redshift] section */
|
||||
if let Some(section) = ini.section(Some("redshift")) {
|
||||
if let Some(val) = section.get("temp-day") {
|
||||
config.temp_day = val.parse().ok();
|
||||
}
|
||||
if let Some(val) = section.get("temp-night") {
|
||||
config.temp_night = val.parse().ok();
|
||||
}
|
||||
if let Some(val) = section.get("fade") {
|
||||
config.fade = match val {
|
||||
"0" => Some(false),
|
||||
"1" => Some(true),
|
||||
_ => val.parse().ok(),
|
||||
};
|
||||
}
|
||||
if let Some(val) = section.get("transition") {
|
||||
config.fade = match val {
|
||||
"0" => Some(false),
|
||||
"1" => Some(true),
|
||||
_ => val.parse().ok(),
|
||||
};
|
||||
}
|
||||
|
||||
/* Brightness settings */
|
||||
if let Some(val) = section.get("brightness") {
|
||||
if let Ok((day, night)) = parse_brightness_string(val) {
|
||||
config.brightness_day = Some(day);
|
||||
config.brightness_night = Some(night);
|
||||
}
|
||||
}
|
||||
if let Some(val) = section.get("brightness-day") {
|
||||
config.brightness_day = val.parse().ok();
|
||||
}
|
||||
if let Some(val) = section.get("brightness-night") {
|
||||
config.brightness_night = val.parse().ok();
|
||||
}
|
||||
|
||||
/* Gamma settings */
|
||||
if let Some(val) = section.get("gamma") {
|
||||
if let Ok(gamma) = parse_gamma_string(val) {
|
||||
config.gamma_day = Some(gamma);
|
||||
config.gamma_night = Some(gamma);
|
||||
}
|
||||
}
|
||||
if let Some(val) = section.get("gamma-day") {
|
||||
if let Ok(gamma) = parse_gamma_string(val) {
|
||||
config.gamma_day = Some(gamma);
|
||||
}
|
||||
}
|
||||
if let Some(val) = section.get("gamma-night") {
|
||||
if let Ok(gamma) = parse_gamma_string(val) {
|
||||
config.gamma_night = Some(gamma);
|
||||
}
|
||||
}
|
||||
|
||||
/* Elevation settings */
|
||||
if let Some(val) = section.get("elevation-high") {
|
||||
config.elevation_high = val.parse().ok();
|
||||
}
|
||||
if let Some(val) = section.get("elevation-low") {
|
||||
config.elevation_low = val.parse().ok();
|
||||
}
|
||||
|
||||
/* Time-based transition settings */
|
||||
if let Some(val) = section.get("dawn-time") {
|
||||
config.dawn_time = parse_time_range(val).ok();
|
||||
}
|
||||
if let Some(val) = section.get("dusk-time") {
|
||||
config.dusk_time = parse_time_range(val).ok();
|
||||
}
|
||||
|
||||
/* Provider/method settings */
|
||||
if let Some(val) = section.get("location-provider") {
|
||||
config.location_provider = Some(val.to_string());
|
||||
}
|
||||
if let Some(val) = section.get("adjustment-method") {
|
||||
config.adjustment_method = Some(val.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/* Parse [manual] section for location */
|
||||
if let Some(section) = ini.section(Some("manual")) {
|
||||
if let Some(val) = section.get("lat") {
|
||||
config.manual_lat = val.parse().ok();
|
||||
}
|
||||
if let Some(val) = section.get("lon") {
|
||||
config.manual_lon = val.parse().ok();
|
||||
}
|
||||
}
|
||||
|
||||
/* Parse [randr] section for gamma method settings */
|
||||
if let Some(section) = ini.section(Some("randr")) {
|
||||
if let Some(val) = section.get("screen") {
|
||||
config.randr_screen = val.parse().ok();
|
||||
}
|
||||
if let Some(val) = section.get("crtc") {
|
||||
config.randr_crtc = val.parse().ok();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Get manual location if specified
|
||||
pub fn get_manual_location(&self) -> Option<Location> {
|
||||
if let (Some(lat), Some(lon)) = (self.manual_lat, self.manual_lon) {
|
||||
Some(Location { lat, lon })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse brightness string: "0.9" or "0.7:0.4" (day:night)
|
||||
pub fn parse_brightness_string(s: &str) -> Result<(f32, f32), String> {
|
||||
let parts: Vec<&str> = s.split(':').collect();
|
||||
|
||||
if parts.len() == 1 {
|
||||
/* Same value for day and night */
|
||||
let val: f32 = parts[0].parse()
|
||||
.map_err(|_| format!("Invalid brightness value: {}", parts[0]))?;
|
||||
Ok((val, val))
|
||||
} else if parts.len() == 2 {
|
||||
/* Separate values for day and night */
|
||||
let day: f32 = parts[0].parse()
|
||||
.map_err(|_| format!("Invalid day brightness: {}", parts[0]))?;
|
||||
let night: f32 = parts[1].parse()
|
||||
.map_err(|_| format!("Invalid night brightness: {}", parts[1]))?;
|
||||
Ok((day, night))
|
||||
} else {
|
||||
Err("Brightness must be single value or day:night".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse gamma string: "0.8" or "0.8:0.7:0.8" (R:G:B)
|
||||
pub fn parse_gamma_string(s: &str) -> Result<[f32; 3], String> {
|
||||
let parts: Vec<&str> = s.split(':').collect();
|
||||
|
||||
if parts.len() == 1 {
|
||||
/* Use same value for all channels */
|
||||
let val: f32 = parts[0].parse()
|
||||
.map_err(|_| format!("Invalid gamma value: {}", parts[0]))?;
|
||||
Ok([val, val, val])
|
||||
} else if parts.len() == 3 {
|
||||
/* Separate values for R, G, B */
|
||||
let r: f32 = parts[0].parse()
|
||||
.map_err(|_| format!("Invalid red gamma: {}", parts[0]))?;
|
||||
let g: f32 = parts[1].parse()
|
||||
.map_err(|_| format!("Invalid green gamma: {}", parts[1]))?;
|
||||
let b: f32 = parts[2].parse()
|
||||
.map_err(|_| format!("Invalid blue gamma: {}", parts[2]))?;
|
||||
Ok([r, g, b])
|
||||
} else {
|
||||
Err("Gamma must be single value or R:G:B".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse time range string: "6:00" or "6:00-7:45"
|
||||
fn parse_time_range(s: &str) -> Result<TimeRange, String> {
|
||||
let parts: Vec<&str> = s.split('-').collect();
|
||||
|
||||
let start_time = parse_time(parts[0])?;
|
||||
let end_time = if parts.len() == 2 {
|
||||
parse_time(parts[1])?
|
||||
} else if parts.len() == 1 {
|
||||
start_time
|
||||
} else {
|
||||
return Err("Time range must be HH:MM or HH:MM-HH:MM".to_string());
|
||||
};
|
||||
|
||||
Ok(TimeRange {
|
||||
start: start_time,
|
||||
end: end_time,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse time string "HH:MM" to seconds since midnight
|
||||
fn parse_time(s: &str) -> Result<i32, String> {
|
||||
let parts: Vec<&str> = s.split(':').collect();
|
||||
|
||||
if parts.len() != 2 {
|
||||
return Err(format!("Time must be in HH:MM format: {}", s));
|
||||
}
|
||||
|
||||
let hours: i32 = parts[0].parse()
|
||||
.map_err(|_| format!("Invalid hour: {}", parts[0]))?;
|
||||
let minutes: i32 = parts[1].parse()
|
||||
.map_err(|_| format!("Invalid minute: {}", parts[1]))?;
|
||||
|
||||
if hours < 0 || hours >= 24 {
|
||||
return Err(format!("Hours must be 0-23: {}", hours));
|
||||
}
|
||||
if minutes < 0 || minutes >= 60 {
|
||||
return Err(format!("Minutes must be 0-59: {}", minutes));
|
||||
}
|
||||
|
||||
Ok(hours * 3600 + minutes * 60)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_brightness_single() {
|
||||
let (day, night) = parse_brightness_string("0.9").unwrap();
|
||||
assert_eq!(day, 0.9);
|
||||
assert_eq!(night, 0.9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_brightness_separate() {
|
||||
let (day, night) = parse_brightness_string("0.7:0.4").unwrap();
|
||||
assert_eq!(day, 0.7);
|
||||
assert_eq!(night, 0.4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_gamma_single() {
|
||||
let gamma = parse_gamma_string("0.8").unwrap();
|
||||
assert_eq!(gamma, [0.8, 0.8, 0.8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_gamma_rgb() {
|
||||
let gamma = parse_gamma_string("0.8:0.7:0.9").unwrap();
|
||||
assert_eq!(gamma, [0.8, 0.7, 0.9]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_time() {
|
||||
assert_eq!(parse_time("6:00").unwrap(), 6 * 3600);
|
||||
assert_eq!(parse_time("18:30").unwrap(), 18 * 3600 + 30 * 60);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_time_range() {
|
||||
let range = parse_time_range("6:00-7:45").unwrap();
|
||||
assert_eq!(range.start, 6 * 3600);
|
||||
assert_eq!(range.end, 7 * 3600 + 45 * 60);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_time_range_single() {
|
||||
let range = parse_time_range("6:00").unwrap();
|
||||
assert_eq!(range.start, 6 * 3600);
|
||||
assert_eq!(range.end, 6 * 3600);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod cities;
|
||||
pub mod colorramp;
|
||||
pub mod config;
|
||||
pub mod config_ini;
|
||||
pub mod gamma;
|
||||
pub mod gamma_guard;
|
||||
pub mod gamma_randr;
|
||||
|
||||
+151
-14
@@ -1,6 +1,7 @@
|
||||
mod cities;
|
||||
mod colorramp;
|
||||
mod config;
|
||||
mod config_ini;
|
||||
mod gamma;
|
||||
mod gamma_guard;
|
||||
mod gamma_randr;
|
||||
@@ -67,6 +68,34 @@ struct Args {
|
||||
/// Night temperature (default: 3500K)
|
||||
#[arg(long, default_value = "3500")]
|
||||
temp_night: i32,
|
||||
|
||||
/// Brightness (day:night or single value)
|
||||
#[arg(short = 'b', long)]
|
||||
brightness: Option<String>,
|
||||
|
||||
/// Gamma (R:G:B or single value)
|
||||
#[arg(short = 'g', long)]
|
||||
gamma: Option<String>,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
/// Merge with INI config (CLI args take priority)
|
||||
fn merge_with_ini(&mut self, ini_config: &config_ini::RedshiftConfig) {
|
||||
/* Temperature settings - only use INI if CLI used defaults */
|
||||
if self.temp_day == 6500 {
|
||||
if let Some(temp) = ini_config.temp_day {
|
||||
self.temp_day = temp;
|
||||
}
|
||||
}
|
||||
if self.temp_night == 3500 {
|
||||
if let Some(temp) = ini_config.temp_night {
|
||||
self.temp_night = temp;
|
||||
}
|
||||
}
|
||||
|
||||
/* Brightness and gamma - these are new, so always use from INI if not in CLI */
|
||||
/* These will be handled separately when building the scheme */
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_location(loc_str: &str) -> Result<Location, String> {
|
||||
@@ -215,8 +244,11 @@ fn ease_fade(t: f64) -> f64 {
|
||||
t * t * (3.0 - 2.0 * t)
|
||||
}
|
||||
|
||||
/// Determine location using priority system
|
||||
fn determine_location(args: &Args) -> Result<(Location, Config), Box<dyn std::error::Error>> {
|
||||
/// Determine location using priority system (with INI config support)
|
||||
fn determine_location_with_ini(
|
||||
args: &Args,
|
||||
ini_config: &config_ini::RedshiftConfig,
|
||||
) -> Result<(Location, Config), Box<dyn std::error::Error>> {
|
||||
// Priority 1: Command-line argument
|
||||
if let Some(loc_str) = &args.location {
|
||||
let loc = parse_location(loc_str)?;
|
||||
@@ -253,7 +285,15 @@ fn determine_location(args: &Args) -> Result<(Location, Config), Box<dyn std::er
|
||||
// Load or create config
|
||||
let mut config = Config::load().unwrap_or_default();
|
||||
|
||||
// Priority 2: Try GeoClue2 if it's time for daily check
|
||||
// Priority 2: INI config file manual location
|
||||
if let Some(ini_loc) = ini_config.get_manual_location() {
|
||||
if args.verbose {
|
||||
println!("Using location from INI config: {:.4}, {:.4}", ini_loc.lat, ini_loc.lon);
|
||||
}
|
||||
return Ok((ini_loc, config));
|
||||
}
|
||||
|
||||
// Priority 3: Try GeoClue2 if it's time for daily check
|
||||
if config.should_check_geoclue() {
|
||||
if args.verbose {
|
||||
eprintln!("Checking for automatic location via GeoClue2...");
|
||||
@@ -276,7 +316,7 @@ fn determine_location(args: &Args) -> Result<(Location, Config), Box<dyn std::er
|
||||
config.save().ok();
|
||||
}
|
||||
|
||||
// Priority 3: Use saved configuration
|
||||
// Priority 4: Use saved TOML configuration
|
||||
if let Some(saved_loc) = config.get_location() {
|
||||
if args.verbose {
|
||||
let source_name = config.location.as_ref().map(|l| match l.source {
|
||||
@@ -297,7 +337,7 @@ fn determine_location(args: &Args) -> Result<(Location, Config), Box<dyn std::er
|
||||
return Ok((saved_loc, config));
|
||||
}
|
||||
|
||||
// Priority 4: Interactive selection
|
||||
// Priority 5: Interactive selection
|
||||
if args.no_auto_location {
|
||||
eprintln!("Error: --no-auto-location requires -l LAT:LON or saved configuration");
|
||||
std::process::exit(1);
|
||||
@@ -329,12 +369,110 @@ fn try_geoclue2(verbose: bool) -> Result<Location, String> {
|
||||
provider.get_location()
|
||||
}
|
||||
|
||||
/// Build transition scheme from args and INI config
|
||||
fn build_transition_scheme(
|
||||
args: &Args,
|
||||
ini_config: &config_ini::RedshiftConfig,
|
||||
) -> Result<TransitionScheme, String> {
|
||||
let mut scheme = TransitionScheme::default();
|
||||
|
||||
/* Set temperatures from merged args */
|
||||
scheme.day.temperature = args.temp_day;
|
||||
scheme.night.temperature = args.temp_night;
|
||||
|
||||
/* Parse and apply brightness from CLI or INI */
|
||||
if let Some(ref brightness_str) = args.brightness {
|
||||
let (day, night) = config_ini::parse_brightness_string(brightness_str)?;
|
||||
scheme.day.brightness = day;
|
||||
scheme.night.brightness = night;
|
||||
} else {
|
||||
if let Some(day) = ini_config.brightness_day {
|
||||
scheme.day.brightness = day;
|
||||
}
|
||||
if let Some(night) = ini_config.brightness_night {
|
||||
scheme.night.brightness = night;
|
||||
}
|
||||
}
|
||||
|
||||
/* Parse and apply gamma from CLI or INI */
|
||||
if let Some(ref gamma_str) = args.gamma {
|
||||
let gamma = config_ini::parse_gamma_string(gamma_str)?;
|
||||
scheme.day.gamma = gamma;
|
||||
scheme.night.gamma = gamma;
|
||||
} else {
|
||||
if let Some(gamma) = ini_config.gamma_day {
|
||||
scheme.day.gamma = gamma;
|
||||
}
|
||||
if let Some(gamma) = ini_config.gamma_night {
|
||||
scheme.night.gamma = gamma;
|
||||
}
|
||||
}
|
||||
|
||||
/* Apply elevation settings from INI */
|
||||
if let Some(high) = ini_config.elevation_high {
|
||||
scheme.high = high;
|
||||
}
|
||||
if let Some(low) = ini_config.elevation_low {
|
||||
scheme.low = low;
|
||||
}
|
||||
|
||||
/* Apply time-based transition if specified */
|
||||
if let Some(dawn) = ini_config.dawn_time {
|
||||
scheme.use_time = true;
|
||||
scheme.dawn = dawn;
|
||||
}
|
||||
if let Some(dusk) = ini_config.dusk_time {
|
||||
scheme.use_time = true;
|
||||
scheme.dusk = dusk;
|
||||
}
|
||||
|
||||
/* Validate brightness bounds */
|
||||
if scheme.day.brightness < MIN_BRIGHTNESS || scheme.day.brightness > MAX_BRIGHTNESS {
|
||||
return Err(format!(
|
||||
"Day brightness must be between {} and {}",
|
||||
MIN_BRIGHTNESS, MAX_BRIGHTNESS
|
||||
));
|
||||
}
|
||||
if scheme.night.brightness < MIN_BRIGHTNESS || scheme.night.brightness > MAX_BRIGHTNESS {
|
||||
return Err(format!(
|
||||
"Night brightness must be between {} and {}",
|
||||
MIN_BRIGHTNESS, MAX_BRIGHTNESS
|
||||
));
|
||||
}
|
||||
|
||||
/* Validate gamma bounds */
|
||||
for &gamma in &scheme.day.gamma {
|
||||
if gamma < MIN_GAMMA || gamma > MAX_GAMMA {
|
||||
return Err(format!(
|
||||
"Day gamma must be between {} and {}",
|
||||
MIN_GAMMA, MAX_GAMMA
|
||||
));
|
||||
}
|
||||
}
|
||||
for &gamma in &scheme.night.gamma {
|
||||
if gamma < MIN_GAMMA || gamma > MAX_GAMMA {
|
||||
return Err(format!(
|
||||
"Night gamma must be between {} and {}",
|
||||
MIN_GAMMA, MAX_GAMMA
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(scheme)
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args = Args::parse();
|
||||
let mut args = Args::parse();
|
||||
|
||||
/* Install signal handlers for graceful shutdown and mode toggling */
|
||||
signals::install_handlers()?;
|
||||
|
||||
/* Load INI configuration file */
|
||||
let ini_config = config_ini::RedshiftConfig::load().unwrap_or_default();
|
||||
|
||||
/* Merge INI config with CLI args (CLI takes priority) */
|
||||
args.merge_with_ini(&ini_config);
|
||||
|
||||
/* Validate temperature bounds */
|
||||
if args.temp_day < MIN_TEMP || args.temp_day > MAX_TEMP {
|
||||
eprintln!(
|
||||
@@ -353,11 +491,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
/* Determine location using priority system:
|
||||
1. Command-line argument (-l LAT:LON)
|
||||
2. Saved configuration file
|
||||
3. GeoClue2 automatic detection (with daily retry)
|
||||
4. Interactive selection (country/city list)
|
||||
2. INI config file manual location
|
||||
3. Saved TOML configuration file
|
||||
4. GeoClue2 automatic detection (with daily retry)
|
||||
5. Interactive selection (country/city list)
|
||||
*/
|
||||
let (location, mut config) = determine_location(&args)?;
|
||||
let (location, mut config) = determine_location_with_ini(&args, &ini_config)?;
|
||||
|
||||
/* Set up gamma method */
|
||||
let mut gamma_method: Box<dyn GammaMethod> = match args.method {
|
||||
@@ -368,10 +507,8 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
gamma_method.init()?;
|
||||
gamma_method.start()?;
|
||||
|
||||
/* Create transition scheme */
|
||||
let mut scheme = TransitionScheme::default();
|
||||
scheme.day.temperature = args.temp_day;
|
||||
scheme.night.temperature = args.temp_night;
|
||||
/* Create transition scheme from args and INI config */
|
||||
let scheme = build_transition_scheme(&args, &ini_config)?;
|
||||
|
||||
/* Get current period and color setting */
|
||||
let (period, color_setting) = get_current_period(&location, &scheme);
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
use redshift_rebooted::config_ini::*;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_parse_brightness_single_value() {
|
||||
let (day, night) = parse_brightness_string("0.9").unwrap();
|
||||
assert_eq!(day, 0.9);
|
||||
assert_eq!(night, 0.9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_brightness_separate_values() {
|
||||
let (day, night) = parse_brightness_string("0.8:0.5").unwrap();
|
||||
assert_eq!(day, 0.8);
|
||||
assert_eq!(night, 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_brightness_invalid() {
|
||||
assert!(parse_brightness_string("0.8:0.5:0.3").is_err());
|
||||
assert!(parse_brightness_string("invalid").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_gamma_single_value() {
|
||||
let gamma = parse_gamma_string("0.8").unwrap();
|
||||
assert_eq!(gamma, [0.8, 0.8, 0.8]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_gamma_rgb_values() {
|
||||
let gamma = parse_gamma_string("0.8:0.7:0.9").unwrap();
|
||||
assert_eq!(gamma, [0.8, 0.7, 0.9]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_gamma_invalid() {
|
||||
assert!(parse_gamma_string("0.8:0.7").is_err()); // Only 2 values
|
||||
assert!(parse_gamma_string("invalid").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_full_config() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config_path = temp_dir.path().join("redshift.conf");
|
||||
|
||||
let config_content = r#"
|
||||
[redshift]
|
||||
temp-day=5700
|
||||
temp-night=3500
|
||||
fade=1
|
||||
brightness-day=0.9
|
||||
brightness-night=0.7
|
||||
gamma=0.8:0.7:0.8
|
||||
elevation-high=3
|
||||
elevation-low=-6
|
||||
dawn-time=6:00-7:45
|
||||
dusk-time=18:35-20:15
|
||||
location-provider=manual
|
||||
adjustment-method=randr
|
||||
|
||||
[manual]
|
||||
lat=40.7
|
||||
lon=-74.0
|
||||
|
||||
[randr]
|
||||
screen=0
|
||||
"#;
|
||||
|
||||
let mut file = fs::File::create(&config_path).unwrap();
|
||||
file.write_all(config_content.as_bytes()).unwrap();
|
||||
|
||||
let config = RedshiftConfig::load_from_file(&config_path).unwrap();
|
||||
|
||||
assert_eq!(config.temp_day, Some(5700));
|
||||
assert_eq!(config.temp_night, Some(3500));
|
||||
assert_eq!(config.fade, Some(true));
|
||||
assert_eq!(config.brightness_day, Some(0.9));
|
||||
assert_eq!(config.brightness_night, Some(0.7));
|
||||
assert_eq!(config.gamma_day, Some([0.8, 0.7, 0.8]));
|
||||
assert_eq!(config.gamma_night, Some([0.8, 0.7, 0.8]));
|
||||
assert_eq!(config.elevation_high, Some(3.0));
|
||||
assert_eq!(config.elevation_low, Some(-6.0));
|
||||
assert_eq!(config.manual_lat, Some(40.7));
|
||||
assert_eq!(config.manual_lon, Some(-74.0));
|
||||
assert_eq!(config.randr_screen, Some(0));
|
||||
|
||||
// Check time ranges
|
||||
assert!(config.dawn_time.is_some());
|
||||
let dawn = config.dawn_time.unwrap();
|
||||
assert_eq!(dawn.start, 6 * 3600); // 6:00
|
||||
assert_eq!(dawn.end, 7 * 3600 + 45 * 60); // 7:45
|
||||
|
||||
assert!(config.dusk_time.is_some());
|
||||
let dusk = config.dusk_time.unwrap();
|
||||
assert_eq!(dusk.start, 18 * 3600 + 35 * 60); // 18:35
|
||||
assert_eq!(dusk.end, 20 * 3600 + 15 * 60); // 20:15
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_minimal_config() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config_path = temp_dir.path().join("redshift.conf");
|
||||
|
||||
let config_content = r#"
|
||||
[redshift]
|
||||
temp-day=6500
|
||||
temp-night=4000
|
||||
"#;
|
||||
|
||||
let mut file = fs::File::create(&config_path).unwrap();
|
||||
file.write_all(config_content.as_bytes()).unwrap();
|
||||
|
||||
let config = RedshiftConfig::load_from_file(&config_path).unwrap();
|
||||
|
||||
assert_eq!(config.temp_day, Some(6500));
|
||||
assert_eq!(config.temp_night, Some(4000));
|
||||
assert_eq!(config.brightness_day, None);
|
||||
assert_eq!(config.brightness_night, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transition_alias() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config_path = temp_dir.path().join("redshift.conf");
|
||||
|
||||
let config_content = r#"
|
||||
[redshift]
|
||||
transition=0
|
||||
"#;
|
||||
|
||||
let mut file = fs::File::create(&config_path).unwrap();
|
||||
file.write_all(config_content.as_bytes()).unwrap();
|
||||
|
||||
let config = RedshiftConfig::load_from_file(&config_path).unwrap();
|
||||
|
||||
assert_eq!(config.fade, Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_brightness_single_value_config() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config_path = temp_dir.path().join("redshift.conf");
|
||||
|
||||
let config_content = r#"
|
||||
[redshift]
|
||||
brightness=0.8
|
||||
"#;
|
||||
|
||||
let mut file = fs::File::create(&config_path).unwrap();
|
||||
file.write_all(config_content.as_bytes()).unwrap();
|
||||
|
||||
let config = RedshiftConfig::load_from_file(&config_path).unwrap();
|
||||
|
||||
assert_eq!(config.brightness_day, Some(0.8));
|
||||
assert_eq!(config.brightness_night, Some(0.8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gamma_separate_day_night() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config_path = temp_dir.path().join("redshift.conf");
|
||||
|
||||
let config_content = r#"
|
||||
[redshift]
|
||||
gamma-day=0.8:0.7:0.8
|
||||
gamma-night=0.6
|
||||
"#;
|
||||
|
||||
let mut file = fs::File::create(&config_path).unwrap();
|
||||
file.write_all(config_content.as_bytes()).unwrap();
|
||||
|
||||
let config = RedshiftConfig::load_from_file(&config_path).unwrap();
|
||||
|
||||
assert_eq!(config.gamma_day, Some([0.8, 0.7, 0.8]));
|
||||
assert_eq!(config.gamma_night, Some([0.6, 0.6, 0.6]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_manual_location() {
|
||||
let mut config = RedshiftConfig::default();
|
||||
assert!(config.get_manual_location().is_none());
|
||||
|
||||
config.manual_lat = Some(40.7);
|
||||
config.manual_lon = Some(-74.0);
|
||||
|
||||
let loc = config.get_manual_location().unwrap();
|
||||
assert_eq!(loc.lat, 40.7);
|
||||
assert_eq!(loc.lon, -74.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_search_paths() {
|
||||
let paths = RedshiftConfig::get_config_search_paths();
|
||||
|
||||
// Should have at least the system-wide paths
|
||||
assert!(paths.len() >= 2);
|
||||
|
||||
// Last two should be system paths
|
||||
let len = paths.len();
|
||||
assert!(paths[len - 2].to_str().unwrap().contains("/etc/redshift"));
|
||||
assert!(paths[len - 1].to_str().unwrap().contains("/etc/redshift.conf"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nonexistent_config() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config_path = temp_dir.path().join("nonexistent.conf");
|
||||
|
||||
let result = RedshiftConfig::load_from_file(&config_path);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/// Integration tests for config merging behavior
|
||||
/// Tests that CLI args properly override INI config settings
|
||||
|
||||
use redshift_rebooted::config_ini::RedshiftConfig;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_config_loads_temperatures() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config_path = temp_dir.path().join("redshift.conf");
|
||||
|
||||
let config_content = r#"
|
||||
[redshift]
|
||||
temp-day=5700
|
||||
temp-night=3500
|
||||
"#;
|
||||
|
||||
let mut file = fs::File::create(&config_path).unwrap();
|
||||
file.write_all(config_content.as_bytes()).unwrap();
|
||||
|
||||
let config = RedshiftConfig::load_from_file(&config_path).unwrap();
|
||||
|
||||
assert_eq!(config.temp_day, Some(5700));
|
||||
assert_eq!(config.temp_night, Some(3500));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_loads_brightness_separate() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config_path = temp_dir.path().join("redshift.conf");
|
||||
|
||||
let config_content = r#"
|
||||
[redshift]
|
||||
brightness-day=0.9
|
||||
brightness-night=0.7
|
||||
"#;
|
||||
|
||||
let mut file = fs::File::create(&config_path).unwrap();
|
||||
file.write_all(config_content.as_bytes()).unwrap();
|
||||
|
||||
let config = RedshiftConfig::load_from_file(&config_path).unwrap();
|
||||
|
||||
assert_eq!(config.brightness_day, Some(0.9));
|
||||
assert_eq!(config.brightness_night, Some(0.7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_loads_gamma_separate_day_night() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config_path = temp_dir.path().join("redshift.conf");
|
||||
|
||||
let config_content = r#"
|
||||
[redshift]
|
||||
gamma-day=0.8:0.7:0.9
|
||||
gamma-night=0.6
|
||||
"#;
|
||||
|
||||
let mut file = fs::File::create(&config_path).unwrap();
|
||||
file.write_all(config_content.as_bytes()).unwrap();
|
||||
|
||||
let config = RedshiftConfig::load_from_file(&config_path).unwrap();
|
||||
|
||||
assert_eq!(config.gamma_day, Some([0.8, 0.7, 0.9]));
|
||||
assert_eq!(config.gamma_night, Some([0.6, 0.6, 0.6]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_loads_elevation_settings() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config_path = temp_dir.path().join("redshift.conf");
|
||||
|
||||
let config_content = r#"
|
||||
[redshift]
|
||||
elevation-high=3
|
||||
elevation-low=-6
|
||||
"#;
|
||||
|
||||
let mut file = fs::File::create(&config_path).unwrap();
|
||||
file.write_all(config_content.as_bytes()).unwrap();
|
||||
|
||||
let config = RedshiftConfig::load_from_file(&config_path).unwrap();
|
||||
|
||||
assert_eq!(config.elevation_high, Some(3.0));
|
||||
assert_eq!(config.elevation_low, Some(-6.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_loads_time_based_transition() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config_path = temp_dir.path().join("redshift.conf");
|
||||
|
||||
let config_content = r#"
|
||||
[redshift]
|
||||
dawn-time=6:00-7:45
|
||||
dusk-time=18:35-20:15
|
||||
"#;
|
||||
|
||||
let mut file = fs::File::create(&config_path).unwrap();
|
||||
file.write_all(config_content.as_bytes()).unwrap();
|
||||
|
||||
let config = RedshiftConfig::load_from_file(&config_path).unwrap();
|
||||
|
||||
assert!(config.dawn_time.is_some());
|
||||
assert!(config.dusk_time.is_some());
|
||||
|
||||
let dawn = config.dawn_time.unwrap();
|
||||
assert_eq!(dawn.start, 6 * 3600);
|
||||
assert_eq!(dawn.end, 7 * 3600 + 45 * 60);
|
||||
|
||||
let dusk = config.dusk_time.unwrap();
|
||||
assert_eq!(dusk.start, 18 * 3600 + 35 * 60);
|
||||
assert_eq!(dusk.end, 20 * 3600 + 15 * 60);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_with_all_sections() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config_path = temp_dir.path().join("redshift.conf");
|
||||
|
||||
let config_content = r#"
|
||||
[redshift]
|
||||
temp-day=5700
|
||||
temp-night=3500
|
||||
fade=1
|
||||
brightness-day=0.9
|
||||
brightness-night=0.7
|
||||
gamma=0.8
|
||||
elevation-high=3
|
||||
elevation-low=-6
|
||||
location-provider=manual
|
||||
adjustment-method=randr
|
||||
|
||||
[manual]
|
||||
lat=40.7
|
||||
lon=-74.0
|
||||
|
||||
[randr]
|
||||
screen=0
|
||||
"#;
|
||||
|
||||
let mut file = fs::File::create(&config_path).unwrap();
|
||||
file.write_all(config_content.as_bytes()).unwrap();
|
||||
|
||||
let config = RedshiftConfig::load_from_file(&config_path).unwrap();
|
||||
|
||||
// Redshift section
|
||||
assert_eq!(config.temp_day, Some(5700));
|
||||
assert_eq!(config.temp_night, Some(3500));
|
||||
assert_eq!(config.fade, Some(true));
|
||||
assert_eq!(config.brightness_day, Some(0.9));
|
||||
assert_eq!(config.brightness_night, Some(0.7));
|
||||
assert_eq!(config.gamma_day, Some([0.8, 0.8, 0.8]));
|
||||
assert_eq!(config.elevation_high, Some(3.0));
|
||||
assert_eq!(config.elevation_low, Some(-6.0));
|
||||
|
||||
// Manual location section
|
||||
assert_eq!(config.manual_lat, Some(40.7));
|
||||
assert_eq!(config.manual_lon, Some(-74.0));
|
||||
|
||||
let loc = config.get_manual_location().unwrap();
|
||||
assert_eq!(loc.lat, 40.7);
|
||||
assert_eq!(loc.lon, -74.0);
|
||||
|
||||
// Randr section
|
||||
assert_eq!(config.randr_screen, Some(0));
|
||||
}
|
||||
Reference in New Issue
Block a user