Select Git revision
merge_modules.py
-
Jiří Kalvoda authoredJiří Kalvoda authored
Module.cs 16.95 KiB
// 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)};
}
}
}