Select Git revision
Program.cs
-
Jiří Kalvoda authoredJiří Kalvoda authored
Program.cs 13.89 KiB
#nullable enable
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using Pastel;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Linq;
using System.Threading;
using System.CommandLine;
using System.Runtime.InteropServices;
using Color = System.Drawing.Color;
using Process = System.Diagnostics.Process;
using Config;
namespace i3csstatus {
static class ProcesExtended
{
[DllImport("libc", SetLastError = true)]
public static extern int kill(int pid, int sig);
public static int KillBySignal(this Process p, int sig)
{
return kill(p.Id, sig);
}
}
static class JsonExtended
{
public static JsonObject RemoveNull(this JsonObject o)
{
List<string> toDelete = new();
foreach(var (k, v) in o)
{
if(v == null)
toDelete.Add(k);
}
foreach(var k in toDelete)
o.Remove(k);
return o;
}
}
static class ColorToHexExtended
{
static public string ToHex(this Color c)
{
return $"#{c.R:X2}{c.G:X2}{c.B:X2}";
}
}
record Element(
string text,
string? short_text=null,
Color? color=null
);
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(){}
}
#nullable disable
interface Module
{
IEnumerable<Element> Get();
void Init(StatusBar _bar, ConfigParser config, ConfigSection section);
}
[ModuleName("constant")]
class ModuleConstant: Module
{
string text;
Color color;
public void Init(StatusBar _bar, ConfigParser config, ConfigSection section)
{
text = section.Mandatory("text");
color = section?["color"]?.AsColor() ?? System.Drawing.ColorTranslator.FromHtml("white");
}
public IEnumerable<Element> Get()
{
return new Element[]{new Element(text, color: color)};
}
}
[ModuleName("file")]
class ModuleFile: Module
{
string path;
protected Color color;
InnerStatusBar ifNotFound;
InnerStatusBar ifReadError;
public void Init(StatusBar _bar, ConfigParser config, ConfigSection section)
{
path = section.Mandatory("path").AsPath();
color = section.Optional("color")?.AsColor() ?? System.Drawing.ColorTranslator.FromHtml("white");
ifNotFound = new InnerStatusBar(_bar, section.Optional("not_found")?.AsConfig() ??
section.Optional("read_error")?.AsConfig() ??
new ConfigParser(
@"
[constant]
color = red
text = NFound
"));
ifReadError = new InnerStatusBar(_bar, section.Optional("read_error")?.AsConfig() ??
new ConfigParser(
@"
[constant]
color = red
text = ERR
"));
}
protected virtual IEnumerable<Element> Parse(string text)
{
if(text.Length > 0 && text[^1] == '\n') text = text[..^1];
return text.Split("\n").Select(x => new Element(x, color: color)).ToArray();
}
public IEnumerable<Element> Get()
{
string text;
try
{
text = File.ReadAllText(path);
}
catch (Exception ex) when (ex is FileNotFoundException || ex is DirectoryNotFoundException)
{
return ifNotFound.Get();
}
catch (Exception)
{
return ifReadError.Get();
}
return Parse(text);
}
}
[ModuleName("osdd_last")]
class ModuleOsddLast: ModuleFile
{
public new void Init(StatusBar _bar, ConfigParser config, ConfigSection section)
{
base.Init(_bar, config, section);
}
static string timeShow(long t_s)
{
if(t_s<180) return $"{t_s}s";
if(t_s<180*60) return $"{t_s/60}min";
return $"{t_s/60/60}h";
}
protected override IEnumerable<Element> Parse(string text)
{
try
{
var lines = text.Split("\n");
List<Element> output = new();
long timeEleapsed_s = DateTimeOffset.Now.ToUnixTimeSeconds()-long.Parse(lines[0]);
bool first = true;
foreach(var l in lines[1..])
{
if(l.Length > 7)
output.Add(new Element((first?$"[{timeShow(timeEleapsed_s)}] ":"") + l[7..], color: System.Drawing.ColorTranslator.FromHtml("#"+l[..6])));
first = false;
}
return output;
}
catch(Exception)
{
return new Element[]{new Element("OSDD parse ERROR", color: Color.Red)};
}
}
}
[ModuleName("i3status")]
class ModuleI3Status: Module
{
class ModuleI3StatusException: ModuleException
{
public override string ToString()
{
return $"I3Status don't work correctly.";
}
}
string name;
string configuration;
Element[] 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;
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();
}
static Element parseElement(JsonObject json)
{
return new Element(
text: json["full_text"].AsValue().GetValue<string>(),
short_text: json["short_text"]?.AsValue()?.GetValue<string>(),
color: System.Drawing.ColorTranslator.FromHtml(json["color"]?.AsValue()?.GetValue<string>() ?? "white")
);
}
public override void GetBegin()
{
long t = Environment.TickCount64;
if(lastGet_ms + cache_ms > t)
return;
lastGet_ms = t;
process.KillBySignal(10);
string line = process.StandardOutput.ReadLine();
if(line == null)
{
throw new ModuleI3StatusException();
}
JsonArray json = JsonObject.Parse(line[1..]).AsArray();
if(json.Count != modules.Count)
throw new Exception("Parse i3status error");
for(int i=0;i<modules.Count;i++)
{
modules[i].elements = new []{parseElement(json[i].AsObject())};
}
}
}
public void Init(StatusBar _bar, ConfigParser config, ConfigSection section)
{
name = section["name"];
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<Element> Get() => elements ?? new Element[]{};
}
#nullable restore
abstract class StatusBar
{
List<Module> modules = new();
protected void parseConfigFile(FileInfo configFile)
{
parseConfigString(File.ReadAllText(configFile.ToString()));
}
protected void parseConfigString(string configString)
{
parseConfig(new ConfigParser(configString));
}
virtual protected void parseConfig(ConfigParser p)
{
var moduleTypes = new Dictionary<string, Type>(
from assembly in AppDomain.CurrentDomain.GetAssemblies()
from type in assembly.GetTypes()
where type.IsDefined(typeof(ModuleName), false)
where typeof(Module).IsAssignableFrom(type)
from name in type.GetCustomAttributes(typeof(ModuleName), false)
select new KeyValuePair<string, Type>(((ModuleName)name).GetName(), type)
);
var constructorSignature = new Type[]{};
foreach(var s in p)
{
string type = s["_type"] ?? s.SectionName;
var constructor = moduleTypes[type].GetConstructor(constructorSignature);
if(constructor == null)
throw new Exception($"Missing constructor of {type} module");
Module module = (Module) constructor.Invoke(new object[]{});
module.Init(this, p, s);
modules.Add(module);
}
}
public List<Element> Get()
{
var elements = new List<Element>();
foreach(var m in modules)
elements.AddRange(m.Get());
return elements;
}
public abstract void Schedule(int in_ms);
public abstract T GetGlobal<T>() where T: GlobalModuleResource, new();
}
class InnerStatusBar: StatusBar
{
StatusBar parrent;
public InnerStatusBar(StatusBar _parrent, ConfigParser p)
{
parrent = _parrent;
parseConfig(p);
}
public override void Schedule(int in_ms) => parrent.Schedule(in_ms);
public override T GetGlobal<T>() => parrent.GetGlobal<T>();
}
abstract class RootStatusBar: StatusBar
{
long nextRun = 0;
long refresh_ms;
object nextRunMonitor = new();
public RootStatusBar(FileInfo configFile)
{
parseConfigFile(configFile);
}
override protected void parseConfig(ConfigParser p)
{
refresh_ms = p.MainSection["refresh"]?.AsMs() ?? 1000;
base.parseConfig(p);
foreach(var g in globalModuleResources)
g.Value.InitEnd();
}
void wait()
{
lock(nextRunMonitor)
{
while(true)
{
long actual = Environment.TickCount64;
if(nextRun <= actual)
break;
long wait_ms = Math.Min(10000, nextRun - actual);
Monitor.Wait(nextRunMonitor, (int)wait_ms, true);
}
nextRun = Environment.TickCount64 + refresh_ms;
}
}
public void Run(TextWriter w)
{
initOutput(w);
while(true)
{
wait();
Print(w);
}
}
abstract protected void format(TextWriter w, List<Element> elements);
virtual protected void initOutput(TextWriter w){}
public void Print(TextWriter w)
{
foreach(var g in globalModuleResources)
g.Value.GetBegin();
var elements = Get();
foreach(var g in globalModuleResources)
g.Value.GetEnd();
format(w, elements);
w.Flush();
}
public override void Schedule(int in_ms)
{
lock(nextRunMonitor)
{
long actual = Environment.TickCount64;
if(nextRun <= actual + in_ms)
return;
nextRun = actual + in_ms;
Monitor.Pulse(nextRunMonitor);
}
}
Dictionary<Type, GlobalModuleResource> globalModuleResources = new();
public override T GetGlobal<T>()
{
if(!globalModuleResources.ContainsKey(typeof(T)))
globalModuleResources[typeof(T)] = new T();
return (T)globalModuleResources[typeof(T)];
}
}
class StatusBarPlainText: RootStatusBar
{
public StatusBarPlainText(FileInfo configFile):base(configFile){}
override protected void format(TextWriter w, List<Element> elements)
{
bool first = true;
foreach(var e in elements)
{
if(!first)
w.Write("|");
first = false;
w.Write(e.text);
}
w.WriteLine("");
}
}
class StatusBarTerminal: RootStatusBar
{
Thread inputThread;
public StatusBarTerminal(FileInfo configFile, bool doInput):base(configFile)
{
if(doInput)
{
inputThread = new Thread(this.inputThreadFunc);
inputThread.IsBackground = true;
inputThread.Start();
}
}
void inputThreadFunc()
{
while(true)
{
System.ConsoleKeyInfo k = Console.ReadKey();
if(k.KeyChar == '\r')
Schedule(0);
else if(k.KeyChar == 'q')
Environment.Exit(0);
else
Console.WriteLine("");
}
}
override protected void format(TextWriter w, List<Element> elements)
{
bool first = true;
foreach(var e in elements)
{
if(!first)
w.Write("|".Pastel(Color.FromArgb(165, 229, 250)));
first = false;
w.Write(e.color!=null?e.text.Pastel(e.color.Value):e.text);
}
w.WriteLine("");
}
}
class StatusBarI3: RootStatusBar
{
public StatusBarI3(FileInfo configFile):base(configFile){}
override protected void initOutput(TextWriter w)
{
w.WriteLine("{\"version\":1}");
w.WriteLine("[{}");
}
override protected void format(TextWriter w, List<Element> elements)
{
var json = new JsonArray((from e in elements select new JsonObject(){
["full_text"] = e.text,
["short_text"] = e.short_text,
["color"] = e.color?.ToHex(),
}.RemoveNull()).ToArray());
var opt = new JsonSerializerOptions();
opt.DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull;
w.Write(",");
w.WriteLine(json.ToJsonString(opt));
}
}
class Program
{
#pragma warning disable 1998
static int Main(string[] args)
{
var configOption = new Option<FileInfo>
("--config", "Path to configuration file.");
configOption.AddAlias("-c");
var rootCommand = new RootCommand("An alternative implementation for i3 status bar generating.");
rootCommand.AddGlobalOption(configOption);
var c_plainText = new Command("plain-text", "Print status bar as plain text.");
rootCommand.Add(c_plainText);
c_plainText.SetHandler(async (config) =>
{
(new StatusBarPlainText(config)).Run(Console.Out);
}, configOption);
var c_terminal = new Command("terminal", "Print status bar with terminal escape secvence.");
rootCommand.Add(c_terminal);
var c_terminal_input = new Option<bool>
("--input", "Read key input.");
c_terminal_input.AddAlias("-i");
c_terminal.Add(c_terminal_input);
c_terminal.SetHandler(async (config, input) =>
{
(new StatusBarTerminal(config, input)).Run(Console.Out);
}, configOption, c_terminal_input);
var c_i3 = new Command("i3", "Comunicate with i3bar.");
rootCommand.Add(c_i3);
c_i3.SetHandler(async (config) =>
{
(new StatusBarI3(config)).Run(Console.Out);
}, configOption);
rootCommand.Invoke(args);
return 0;
}
#pragma warning restore 1998
}
}