Emoji+Unicode utility

main
Oliver Kennedy 2024-01-04 13:26:35 -05:00
parent 5dc4035686
commit 2a80df16d9
Signed by: okennedy
GPG Key ID: 3E5F9B3ABD3FDB60
11 changed files with 287 additions and 11 deletions

3
.gitignore vendored
View File

@ -1 +1,4 @@
/target
emoji-test.txt
emoji.txt
UnicodeData.txt

View File

@ -26,3 +26,7 @@ path = "src/pass.rs"
[[bin]]
name = "todo"
path = "src/todo.rs"
[[bin]]
name = "emoji"
path = "src/emoji.rs"

0
data/.hold-for-git Normal file
View File

11
emoji.ron Normal file
View File

@ -0,0 +1,11 @@
(
name: "Emoji",
description: "Syntax: e [keywords]",
bin: (path: "emoji"),
icon: Name("accessories-character-map"),
query: (
regex: "^(e|emoji)\\s.*",
isolate: true,
help: "emoji",
)
)

View File

@ -1,11 +1,12 @@
PLUGINS="todo pass"
PLUGINS="todo pass emoji"
cargo build
for plug in $PLUGINS; do
DIR=~/.local/share/pop-launcher/plugins/$plug
BIN=target/debug/$plug
DATA=data
RON=./$plug.ron
if [ ! -d $DIR ] ; then
@ -16,4 +17,8 @@ for plug in $PLUGINS; do
cp $RON $DIR/plugin.ron
echo Installing $BIN "->" $DIR
cp $BIN $DIR
done
if [ "$plug" = "emoji" ] ; then
echo Installing $DATA/emoji.txt "->" $DIR/emoji.txt
cp $DATA/emoji.txt $DIR/
fi
done

13
scripts/gen_emoji.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash
cd `dirname $0`
cd ../data
if [ ! -f UnicodeData.txt ] ; then
wget https://www.unicode.org/Public/15.1.0/ucd/UnicodeData.txt
fi
if [ ! -f emoji-test.txt ] ; then
wget https://www.unicode.org/Public/emoji/15.0/emoji-test.txt
fi
cd ../
./scripts/gen_unicode.py data/UnicodeData.txt data/emoji-test.txt > data/emoji.txt
cp data/emoji.txt target/debug/emoji.txt

80
scripts/gen_unicode.py Executable file
View File

@ -0,0 +1,80 @@
#!/usr/bin/env python3
# Liberally borrowed from Gnome Characters
# https://gitlab.gnome.org/GNOME/gnome-characters/-/blob/main/lib/gen-names.py
# Input: https://www.unicode.org/Public/UNIDATA/UnicodeData.txt
import io
import re
import json
import sys
class Builder(object):
def __init__(self):
pass
def read(self, ucdfile, emojifile):
emoji = set()
for line in emojifile:
m = re.match('([0-9A-F ]+); fully-qualified\s+#.*E\d+.\d+ (.+)', line)
if not m:
continue
cp = m.group(1).strip()
if cp.find(' ') > 0:
continue
emoji.add(int(cp, 16))
names = []
for line in ucdfile:
if line.startswith('#'):
continue
line = line.strip()
if len(line) == 0:
continue
(codepoint, name, _other) = line.split(';', 2)
# Names starting with < are signifying controls and special blocks,
# they aren't useful for us
if name[0] == '<':
continue
cp = int(codepoint, 16)
names.append(
(codepoint, cp in emoji, name)
)
return names
def write(self, data):
# data = data[:10]
# json.dump([
# {"cp": chr(int(codepoint, 16)), "name": name.lower()}
# for codepoint, name in data
# ], sys.stdout)
data.sort(key = lambda d: (not d[1], d[2]))
for codepoint, is_emoji, name in data:
print("{0};{1}".format(codepoint, name.lower()))
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description='build')
parser.add_argument('ucdfile', type=argparse.FileType('r'),
help='UnicodeData.txt')
parser.add_argument('emojifile', type=argparse.FileType('r'),
help='emoji-test.txt')
args = parser.parse_args()
builder = Builder()
# FIXME: argparse.FileType(encoding=...) is available since Python 3.4
data = builder.read(io.open(args.ucdfile.name, encoding='utf_8_sig'), io.open(args.emojifile.name, encoding='utf_8_sig'))
builder.write(data)

161
src/emoji.rs Normal file
View File

@ -0,0 +1,161 @@
mod launcher;
use serde::{Deserialize, Serialize};
use std::env;
use std::fmt::Display;
use std::process::Command;
use launcher::{ Request, PluginResponse, PluginSearchResult };
use std::result::Result;
use std::error::Error;
use std::fs::File;
use std::io::{self, BufRead, Write};
#[derive(Debug)]
struct EmojiError
{
message: String
}
impl Display for EmojiError
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Error: ")?;
f.write_str(&self.message)?;
Ok(())
}
}
impl Error for EmojiError
{
fn source(&self) -> Option<&(dyn Error + 'static)> {
None
}
fn description(&self) -> &str {
"description() is deprecated; use Display"
}
fn cause(&self) -> Option<&dyn Error> {
self.source()
}
}
#[derive(Deserialize, Serialize)]
struct Emoji
{
cp: String,
name: String
}
fn read_emoji() -> Result<Vec<Emoji>, io::Error>
{
let mut path = env::current_exe()?;
path.pop();
path.push("emoji.txt");
let file = File::open(path)?;
let reader = io::BufReader::new(file);
let result =
reader.lines().flatten()
.map(|line:String| {
let mut fields = line.split(";");
Emoji {
cp: format!("{}", char::from_u32(u32::from_str_radix(fields.next().unwrap(), 16).unwrap()).unwrap()),
name: fields.next().unwrap().to_string(),
}
})
.collect();
Ok(result)
}
fn respond(response: &PluginResponse) -> io::Result<()>
{
let encoded = serde_json::to_string(&response)?;
print!("{}\n", encoded);
Ok(())
}
fn copy_emoji(emoji: &Emoji) -> Result<(), Box<dyn Error>>
{
let mut cmd =
Command::new("xclip")
.arg("-i")
.arg("-selection").arg("clip")
.stdin(std::process::Stdio::piped())
.spawn()?;
cmd.stdin.as_ref().unwrap().write_all(&emoji.cp.as_bytes())?;
cmd.wait()?;
Ok(())
}
fn main() -> Result<(), Box<dyn Error>> {
let emoji = read_emoji()?;
// println!("Read: {}", emoji.len());
// for e in emoji.iter().take(10)
// {
// println!("{}: {}", e.name, e.cp);
// }
let mut keep_going = true;
let mut buffer = String::new();
let stdin = io::stdin();
let mut last_matches: Vec<&Emoji> = Vec::new();
while keep_going {
if stdin.read_line(&mut buffer)? > 0
{
let request: Request = serde_json::from_str(&buffer)?;
match request
{
Request::Activate(id) =>
{
copy_emoji(&last_matches[id as usize])?;
respond(&PluginResponse::Close)?;
}
Request::Search(term) =>
{
let term =
term.strip_prefix("e ")
.unwrap_or_else(||{
term.strip_prefix("emoji ").unwrap_or(term.as_str())
});
last_matches =
emoji.iter()
.filter(|e| e.name.contains(&term))
.take(40)
.collect();
for (id, e) in last_matches.iter().enumerate()
{
respond(&PluginResponse::Append(
PluginSearchResult {
id: id as u32,
name: e.cp.clone(),
description: e.name.clone(),
keywords: None,
icon: None,
exec: None,
window: None
}
))?;
}
respond(&PluginResponse::Finished)?;
}
_ => ()
}
buffer.clear();
} else {
keep_going = false;
}
}
Ok(())
}

View File

@ -8,6 +8,7 @@ use std::io;
use pass_menu::Menu;
use launcher::{ Request, PluginResponse };
use std::result::Result;
use std::error::Error;
fn respond(response: &PluginResponse) -> io::Result<()>
{
@ -16,7 +17,7 @@ fn respond(response: &PluginResponse) -> io::Result<()>
Ok(())
}
fn process_request(request_str: &String, menu: &Menu) -> Result<bool, String>
fn process_request(request_str: &String, menu: &Menu) -> Result<bool, Box<dyn Error>>
{
let request: Request = serde_json::from_str(&request_str).map_err(|err| err.to_string())?;

View File

@ -96,18 +96,18 @@ impl Menu
ret
}
pub fn activate(&self, id: u32) -> Result<(),String>
pub fn activate(&self, id: u32) -> Result<(),Box<dyn std::error::Error>>
{
let idx = usize::try_from(id)
.map_err(|_err| "Invalid index")?;
if idx >= self.entries.len() { return Err(String::from("Invalid index")) }
if idx >= self.entries.len() { return Ok(()); }
let entry = &self.entries[idx];
Command::new("pass")
.arg("-c")
.arg(entry.full_name.as_str())
.spawn()
.map_err(|err| err.to_string())?;
.spawn()?
.wait()?;
Ok(())
}
@ -143,16 +143,15 @@ impl Menu
}
pub fn activate_context(&self, id: u32, action: u32) -> Result<(), String>
pub fn activate_context(&self, id: u32, action: u32) -> Result<(), Box<dyn std::error::Error>>
{
match num::FromPrimitive::from_u32(action)
{
Some(ContextAction::Copy) => self.activate(id)?,
Some(ContextAction::Edit) => self.edit(id)?,
None => return Err(String::from("Invalid context action"))
None => ()
}
Ok(())
}
}

View File

@ -188,7 +188,6 @@ fn main() -> Result<()> {
// home_dir has a deprecation warning because it is broken on
// windows... but PopOS is linux only.
#[allow(deprecated)]
let mut config = env::home_dir().unwrap();
config.push(".config");
config.push("pop_todo.json");