// i3csstatus - Alternative generator of i3 status bar written in c# // (c) 2022 Jiri Kalvoda <jirikalvoda@kam.mff.cuni.cz> // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // any later version. // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with this program. If not, see <https://www.gnu.org/licenses/>. using System; using System.Collections.Generic; using System.Text; using System.IO; using System.Text.Json; using System.Text.Json.Nodes; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.CommandLine; using System.Runtime.InteropServices; using Color = System.Drawing.Color; using Process = System.Diagnostics.Process; using System.Net.Http; using Config; namespace i3csstatus; class FormatExpander { static public string Expand(string f, Func<string, string> callback) { StringBuilder sb = new(); StringBuilder arg = null; bool freeClosing = false; foreach(char c in f) { if(arg == null && c == '{') arg = new(); else if(arg != null && c == '{') { sb.Append('{'); arg = null; } else if(arg == null && c == '}') { if(freeClosing) sb.Append('}'); freeClosing = !freeClosing; } else if(arg != null && c == '}') { sb.Append(callback(arg.ToString())); arg = null; } else if(arg != null) arg.Append(c); else sb.Append(c); } return sb.ToString(); } } class ModuleException: Exception { } [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true)] public class ModuleName : System.Attribute { string name; public ModuleName(string _name) { name = _name; } public string GetName() => name; } class GlobalModuleResource { public virtual void InitEnd(){} public virtual void GetBegin(){} public virtual void GetEnd(){} } interface Module { IEnumerable<Block> Get(); void Init(ModuleParent _bar, ConfigSection section); } [ModuleName("constant")] class ModuleConstant: Module { string text; public void Init(ModuleParent _bar, ConfigSection section) { text = section.OptionalDefaultIsSectionName("text").AsString(); } public IEnumerable<Block> Get() { return new Block[]{new Block(text)}; } } [ModuleName("file")] class ModuleFile: Module { string path; InnerStatusBar ifNotFound; InnerStatusBar ifReadError; Parser parser; public void Init(ModuleParent _bar, ConfigSection section) { path = section.Mandatory("path").AsPath(); ifNotFound = new InnerStatusBar(_bar, section.Optional("not_found_handler")?.AsConfig() ?? section.Optional("not_found_handler")?.AsConfig() ?? new ConfigParser( @" [constant] _color = red text = NFound ")); ifReadError = new InnerStatusBar(_bar, section.Optional("error_handler")?.AsConfig() ?? new ConfigParser( @" [constant] _color = red text = ERR ")); parser = _bar.GetGlobal<ParserGetter>().ByNameFromConfig(section.OptionalDefaultIsSectionName("parser")); parser.Init(_bar, this, section); } public IEnumerable<Block> Get() { string text; try { text = File.ReadAllText(path); } catch (Exception e) when (e is FileNotFoundException || e is DirectoryNotFoundException) { return ifNotFound.Get(); } catch (Exception e) { Console.Error.WriteLine(e); return ifReadError.Get(); } return parser.Parse(text); } } abstract class ModuleSourceThreadAndParser: Module { protected ModuleParent bar; protected string data; protected long dataTick; Thread inputThread; protected InnerStatusBar ifNoData; protected int scheudleIn_ms; protected Parser parser; int? maxOld_ms; int? showOld_ms; protected abstract void inputThreadFunc(); public virtual void Init(ModuleParent _bar, ConfigSection section) { bar = _bar; ifNoData = new InnerStatusBar(_bar, section.Optional("no_data_handler")?.AsConfig() ?? new ConfigParser( @" ")); scheudleIn_ms = section.Optional("delay")?.AsMs() ?? 10; showOld_ms = section.Optional("show_old")?.AsMs(); maxOld_ms = section.Optional("max_old")?.AsMs(); parser = _bar.GetGlobal<ParserGetter>().ByNameFromConfig(section.OptionalDefaultIsSectionName("parser")); parser.Init(_bar, this, section); inputThread = new Thread(this.inputThreadFunc); inputThread.IsBackground = true; inputThread.Start(); } protected void setData(string _data, long t) { lock(this) { data = _data; dataTick = t; } bar.Schedule(scheudleIn_ms); } public abstract IEnumerable<Block> Get(); protected IEnumerable<Block> parse(string _text, long _dataTick) { long t = Environment.TickCount64; if(_text == null ) return ifNoData.Get(); if(maxOld_ms != null && _dataTick + maxOld_ms <= t) return ifNoData.Get(); var v = parser.Parse(_text); if(showOld_ms != null && _dataTick + showOld_ms <= t) { bool first = true; v = v.Select(x => { if(first) x = x with { Text = $"[{TimeShow.Show((t-_dataTick)/1000)}] {x.Text}" }; first = false; return x; }).ToArray(); } return v; } } abstract class ModuleAbstractPipe: ModuleSourceThreadAndParser { int msgSeparator; public override void Init(ModuleParent _bar, ConfigSection section) { msgSeparator = section.Optional("separator")?.AsInt() ?? 0; base.Init(_bar, section); } protected abstract StreamReader getPipe(); protected override void inputThreadFunc() { var sr = getPipe(); StringBuilder s = new(); while(true) { int c = sr.Read(); if(c == -1) throw new IOException(); if(c == msgSeparator) { setData(s.ToString(), Environment.TickCount64); s = new StringBuilder(); } else { s.Append((char)c); } } } override public IEnumerable<Block> Get() { string _text; long _dataTick; lock(this) { _text = data; _dataTick = dataTick; } return parse(_text, _dataTick); } } [ModuleName("pipe")] class ModulePipe: ModuleAbstractPipe { string path; public override void Init(ModuleParent _bar, ConfigSection section) { path = section.Mandatory("path").AsPath(); base.Init(_bar, section); } StreamWriter sw; // Only for keeping pipe alive protected override StreamReader getPipe() { while(true) { int r = POSIX.mkfifo(path, (7<<6)+(7<<3)+7); int err = POSIX.ERRNO; if(r == 0) break; if(r != 0 && err == POSIX.EEXIST) { File.Delete(path); continue; } throw new Exception($"POSIX.mkfifo fail with ERRNO {err}"); } var sr = new StreamReader(path); sw = new StreamWriter(path); // Only for keeping pipe alive return sr; } } [ModuleName("exec")] class ModuleExec: ModuleAbstractPipe { string programName; string arguments; Process process; string stdinString; public override void Init(ModuleParent _bar, ConfigSection section) { programName = section.Mandatory("program").AsPath(); stdinString = section.Optional("stdin")?.AsString() ?? ""; arguments = section.Optional("arguments")?.AsString() ?? ""; base.Init(_bar, section); } protected override StreamReader getPipe() { process = new(); process.StartInfo.FileName = programName; process.StartInfo.Arguments = arguments; process.StartInfo.RedirectStandardInput = true; process.StartInfo.RedirectStandardOutput = true; process.Start(); TextWriter stdin = process.StandardInput; var stdout = process.StandardOutput; stdin.Write(stdinString); stdin.Close(); return stdout; } } abstract class ModuleHttpAbstract: ModuleSourceThreadAndParser { HttpRequestException error; InnerStatusBar ifReadError; int period_ms; int timeout_ms; public override void Init(ModuleParent _bar, ConfigSection section) { ifReadError = new InnerStatusBar(_bar, section.Optional("error_handler")?.AsConfig() ?? new ConfigParser( @" [constant] _color = red text = ERR ")); period_ms = section.Optional("period")?.AsMs() ?? 10000; timeout_ms = section.Optional("timeout")?.AsMs() ?? 10000; base.Init(_bar, section); } abstract protected Task<string> inputThreadFunc_Get(HttpClient client); async override protected void inputThreadFunc() { HttpClient client = null; void reloadClient() { if(client != null) client.Dispose(); client = new HttpClient(); client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.134 Safari/537.36"); client.Timeout = TimeSpan.FromMilliseconds(10000); } reloadClient(); while(true) { try { long t = Environment.TickCount64; string _data = await inputThreadFunc_Get(client); lock(this) { dataTick = t; data = _data; error = null; } } catch(HttpRequestException e) { if(e.StatusCode != null) lock(this) { error = e; } } catch(System.Threading.Tasks.TaskCanceledException) { } catch(System.OperationCanceledException) { } Thread.Sleep(period_ms); } } public override IEnumerable<Block> Get() { string _data; long _dataTick; HttpRequestException _error; lock(this) { _data = data; _dataTick = dataTick; _error = error; } long t = Environment.TickCount64; if(_error != null) return ifReadError.Get(); return parse(_data, _dataTick); } } [ModuleName("http")] class ModuleHttp: ModuleHttpAbstract { string url; public override void Init(ModuleParent _bar, ConfigSection section) { url = section.Mandatory("url").AsString(); base.Init(_bar, section); } async override protected Task<string> inputThreadFunc_Get(HttpClient client) { return await client.GetStringAsync(url); } } [ModuleName("http_multi")] class ModuleHttpMulti: ModuleHttpAbstract { string[] urlList; string separator; public override void Init(ModuleParent _bar, ConfigSection section) { urlList = section.Mandatory("urls").Lines().Select(x => x.AsString()).ToArray(); separator = section.Optional("separator")?.AsString() ?? "\n"; base.Init(_bar, section); } async override protected Task<string> inputThreadFunc_Get(HttpClient client) { List<string> output = new(); foreach(string url in urlList) { string r = await client.GetStringAsync(url); output.Add(r); } return string.Join(separator, output); } } [ModuleName("i3status")] class ModuleI3Status: Module { string name; string configuration; Block[] elements; class Global: GlobalModuleResource { public List<ModuleI3Status> modules = new(); public Dictionary<string, ModuleI3Status> moduleByName = new(); System.Diagnostics.Process process; public int? cache_ms; long lastGet_ms = 0; Parser parser = new ParserI3(); public override void InitEnd() { process = new(); //process.StartInfo.FileName = "tail"; //process.StartInfo.Arguments = "-n 5"; process.StartInfo.FileName = "i3status"; process.StartInfo.Arguments = "-c /dev/stdin"; process.StartInfo.RedirectStandardInput = true; process.StartInfo.RedirectStandardOutput = true; process.Start(); TextWriter stdin = process.StandardInput; //stdin = Console.Out; var stdout = process.StandardOutput; stdin.WriteLine("general {"); stdin.WriteLine("output_format = \"i3bar\""); stdin.WriteLine("colors = true"); stdin.WriteLine("interval = 1000000000"); stdin.WriteLine("}"); foreach(var m in modules) { stdin.WriteLine($"order += \"{m.name}\""); stdin.WriteLine($"{m.name} {{"); if(m.configuration != null) stdin.WriteLine(m.configuration); stdin.WriteLine($"}}"); } stdin.Close(); stdout.ReadLine(); stdout.ReadLine(); stdout.ReadLine(); } public override void GetBegin() { long t = Environment.TickCount64; if(lastGet_ms + cache_ms > t) return; lastGet_ms = t; process.KillBySignal(POSIX.SIGUSR1); string line = process.StandardOutput.ReadLine(); if(line == null) { throw new PrintAndExit("I3Status don't work correctly."); } Block[] data = (Block[])parser.Parse(line[1..]); if(data.Length != modules.Count) throw new Exception("Parse i3status error"); for(int i=0;i<modules.Count;i++) { modules[i].elements = new []{data[i]}; } } } public void Init(ModuleParent _bar, ConfigSection section) { name = section.OptionalDefaultIsSectionName("name").AsString(); configuration = section.Optional("config")?.AsString(); var g = _bar.GetGlobal<Global>(); g.modules.Add(this); if(g.moduleByName.ContainsKey(name)) throw new ConfigMistake(section["name"], "Duplicit i3status name."); if(section.Optional("global_cache") != null) { int cache_ms = section["global_cache"].AsMs(); if(g.cache_ms != null && g.cache_ms != cache_ms) throw new ConfigMistake(section["global_cache"], "Collision definition of global value."); g.cache_ms = cache_ms; } g.moduleByName[name] = this; } public IEnumerable<Block> Get() => elements ?? new Block[]{}; } [ModuleName("time")] class ModuleTime: Module { ModuleParent bar; string format; string shortFormat; bool refresh; int round_ms; public void Init(ModuleParent _bar, ConfigSection section) { bar = _bar; format = section.Optional("format")?.AsString() ?? "yyyy-MM-dd HH:mm:ss"; shortFormat = section.Optional("short_format")?.AsString(); DateTime.Now.ToString(format); refresh = section.Optional("refresh")?.AsBool() ?? false; round_ms = section.Optional("round")?.AsMs() ?? 1000; } public IEnumerable<Block> Get() { var t = DateTime.Now; var t_ms = (long)t.TimeOfDay.TotalMilliseconds; t = t.AddMilliseconds(-(t_ms % round_ms)); string s = t.ToString(format); string shortText = shortFormat==null?null:t.ToString(shortFormat); if(refresh) bar.Schedule((int)(round_ms - t_ms % round_ms + 1)); return new Block[]{new Block(s, ShortText: shortText)}; } } [ModuleName("battery")] class ModuleBattery: Module { string path; string format; string shortFormat; long timeWindow_ms; string lastStatus; record HistoryElement( long time_ms, int energy ); Queue<HistoryElement> history = new(); public void Init(ModuleParent _bar, ConfigSection section) { string instance = section.Optional("instance")?.AsString() ?? "BAT1"; path = section.Optional("path")?.AsPath() ?? $"/sys/class/power_supply/{instance}/uevent"; format = section.Optional("format")?.AsString() ?? "{status} {derivation} {percent} {remaining}"; timeWindow_ms = section.Optional("time_window")?.AsMs() ?? 60000; shortFormat = section.Optional("short_format")?.AsString(); } public IEnumerable<Block> Get() { long t = Environment.TickCount64; while(history.Count > 0 && history.Peek().time_ms + timeWindow_ms < t) history.Dequeue(); try { var c = new ConfigParser(File.ReadAllText(path)).MainSection; int energyNow = c["POWER_SUPPLY_ENERGY_NOW"].AsInt(); int energyDesign = c["POWER_SUPPLY_ENERGY_FULL_DESIGN"].AsInt(); int energyFull = c["POWER_SUPPLY_ENERGY_FULL"].AsInt(); string status = c["POWER_SUPPLY_STATUS"].AsString(); double percent = energyNow / (double)energyDesign * 100; long? timeDelta_ms = null; int? energyDelta = null; double? percentPerHour = null; long? remainingTime_s = null; if(lastStatus != status) history.Clear(); if(history.Count>0 && history.Peek().time_ms != t) { HistoryElement h = history.Peek(); timeDelta_ms = t - h.time_ms; energyDelta = energyNow - h.energy; var derivationEnergy = energyDelta/(double)timeDelta_ms; percentPerHour = derivationEnergy * 1000 * 60 * 60 / energyDesign * 100; if(derivationEnergy > 0) remainingTime_s = (long)((energyFull-energyNow)/derivationEnergy/1000); if(derivationEnergy < 0) remainingTime_s = (long)((energyNow)/-derivationEnergy/1000); } history.Enqueue(new HistoryElement(t, energyNow)); lastStatus = status; string genFormat(string f) { if(f == null) return null; return FormatExpander.Expand(f, x => { if(x == "status") return (status.Length > 3 ? status[0..3] : status); if(x == "percent") return percent.ToString("N2"); if(x == "derivation") return percentPerHour?.ToString("N2") ?? ""; if(x == "remaining") return remainingTime_s == null ?"": TimeShow.Show(remainingTime_s.Value); return "UNDEF"; }).Trim(); } string text = genFormat(format); string shortText = genFormat(shortFormat); Color? color=null; if(status == "Charging") { if(percentPerHour <= 0) color = Color.FromArgb(255,0,0); else color = Color.FromArgb(0,255,0); } else if(status == "Full") color = Color.FromArgb(0,255,0); else { if(percent < 10) color = Color.FromArgb(255,0,0); else if(percent < 30) color = Color.Orange; } return new Block[]{new Block(text, ShortText: shortText, Color: color)}; } catch (IOException) { return new Block[]{new Block("NO BATTERY", Color:Color.Red)}; } catch (ConfigException e) { Console.Error.WriteLine(e); return new Block[]{new Block("Battery parse error", Color:Color.Red)}; } } }