Skip to content

Commit 07f16e7

Browse files
authored
Merge pull request #1366 from cfsmp3/external_ip_address
Create block to display external IP address information
2 parents bd600d6 + 32283af commit 07f16e7

File tree

6 files changed

+349
-1
lines changed

6 files changed

+349
-1
lines changed

doc/blocks.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ You may find that the block you desire is not in the list below. In that case, f
2727
- [Custom DBus](#custom-dbus)
2828
- [Disk Space](#disk-space)
2929
- [Docker](#docker)
30+
- [ExternalIP](#external-ip)
3031
- [Focused Window](#focused-window)
3132
- [GitHub](#github)
3233
- [Hueshift](#hueshift)
@@ -649,6 +650,74 @@ Key | Value | Type
649650

650651
###### [↥ back to top](#list-of-available-blocks)
651652

653+
## ExternalIP
654+
655+
Creates a block which displays the external IP address and various information about it.
656+
657+
#### Examples
658+
659+
```toml
660+
[[block]]
661+
block = "externalip"
662+
format = "{ip} {country_code}"
663+
```
664+
665+
#### Options
666+
667+
Key | Values | Required | Default
668+
----|--------|----------|--------
669+
`format` | A string to customise the output of this block. See below for available placeholders. Text may need to be escaped, refer to [Escaping Text](#escaping-text). | No | `"{address} {country_flag}"`
670+
`refresh_interval_success` | Interval in seconds for automatic updates when the previous update was successful | No | 300
671+
`refresh_interval_failure` | Interval in seconds for automatic updates when the previous update failed | No | 15
672+
`with_network_manager` | If 'true', listen for NetworkManager events and update the IP immediately if there was a change | No | "true"
673+
674+
#### Available Format Keys
675+
676+
Key | Value | Type
677+
-----|-------|-----
678+
`{ip}` | The external IP address, as seen from a remote server | String
679+
`{version}` | IPv4 or IPv6 | String
680+
`{city}` | City name, such as "San Francisco" | Integer
681+
`{region}` | Region name, such as "California" | String
682+
`{region_code}` | Region code, such as "CA" for California | String
683+
`{country}` | Country code (2 letter, ISO 3166-1 alpha-2) | String
684+
`{country_name}` | Short country name | String
685+
`{country_code}` | Country code (2 letter, ISO 3166-1 alpha-2) | String
686+
`{country_code_iso3}` | Country code (3 letter, ISO 3166-1 alpha-3) | String
687+
`{country_capital}` | Capital of the country | String
688+
`{country_tld}` | Country specific TLD (top-level domain) | String
689+
`{continent_code}` | Continent code | String
690+
`{in_eu}` | Region code, such as "CA" | String
691+
`{postal}` | ZIP / Postal code | String
692+
`{latitude}` | Latitude | Float
693+
`{longitude}` | Longitude | Float
694+
`{timezone}` | City | String
695+
`{utc_offset}` | UTC offset (with daylight saving time) as +HHMM or -HHMM (HH is hours, MM is minutes) | String
696+
`{country_calling_code}` | Country calling code (dial in code, comma separated) | String
697+
`{currency}` | Currency code (ISO 4217) | String
698+
`{currency_name}` | Currency name | String
699+
`{languages}` | Languages spoken (comma separated 2 or 3 letter ISO 639 code with optional hyphen separated country suffix) | String
700+
`{country_area}` | Area of the country (in sq km) | Float
701+
`{country_population}` | Population of the country | Float
702+
`{timezone}` | Time zone | String
703+
`{org}` | Organization | String
704+
`{asn}` | Autonomous system (AS) | String
705+
`{country_flag}` | Flag of the country | String (glyph)
706+
707+
##### Notes
708+
All the information comes from https://ipapi.co/json/
709+
Check their documentation here: https://ipapi.co/api/#complete-location5
710+
711+
The IP is queried, 1) When i3status-rs starts, 2) When a signal is received
712+
on D-Bus about a network configuration change, 3) Every 5 minutes. This
713+
periodic refresh exists to catch IP updates that don't trigger a notification,
714+
for example due to a IP refresh at the router.
715+
716+
Flags: They are not icons but unicode glyphs. You will need a font that
717+
includes them. Tested with: https://www.babelstone.co.uk/Fonts/Flags.html
718+
719+
###### [↥ back to top](#list-of-available-blocks)
720+
652721
## Focused Window
653722

654723
Creates a block which displays the title or the active marks of the currently focused window. Uses push updates from i3 IPC, so no need to worry about resource usage. The block only updates when the focused window changes title or the focus changes. Also works with sway, due to it having compatibility with i3's IPC.

src/blocks.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub mod custom_dbus;
99
pub mod disk_space;
1010
pub mod dnf;
1111
pub mod docker;
12+
pub mod external_ip;
1213
pub mod focused_window;
1314
pub mod github;
1415
pub mod hueshift;
@@ -52,6 +53,7 @@ use self::custom_dbus::*;
5253
use self::disk_space::*;
5354
use self::dnf::*;
5455
use self::docker::*;
56+
use self::external_ip::*;
5557
use self::focused_window::*;
5658
use self::github::*;
5759
use self::hueshift::*;
@@ -234,6 +236,7 @@ pub fn create_block(
234236
"disk_space" => block!(DiskSpace, id, block_config, shared_config, update_request),
235237
"dnf" => block!(Dnf, id, block_config, shared_config, update_request),
236238
"docker" => block!(Docker, id, block_config, shared_config, update_request), ///////
239+
"external_ip" => block!(ExternalIP, id, block_config, shared_config, update_request),
237240
"focused_window" => block!(
238241
FocusedWindow,
239242
id,

src/blocks/external_ip.rs

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
use std::thread;
2+
use std::time::Instant;
3+
4+
use crossbeam_channel::Sender;
5+
use dbus::ffidisp::{BusType, Connection, ConnectionItem};
6+
use serde::{Deserialize as des, Serialize as ser};
7+
use serde_derive::Deserialize;
8+
9+
use crate::blocks::{Block, ConfigBlock, Update};
10+
use crate::config::SharedConfig;
11+
use crate::errors::*;
12+
use crate::formatting::value::Value;
13+
use crate::formatting::FormatTemplate;
14+
use crate::http;
15+
use crate::scheduler::Task;
16+
use crate::util::country_flag_from_iso_code;
17+
use crate::widgets::text::TextWidget;
18+
use crate::widgets::{I3BarWidget, State};
19+
use crate::Duration;
20+
21+
const API_ENDPOINT: &str = "https://ipapi.co/json/";
22+
const BLOCK_NAME: &str = "external_ip";
23+
24+
#[derive(ser, des, Default)]
25+
#[serde(default)]
26+
struct IPAddressInfo {
27+
error: bool,
28+
reason: String,
29+
ip: String,
30+
version: String,
31+
city: String,
32+
region: String,
33+
region_code: String,
34+
country: String,
35+
country_name: String,
36+
country_code: String,
37+
country_code_iso3: String,
38+
country_capital: String,
39+
country_tld: String,
40+
continent_code: String,
41+
in_eu: bool,
42+
postal: Option<String>,
43+
latitude: f64,
44+
longitude: f64,
45+
timezone: String,
46+
utc_offset: String,
47+
country_calling_code: String,
48+
currency: String,
49+
currency_name: String,
50+
languages: String,
51+
country_area: f64,
52+
country_population: f64,
53+
asn: String,
54+
org: String,
55+
}
56+
57+
pub struct ExternalIP {
58+
id: usize,
59+
output: TextWidget,
60+
format: FormatTemplate,
61+
refresh_interval_success: u64,
62+
refresh_interval_failure: u64,
63+
}
64+
65+
#[derive(Deserialize, Debug, Clone)]
66+
#[serde(deny_unknown_fields, default)]
67+
pub struct ExternalIPConfig {
68+
/// External IP formatter.
69+
pub format: FormatTemplate,
70+
pub refresh_interval_success: u64,
71+
pub refresh_interval_failure: u64,
72+
pub with_network_manager: bool,
73+
}
74+
75+
impl Default for ExternalIPConfig {
76+
fn default() -> Self {
77+
Self {
78+
format: FormatTemplate::default(),
79+
refresh_interval_success: 300,
80+
refresh_interval_failure: 15,
81+
with_network_manager: true,
82+
}
83+
}
84+
}
85+
86+
impl ConfigBlock for ExternalIP {
87+
type Config = ExternalIPConfig;
88+
89+
fn new(
90+
id: usize,
91+
block_config: Self::Config,
92+
shared_config: SharedConfig,
93+
send: Sender<Task>,
94+
) -> Result<Self> {
95+
if block_config.with_network_manager {
96+
thread::Builder::new()
97+
.name("externalip".into())
98+
.spawn(move || {
99+
let c = Connection::get_private(BusType::System).unwrap();
100+
c.add_match(
101+
"type='signal',\
102+
path='/org/freedesktop/NetworkManager',\
103+
interface='org.freedesktop.DBus.Properties',\
104+
member='PropertiesChanged'",
105+
)
106+
.unwrap();
107+
c.add_match(
108+
"type='signal',\
109+
path_namespace='/org/freedesktop/NetworkManager/ActiveConnection',\
110+
interface='org.freedesktop.DBus.Properties',\
111+
member='PropertiesChanged'",
112+
)
113+
.unwrap();
114+
c.add_match(
115+
"type='signal',\
116+
path_namespace='/org/freedesktop/NetworkManager/IP4Config',\
117+
interface='org.freedesktop.DBus',\
118+
member='PropertiesChanged'",
119+
)
120+
.unwrap();
121+
122+
loop {
123+
let timeout = 300_000;
124+
125+
for event in c.iter(timeout) {
126+
match event {
127+
ConnectionItem::Nothing => (),
128+
_ => {
129+
send.send(Task {
130+
id,
131+
update_time: Instant::now(),
132+
})
133+
.unwrap();
134+
}
135+
}
136+
}
137+
}
138+
})
139+
.unwrap();
140+
}
141+
Ok(ExternalIP {
142+
id,
143+
output: TextWidget::new(id, 0, shared_config),
144+
format: block_config.format.with_default("{ip} {country_flag}")?,
145+
refresh_interval_success: block_config.refresh_interval_success,
146+
refresh_interval_failure: block_config.refresh_interval_failure,
147+
})
148+
}
149+
}
150+
151+
impl Block for ExternalIP {
152+
fn id(&self) -> usize {
153+
self.id
154+
}
155+
156+
fn update(&mut self) -> Result<Option<Update>> {
157+
let (external_ip, success) = {
158+
let ip_info: Result<IPAddressInfo> =
159+
match http::http_get_json(API_ENDPOINT, Some(Duration::from_secs(3)), vec![]) {
160+
Ok(ip_info_json) => serde_json::from_value(ip_info_json.content)
161+
.block_error(BLOCK_NAME, "Failed to decode JSON"),
162+
_ => Err(BlockError(
163+
BLOCK_NAME.to_string(),
164+
"Failed to contact API".to_string(),
165+
)),
166+
};
167+
match ip_info {
168+
Ok(ip_info) => match ip_info.error {
169+
false => {
170+
self.output.set_state(State::Idle);
171+
let flag = country_flag_from_iso_code(ip_info.country_code.as_str());
172+
let values = map!(
173+
"ip" => Value::from_string (ip_info.ip),
174+
"version" => Value::from_string (ip_info.version),
175+
"city" => Value::from_string (ip_info.city),
176+
"region" => Value::from_string (ip_info.region),
177+
"region_code" => Value::from_string (ip_info.region_code),
178+
"country" => Value::from_string (ip_info.country),
179+
"country_name" => Value::from_string (ip_info.country_name),
180+
"country_code" => Value::from_string (ip_info.country_code),
181+
"country_code_iso3" => Value::from_string (ip_info.country_code_iso3),
182+
"country_capital" => Value::from_string (ip_info.country_capital),
183+
"country_tld" => Value::from_string (ip_info.country_tld),
184+
"continent_code" => Value::from_string (ip_info.continent_code),
185+
"in_eu" => Value::from_boolean (ip_info.in_eu),
186+
"postal" => Value::from_string (ip_info.postal.unwrap_or_else(|| "No postal code".to_string())),
187+
"latitude" => Value::from_float (ip_info.latitude),
188+
"longitude" => Value::from_float (ip_info.longitude),
189+
"timezone" => Value::from_string (ip_info.timezone),
190+
"utc_offset" => Value::from_string (ip_info.utc_offset),
191+
"country_calling_code" => Value::from_string (ip_info.country_calling_code),
192+
"currency" => Value::from_string (ip_info.currency),
193+
"currency_name" => Value::from_string (ip_info.currency_name),
194+
"languages" => Value::from_string (ip_info.languages),
195+
"country_area" => Value::from_float (ip_info.country_area),
196+
"country_population" => Value::from_float (ip_info.country_population),
197+
"asn" => Value::from_string (ip_info.asn),
198+
"org" => Value::from_string (ip_info.org),
199+
"country_flag" => Value::from_string(flag),
200+
);
201+
let s = self.format.render(&values)?;
202+
(s.0, true)
203+
}
204+
true => {
205+
self.output.set_state(State::Critical);
206+
(format!("Error: {}", ip_info.reason), false)
207+
}
208+
},
209+
Err(err) => {
210+
self.output.set_state(State::Critical);
211+
(err.to_string(), false)
212+
}
213+
}
214+
};
215+
216+
self.output.set_text(external_ip);
217+
match success {
218+
/* The external IP address can change without triggering a
219+
* notification (for example a refresh between the router and
220+
* the ISP) so check from time to time even on success */
221+
true => Ok(Some(
222+
Duration::from_secs(self.refresh_interval_success).into(),
223+
)),
224+
false => Ok(Some(
225+
Duration::from_secs(self.refresh_interval_failure).into(),
226+
)),
227+
}
228+
}
229+
230+
fn view(&self) -> Vec<&dyn I3BarWidget> {
231+
vec![&self.output]
232+
}
233+
}

src/formatting.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ impl FormatTemplate {
6161
Self::format_contains(&self.full, var) || Self::format_contains(&self.short, var)
6262
}
6363

64+
pub fn has_tokens(&self) -> bool {
65+
!self.full.as_ref().map(Vec::is_empty).unwrap_or(true)
66+
|| !self.short.as_ref().map(Vec::is_empty).unwrap_or(true)
67+
}
68+
6469
fn format_contains(format: &Option<Vec<Token>>, var: &str) -> bool {
6570
if let Some(tokens) = format {
6671
for token in tokens {

src/formatting/value.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ enum InternalValue {
1717
Text(String),
1818
Integer(i64),
1919
Float(f64),
20+
Boolean(bool),
2021
}
2122

2223
fn format_number(
@@ -150,6 +151,14 @@ impl Value {
150151
value: InternalValue::Float(value),
151152
}
152153
}
154+
pub fn from_boolean(value: bool) -> Self {
155+
Self {
156+
icon: None,
157+
min_width: 2,
158+
unit: Unit::None,
159+
value: InternalValue::Boolean(value),
160+
}
161+
}
153162

154163
// Set options
155164
pub fn icon(mut self, icon: String) -> Self {
@@ -248,6 +257,10 @@ impl Value {
248257
// Apply engineering notation (Float-only)
249258
format_number(value, min_width, var.min_prefix, unit, pad_with)
250259
}
260+
InternalValue::Boolean(value) => match value {
261+
true => String::from("T"),
262+
false => String::from("F"),
263+
},
251264
};
252265

253266
// We prepend the resulting string with the icon if it is set

0 commit comments

Comments
 (0)