Skip to content
Snippets Groups Projects
Select Git revision
  • a1e7c1e3f915de69c76765efb87c80c8a6543ca1
  • devel default
  • master
  • fo
  • jirka/typing
  • fo-base
  • mj/submit-images
  • jk/issue-96
  • jk/issue-196
  • honza/add-contestant
  • honza/mr7
  • honza/mrf
  • honza/mrd
  • honza/mra
  • honza/mr6
  • honza/submit-images
  • honza/kolo-vs-soutez
  • jh-stress-test-wip
  • shorten-schools
19 results

create-round

Blame
  • 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)};
    		}
    	}
    }