ThinkPad T series (also X1 Carbon) laptops have a great keyboard that I’ve been using delightfully for years. However, there’s a minor issue with its keyboard layout: they replaced the Menu key with a PrtSc. In my day to day work, I almost always accidentally hit that key while using my lovely Ctrl and Alt keys, upon which my laptop happily plays a shatter sound, flashes my screen white for a split second and spawns a screenshot file under my Pictures/ (thank you, GNOME). Whereas when I wanted to use my Menu key, it’s nowhere to be found.

However, there’s still an Insert key lying quietly in the top-right corner, which I never used (except for checking if some app even supports it). So why not make my old Insert PrtSc and my old PrtSc the new Menu?

Moreover, there are also 4 special keys (Fn + F9F12) that could have been my media keys, but are by default strange things like Settings and Search. Why not map them to media keys as well?


Things I tried

xmodmap was the go-to tool for remapping keys, and it works on the X11 server level so the only thing you need to care about is the X11 key symbols (no key codes, scan codes and other nightmares). However, naturally this tool won’t work under Wayland (which supports fractional scaling etc), and it cannot get automatically loaded by my GNOME 3 (even with autostart). Some say it’s been deprecated, and people should use xkb instead, so no luck here.

The other way is to modify the xkb key symbol database. Although it doesn’t provide any means of overriding in /etc/, you can directly edit the files under /usr/share/X11/xkb/symbols/. The interesting files are pc for the standard keys, and inet for the special keys, and here’s the patch I’ve been using for two years:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
diff --git a/pc b/pc.zhnew
index 0199713..33631cb 100644
--- a/pc
+++ b/pc.zhnew
@@ -29,7 +29,7 @@ xkb_symbols "pc105" {
key <RTSH> {[Shift_R]};
key <RCTL> {[Control_R]};
key <RWIN> {[Super_R]};
- key <MENU> {[Menu]};
+ key <PRSC> {[Menu]};

// Beginning of modifier mappings.
modifier_map Shift { Shift_L, Shift_R };
@@ -64,7 +64,7 @@ xkb_symbols "pc105" {

hidden partial alphanumeric_keys
xkb_symbols "editing" {
- key <PRSC> {
+ key <INS> {
type= "PC_ALT_LEVEL2",
symbols[Group1]= [Print, Sys_Req]
};
@@ -73,7 +73,7 @@ xkb_symbols "editing" {
type= "PC_CONTROL_LEVEL2",
symbols[Group1]= [Pause, Break]
};
- key <INS> {[Insert]};
+ key <MENU> {[Insert]};
key <HOME> {[Home]};
key <PGUP> {[Prior]};
key <DELE> {[Delete]};

diff --git a/inet b/inet.zhnew
index 755597e..f4b86d6 100644
--- a/inet
+++ b/inet.zhnew
@@ -121,13 +121,13 @@ xkb_symbols "evdev" {

// key <I120> { [] }; // KEY_MACRO
key <I126> { [plusminus] };
- key <I128> { [XF86LaunchA] };
+ key <I173> { [XF86LaunchA] };
key <I147> { [XF86MenuKB] };
key <I148> { [XF86Calculator] };
// key <I149> { [] }; // KEY_SETUP
key <I150> { [XF86Sleep] };
key <I151> { [XF86WakeUp] };
- key <I152> { [XF86Explorer] };
+ key <I171> { [XF86Explorer] };
key <I153> { [XF86Send] };
// key <I154> { [] }; // KEY_DELETEFILE
key <I155> { [XF86Xfer] };
@@ -146,15 +146,15 @@ xkb_symbols "evdev" {
// key <I168> { [] }; // KEY_CLOSECD (opposite of eject)
key <I169> { [XF86Eject] };
key <I170> { [XF86Eject, XF86Eject] };
- key <I171> { [XF86AudioNext] };
- key <I172> { [XF86AudioPlay, XF86AudioPause] };
- key <I173> { [XF86AudioPrev] };
- key <I174> { [XF86AudioStop, XF86Eject] };
+ key <I152> { [XF86AudioNext] };
+ key <I179> { [XF86AudioPlay, XF86AudioPause] };
+ key <I128> { [XF86AudioPrev] };
+ key <I225> { [XF86AudioStop, XF86Eject] };
key <I175> { [XF86AudioRecord] };
key <I176> { [XF86AudioRewind] };
key <I177> { [XF86Phone] };
// key <I178> { [] }; // KEY_ISO
- key <I179> { [XF86Tools] };
+ key <I172> { [XF86Tools] };
key <I180> { [XF86HomePage] };
key <I181> { [XF86Reload] };
key <I182> { [XF86Close] };
@@ -188,7 +188,7 @@ xkb_symbols "evdev" {
// key <I222> { [] }; // KEY_QUESTION
key <I223> { [XF86Mail] };
key <I224> { [XF86Messenger] }; // KEY_CHAT
- key <I225> { [XF86Search] };
+ key <I174> { [XF86Search] };
key <I226> { [XF86Go] }; // KEY_CONNECT
key <I227> { [XF86Finance] };
key <I228> { [XF86Game] }; // KEY_SPORT

It works perfectly, without any overhead. The only problem with it, is that because these things live inside /usr/, which is managed by pacman, it is reverted to packaged version every time xkeyboard-config is updated, and it actually do get updated sometimes. In that case I’ll find me suddenly making screenshots again, and need to patch those files and reboot for things to work.

Using udev hwdb

Recently when I was doing some brief research about new ThinkPads, I came across the ArchWiki for ThinkPad T480, which mentioned using something called hwdb to add support for its two special buttons. It looked promising, and I finally took some hours today to figure it out for my own remapping.

The hwdb in udev works on a much lower level: it maps the scan codes from your keyboard to standard key codes, and /etc/udev/hwdb.d/ provides a means of customization, which allows overriding the way scan codes are mapped. Some more detail can be found out on Arch Wiki.

And here is the final hwdb file I came up with:

1
2
3
4
5
6
7
8
9
10
11
# /etc/udev/hwdb.d/90-zh-thinkpad.hwdb

evdev:atkbd:dmi:bvn*:bvr*:bd*:svnLENOVO*:pn*:pvrThinkPad*
KEYBOARD_KEY_b7=compose
KEYBOARD_KEY_d2=sysrq

evdev:name:ThinkPad Extra Buttons:dmi:bvn*:bvr*:bd*:svnLENOVO*:pn*:pvrThinkPad*
KEYBOARD_KEY_1c=playpause
KEYBOARD_KEY_1d=stopcd
KEYBOARD_KEY_1e=previoussong
KEYBOARD_KEY_1f=nextsong

The hwdb rules we need to write consists of two parts: matching and mapping. The matching expression is a shell glob that matches the device, where as the mapping maps the scan code (in hex) to key code macro names in kernel’s (include/uapi/linux/input-event-codes.h)[https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h]. man hwdb provided some simple example, but actually comments in the built-in hwdb file provides much more details about this file.

In order to find out the scan codes for my keys, I tried both methods in Arch Wiki. The traditional showkey --scancodes didn’t work well for me, requiring switching to a tty, and was printing multiple bytes of hex for a single keystroke of mine. In contrast, evtest was just the right tool. Just execute sudo evtest in a terminal and select something like AT Translated Set 2 keyboard for the builtin keyboard (mine is 3), and you can test your keystrokes to find out its scan code in output like (MSC_SCAN), value <SCAN CODE HERE>.

Another caveat is that, for the special keys on ThinkPad keyboard, unlike regular keys they are not listed under the AT Translated Set 2 keyboard, but actually under another input device named ThinkPad Extra Buttons. It took me some time to realize this, and I also tried showkey which disappointed me again.

After we’ve got the scan codes from evtest and key codes from input-event-codes.h, it’s time to write the rule. We need the matching part for the two devices, whose format specification is available in the comment inside systemd’s bulitin hwdb file. The exact info for your current machine can be obtained from cat /sys/class/dmi/id/modalias, and combining with other existing ThinkPad rules in the builtin file I derived mine successfully.

The actual rules read by udev upon boot is a compiled binary file called hwdb.bin, so one will need to compile the configuration files into binary with sudo systemd-hwdb update. To make the changes take effect immediately, run sudo udevadm trigger, and finally, try out the new key mapping!

One last thing… the Menu key

Most of my key mapping worked, except for my new Menu key. I double checked the scan code and the key code name – they both seemed correct. In input-event-codes.h, I also found KEY_OPITON and KEY_CONTEXT_MENU, but neither of them worked as Menu key as well.

So I tried xev. Interestingly, it printed XF86MenuKB as the key symbol on the X11 level, instead of Menu. This must be something with the X11 key symbol database. I did some grep, played around for around half an hour, and finally found out the answer when I expanded my search into /usr/share/X11/xkb/keycodes/evdev:

1
2
3
alias <MENU> = <COMP>;
...
<I147> = 147;// #define KEY_MENU 139

Combined with the output from xmodmap -pke | grep Menu:

1
2
keycode 135 = Menu NoSymbol Menu
keycode 147 = XF86MenuKB NoSymbol XF86MenuKB

I surprisingly found out that my key code KEY_MENU was mapped to key symbol XF86MenuKB. And to map to key symbol Menu, I actually need to map my scan code to key code KEY_COMP.

So one last change, save, and sudo systemd-hwdb update && sudo udevadm trigger. Hooray! All my keys are working flawlessly now, and I don’t need to worry about package updates anymore (just forget about xkeyboard-config). Although the obscure and scattered documentation put me through these tedious trial-and-error attempts, it still kinda excited me when I finally got my keys remapped correctly, after all these years.

Natural sort order is what we usually see in most file managers when browsing a list of numbered files, that it sorts the numerical part of the file name as a number instead of comparing it character by character. For instance:

Lexicographical sort Natural sort
1.txt 1.txt
10.txt 2.txt
100.txt 3.txt
101.txt 4.txt
102.txt 5.txt
103.txt 10.txt
104.txt 11.txt
105.txt 12.txt
11.txt 13.txt
12.txt 14.txt
13.txt 15.txt
14.txt 100.txt
15.txt 101.txt
2.txt 102.txt
3.txt 103.txt
4.txt 104.txt
5.txt 105.txt

Lexicographical sort can be easily implemented using String::compareToIgnoreCase, but it is not very acceptable for end users. However for natural sort, things is actually a little more complicated.


The Definition

There is a small Wikipedia article on Natural Sort Order, which mentioned the following definition for natural sort order:

Natural sort order is an ordering of strings in alphabetical order, except that multi-digit numbers are ordered as a single character.

However, there’s still some undefined behavior about this sort order when case-insensitivity is involved alongside treating numbers as a whole. In an article discussing natural sort order in C, the author mentioned the following case:

Name
Test1
tesT1
test1
tEst2
test2
test3

These string is listed in a reasonable way, however if you simply define the natural sorting to be sorting chunks of characters and numbers individually, the Test string have to be equal to test so that the ordering won’t be deterministic.

And there’s another case involving leading zeros:

Name
test1
test01
test001
test01a

Intuitively, things that are shorter should come before those longer ones, so equal numbers with less leading zeros should come before those with more. Furthermore, if there’s more text following the number, it should come after those without, thus the aforementioned leading zero comparison has a lower priority.

I didn’t find any standard for these corner cases, so I’m merely deciding on the most reasonable approach.

Various Implementations

There’s an article on CodingHorror and a StackOverflow question that referenced some existing implementations.

And here are some additional implementations that I found inside some battlefield applications:

But there’s some common issues in existing implementations:

  • Use of regular expression: The comparator is frequently evaluated during the sort, so performance is our concern and splitting on regular expressions is not what we expect under such conditions.
  • Memory allocation: Also due to the frequent invocation, we should avoid memory allocation (e.g. actually building those “chunks”), and this can be done by iteration over the two strings.
  • Unicode awareness: Unicode code points are not necessarily single chars (think of surrogates), so those code point related methods should be used instead of simple increment to advance the index of current “character”.
  • Locale awareness: Instead of c >= '0' && c <= '9', one should use Character.isDigit() and Character.digit() for getting numbers out of a character.
  • Correctness: Some implementations cannot handle the two aforementioned corner cases properly.
  • Coding style: Some implementations are simply not so readable.

My Implementation

So I wrote my own implementation with those issues in mind, and it basically iterates over the two string and handles consecutive digits together. Most of the code is self-explanatory and the comments can also describe the algorithm:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
public class NaturalOrderComparator implements Comparator<String> {

private static final int DIGIT_RADIX = 10;

@Override
public int compare(String string1, String string2) {
int start1 = 0;
int start2 = 0;
int leadingZeroCompareResult = 0;
while (start1 < string1.length() && start2 < string2.length()) {
int codePoint1 = string1.codePointAt(start1);
int codePoint2 = string2.codePointAt(start2);
// Check if both code points are digits.
if (!Character.isDigit(codePoint1) || !Character.isDigit(codePoint2)) {
if (!codePointEqualsIgnoreCase(codePoint1, codePoint2)) {
return codePointCompareToIgnoreCase(codePoint1, codePoint2);
}
start1 = string1.offsetByCodePoints(start1, 1);
start2 = string2.offsetByCodePoints(start2, 1);
continue;
}
// Get end of current number.
int end1 = start1;
do {
end1 = string1.offsetByCodePoints(end1, 1);
} while (end1 < string1.length() && Character.isDigit(string1.codePointAt(end1)));
int end2 = start2;
do {
end2 = string2.offsetByCodePoints(end2, 1);
} while (end2 < string2.length() && Character.isDigit(string2.codePointAt(end2)));
// Get start of current number without leading zeros.
int noLeadingZeroStart1 = start1;
while (noLeadingZeroStart1 < end1 && Character.digit(string1.codePointAt(
noLeadingZeroStart1), DIGIT_RADIX) == 0) {
noLeadingZeroStart1 = string1.offsetByCodePoints(noLeadingZeroStart1, 1);
}
int noLeadingZeroStart2 = start2;
while (noLeadingZeroStart2 < end2 && Character.digit(string2.codePointAt(
noLeadingZeroStart2), DIGIT_RADIX) == 0) {
noLeadingZeroStart2 = string2.offsetByCodePoints(noLeadingZeroStart2, 1);
}
// If the two lengths of numbers (without leading zeros) differ, the shorter one comes
// first.
int noLeadingZeroLength1 = string1.codePointCount(noLeadingZeroStart1, end1);
int noLeadingZeroLength2 = string2.codePointCount(noLeadingZeroStart2, end2);
if (noLeadingZeroLength1 != noLeadingZeroLength2) {
return noLeadingZeroLength1 - noLeadingZeroLength2;
}
// If any two digits starting from the first non-zero ones differs, the less one comes
// first.
for (int digitIndex1 = noLeadingZeroStart1, digitIndex2 = noLeadingZeroStart2;
digitIndex1 < end1; digitIndex1 = string1.offsetByCodePoints(digitIndex1, 1),
digitIndex2 = string2.offsetByCodePoints(digitIndex2, 1)) {
int digit1 = Character.digit(string1.codePointAt(digitIndex1), DIGIT_RADIX);
int digit2 = Character.digit(string2.codePointAt(digitIndex2), DIGIT_RADIX);
if (digit1 != digit2) {
return digit1 - digit2;
}
}
// If the two numbers are the same, the one with less leading zeros (shorter) comes
// first.
int leadingZeroLength1 = string1.codePointCount(start1, noLeadingZeroStart1);
int leadingZeroLength2 = string2.codePointCount(start2, noLeadingZeroStart2);
if (leadingZeroLength1 != leadingZeroLength2) {
if (leadingZeroCompareResult == 0) {
leadingZeroCompareResult = leadingZeroLength1 - leadingZeroLength2;
}
}
start1 = end1;
start2 = end2;
}
// If one of the two strings is exhausted first, it comes first.
int remainingLength1 = string1.codePointCount(start1, string1.length());
int remainingLength2 = string2.codePointCount(start2, string2.length());
if (remainingLength1 != remainingLength2) {
return remainingLength1 - remainingLength2;
}
// The one with less leading zeros (shorter) comes first if others are the same.
if (leadingZeroCompareResult != 0) {
return leadingZeroCompareResult;
}
// Fall back to plain comparison.
return string1.compareTo(string2);
}

// @see String#regionMatches(boolean, int, String, int, int)
private static boolean codePointEqualsIgnoreCase(int codePoint1, int codePoint2) {
codePoint1 = Character.toUpperCase(codePoint1);
codePoint2 = Character.toUpperCase(codePoint2);
if (codePoint1 == codePoint2) {
return true;
}
codePoint1 = Character.toLowerCase(codePoint1);
codePoint2 = Character.toLowerCase(codePoint2);
return codePoint1 == codePoint2;
}

// @see String.CaseInsensitiveComparator#compare(String, String)
private static int codePointCompareToIgnoreCase(int codePoint1, int codePoint2) {
if (codePoint1 != codePoint2) {
codePoint1 = Character.toUpperCase(codePoint1);
codePoint2 = Character.toUpperCase(codePoint2);
if (codePoint1 != codePoint2) {
codePoint1 = Character.toUpperCase(codePoint1);
codePoint2 = Character.toUpperCase(codePoint2);
if (codePoint1 != codePoint2) {
return codePoint1 - codePoint2;
}
}
}
return 0;
}
}

The code is also available as a GitHub gist and is licensed under the Apache 2.0 license.

Further Considerations

In fact, to account for Unicode’s compatibility decomposition, canonical composition and case folding, an ICU Normalizer should be used with mode NFKC_Casefold. However, since the ICU package is not available on Android until Android Oreo, and the JDK implementation didn’t account for such level of correctness, I’m currently leaving it out and you can trivially add it by applying the normalization before comparison.

For years OpenWeather has been my favorite GNOME extension for quick and beautiful weather information. However today when I looked at my top bar again for weather, I had the impression as if something is missing:

And this is what it used to be like:

My degree Celsius symbol is missing!


Finding the culprit

The first thing I thought was, is this specific to OpenWeather? And I was surprised to find out that when I googled “degree Celsius symbol”, the °C as U+00B0 ° degree sign plus U+0043 C latin capital letter c is present, while ℃ as U+2103 ℃ degree celsius is simply empty. However a <span> with font-family: serif on Wikipedia showed the symbol correctly, so this must be a font/fontconfig issue.

And I lessed my own fontconfig configuration with /etc/fonts/local.conf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<match>
<test name="family"><string>sans-serif</string></test>
<edit name="family" mode="prepend" binding="strong">
<string>WenQuanYi Micro Hei</string>
<string>WenQuanYi Zen Hei</string>
<string>WenQuanYi Bitmap Song</string>
<string>DejaVu Sans</string>
<string>Noto Color Emoji</string>
</edit>
</match>
<match>
<test name="family"><string>serif</string></test>
<edit name="family" mode="prepend" binding="strong">
<string>DejaVu Serif</string>
<string>WenQuanYi Bitmap Song</string>
<string>WenQuanYi Zen Hei Sharp</string>
<string>AR PL UMing CN</string>
<string>AR PL UMing TW</string>
<string>AR PL New Sung</string>
<string>Noto Color Emoji</string>
</edit>
</match>
<match>
<test name="family"><string>monospace</string></test>
<edit name="family" mode="prepend" binding="strong">
<string>WenQuanYi Micro Hei Mono</string>
<string>WenQuanYi Zen Hei Mono</string>
<string>WenQuanYi Bitmap Song</string>
<string>DejaVu Sans Mono</string>
<string>Noto Color Emoji</string>
</edit>
</match>
</fontconfig>

So the different order of font families in sans-serif and serif might be the issue. I googled a while for finding out the font selected by fontconfig for a glyph, and found an article suggesting the use of FC_DEBUG and pango-view:

1
FC_DEBUG=4 pango-view -q --font='<YOUR_FONT_FAMILY>' -t '<YOUR_CHARACTER>' 2>&1 | grep -o 'family:"[^"]+' | cut -c 10- | tail -n 1

The latter part basically filters out the last printed family name from the tedious debug output. And when I run this for my degree Celsius symbol (), I found:

What’s wrong with WenQuanYi Micro Hei? I’ve been using it for years, and now it’s giving me nothing for degree Celsius symbol? And why did fontconfig ever choose it seemed to have no such glyph?

Confused by this I opened my wqy-microhei.ttc with FontForge, and was again surprised to find out that my WenQuanYi Micro Hei is providing an empty glyph (instead of nothing) for certain codepoints!

This is crazy, and I’m left wondering how my degree Celsius symbol ever worked in the past few years.

Finding the way to fix it

There must be some configuration that allow me to blacklist certain glyphs in a font. And after another round of digging around, I found some interesting results.

The first one is a bug on fontconfig Bugzilla which requested such support back in 2006 and was marked fixed in 2011. Feeling excited, I scrolled down and found a single line of comment from the dev:

This is fixed in master with target=scan charset editing.

After all the hard work, why didn’t he or she celebrate the moment of closing this bug with a more detailed explanation, or at least a helpful pointer? Even after reading though man fonts-conf, I found nothing describing how I can edit the charset of a font family. After all, the <charset> element says it must contain one or more integer, however the whole charset normally contains a huge amount of hexadecimal integers.

So I searched harder and found another answer on StackOverflow. It suggested an undocumented way of configuration that’s mentioned somewhere in a RedHat bug thread, which was its only appearance on the whole Internet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<match target="scan">
<test name="family" compare="not_eq">
<string>VL Gothic</string>
</test>
<edit name="charset" mode="assign">
<minus>
<name>charset</name>
<range>
<int>0x0021</int>
<int>0x00FF</int>
</range>
</minus>
</edit>
</match>

This is just what i wanted! So I happily applied this to my mischievous WenQuanYi Micro Hei, changing the compare to "eq" and setting the range to include 0x2103 (My precious degree Celsius symbol). Finally, sudo fc-cache -f && fc-cache -f! [sigh of relief]

Nope.

This could have worked, in some time of the history, but is not working for now, on my latest Arch Linux installation.

No.

But wait. I did saw something mentioning, we have a <charset> element and it must contain some integers? So I started to coin my own solution (You should also do the same for WenQuanYi Micro Hei Mono):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<match target="scan">
<test name="family">
<string>WenQuanYi Micro Hei</string>
</test>
<edit name="charset">
<minus>
<name>charset</name>
<charset>
<int>0x2103</int>
<int>0x2109</int>
<int>0x212B</int>
<int>0x2164</int>
<int>0x2169</int>
</charset>
</minus>
</edit>
</match>

sudo fc-cache -f && fc-cache -f, pango-view again to confirm, and Alt+F2 r to restart my GNOME shell. It worked! After all these hours, my degree Celsius symbol is back and alive again!

A final note

The above solution is only a workaround, and to actually fix this, The Wenquanyi Micro Hei font itself should be patched. In fact this was what I first tried when I surprisingly found out those empty glyphs.

However, when I went to wenq.org, there latest update was posted in late 2011 (We are in 2018 now), and their forum is giving me a blank page with certain resources returning 404. Their SourceForge issue tracker is also unattended for years with that notorious hangul advance bug still open. Sadly, the whole project seems dead. So I decided to work around instead of fixing this.

And one sentence I encountered during my search kept lingering in my mind:

When you gaze long into fontconfig, fontconfig also gazes into you.

Update: With another link on their official website, I was able to find their real bug tracker and this bug, and a similar bug. The dev said it’s fixed, but maybe some recent fontconfig or freetype changes brought it back. Maybe some day I should fork it and try to actually fix these bugs.

And according to the dev’s comment, those glyphs existed in Droid Sans Fallback, so a proper fix to my configuration should also include adding Droid Sans Fallback to the sans-serif and monospace families, right below the WenQuanYi Micro Hei entry.

前言

JavaScript 是一门灵活的动态语言,在实现应用功能时十分易用。最近有写作图形界面程序的需要,又比较喜爱 GTK+ 3 的设计,因此想到可以尝试使用 JavaScript 来完成任务。

选择运行时

首选的运行时当然是 node,因为它有着良好的 ES2015 特性支持,并且可以方便地使用 npm 管理的模块。在经过一番搜索之后,我找到了 WebReflection/node-gtk 这个项目,却发现它无法使用 node-gyp 在我的 Arch Linux 上进行编译。另一个 [creationix/node-gir] 提供了对 GObject Introspection 的绑定,但是在它的 README 中写明了有 bug 和内存管理等问题,看上去也难以令人满意。

因此,我选择了使用 GJS 作为运行时。虽然没有 node 的 ES2015 支持和使用不同模块的便利,但对于使用 GObject Introspection 的绑定而言足够可靠。

编写应用程序

GNOME 官方提供了 一个简单的 GJS 程序示例,可以按照它的架构进行编程。

文档

技巧

通过构造器设置属性

Gtk.Widget 的所有属性均可以通过向构造器传入的 Object 进行设置,而可用的属性可以通过运行 gtk3-widget-factory,然后启用 GtkInspector 来查看和实验。

布局

可以一直使用 Gtk.Grid 进行布局,它的功能类似于 Android 中的 LinearLayout。可以设置它的 border_widthrow_spacingcolumn_spacing 来控制留白,以及设置 expandhexpandvexpand 控制大小。通过调用(继承自 Gtk.Container 并经过覆盖)add() 方法即可按顺序添加子控件,不需要使用 attach() 那样复杂的功能。

设置 CSS Class

例如给 Gtk.Button 添加 suggested-action CSS 类使其变蓝:

1
button.get_style_context().add_class('suggested-action');

控件杂项

Gtk.Entry 相当于 Android 中的 EditText。可以通过设置 width_char 来修改 Gtk.Entry 的最小大小。

Gtk.Widigetsensitive 属性相当于 Android 中的 android:enabled

结语

Gjs 使得我们可以使用灵活的 JavaScript 编写美观的原生程序,总体来说体验是很好的。

我把自己编写的一个小程序放在了 GitHub Gist,可以作为参考。

Gjs 应用程序 OpticalSystem

最后,在编写这篇文章时,我找到了另一个 结合了 Gjs、NPM 和 Babel 的示例项目,读者也可以参考它进行编程。

前言

临近毕设,学院里提供了毕业设计报告的 LaTeX 模板,于是决定离开简单的 Markdown ,开始使用 LaTeX 进行写作。以下就是我使用过程中的一些记录。

样例项目

我对学院提供的旧 LaTeX 模板进行了较多的修改,使其符合了学院方面的最新格式要求,并且修复了设置字号时行间距不正确等等错误,可以直接使用或者用于参考学习:

浙江大学计算机科学与技术、软件工程专业本科毕业设计开题报告 LaTeX 模板

其中的 install-fonts.sh 可以用于从 Windows 分区获取需要的字体并进行安装。

安装 LaTeX

在 Arch Linux 上安装 LaTeX(TeXLive)较为简单,可以参考 Arch Wiki 安装对应的软件包。

也可以直接执行以下命令进行安装,这样基本不会在使用时遇到无法找到常用宏包的情况。

1
sudo pacman -S texlive-most texlive-langchinese

构建文档

xeCJK 是提供 LaTeX 中文支持的宏包,并且依赖于 XeLaTeX,因此,我们需要使用 xelatex 命令进行构建。

LaTeX 在构建交叉索引时需要多次运行,才能最终解析所有的引用,并且期间需要 BibTeX 对参考文献数据库进行处理。因此,一般的手动构建命令是:

1
2
3
4
xelatex main
bibtex main
xelatex main
xelatex main

不同于部分网页上给出的示例,这些命令都可以接受不带后缀名的参数,并且有时写出后缀名可能会阻碍多文件等情况下的正确构建。

对于子目录和多文件使用引用的情况,在主文件中应该使用 include 而非 input,否则会需要其他处理。

为了简化构建步骤,实际上应当使用 mklatex,它会根据需要自动调用各种命令。因此之前的手动构建步骤可以被以下命令替代:

1
mklatex -xelatex main

了解 LaTeX 和解决问题

LaTeX 有很多历史问题带来的不同,也有多重相似但不同的工具选择,因此网上提供的解决方案不一定可以工作,需要自己试验。

可以通过 ShareLaTeX 提供的文档 获得对 LaTeX 的初步了解。但是,这份文档中提到的引用等高级特性的使用可能与我们的方式不同。

推荐使用 TeX – LaTeX Stack Exchange 作为解决问题时的主要信息来源。

可以通过 texdef 命令查看 LaTeX 中命令的定义。例如:

1
texdef -t xelatex -s -c csbachelor thebibliography

零碎知识

  • \:换行,不新建段落。可选参数可以控制空白长度。
  • par:创建段落。parskipparindent 将生效。部分命令需要对段落才能生效,因此可能需要在最后一段文字最后加上 par
  • :一个空格。
  • &:一个 NBSP,不会被分行打断,常用于类似 图~1 这样的场景。
  • hspace{1em}:一个 1 em 长度的空格。
  • renewcommand:修改命令定义。
  • setlength:修改长度变量。
  • setcounter:修改计数器。

结语

LaTeX 十分强大,并且作为纯文本格式依然比 Word 文档等更加清晰、明确、可靠。但是,整个 LaTeX 环境也正因为它的高自由度和复杂,在许多方面缺乏一致性,这给使用者带来了不少困难。

总体来说,在经过大量的学习和尝试之后,我依然认为 LaTeX 是一个优秀的排版工具。它可以让使用者在一次配置之后获得强大的功能,并且在写作过程中始终保持工作在纯文本层面上。

关于各种命令和功能的常见用法,读者可以继续参考我在文章开头提到过的 报告模板,我也会在写作毕业设计论文时继续更新。

前言

因为计算机图形学课程作业的需要,我在使用过 OpenGL 的基础上学习了使用 WebGL 进行二维图形渲染。

WebGL 拥有与 OpenGL ES 相似的 API(前者基于后者),但与 OpenGL 相比,两者缺少基础图形的渲染管线,而是需要手写 shader 进行渲染。

基础

请阅读 WebGL Fundamentals 以获得对于 WebGL 的大体了解。

Shader

在 WebGL 中需要两种 shader 进行渲染。第一种是 vertex shader,负责对顶点进行变换,例如将像素坐标映射到 clip space。而第二种是 fragment shader,负责对像素点进行着色。

Shader 可以有两种参数。第一种是 attribute,在每次调用中不同,例如当前顶点的位置;第二种是 uniform,在渲染过程中共享,例如渲染的变换矩阵。

以下是两个实用的简单 2D shader:

Vertex shader:

1
2
3
4
5
6
7
attribute vec2 a_position;
uniform mat3 u_transformation;
void main() {
gl_Position = vec4((u_transformation * vec3(a_position, 1)).xy, 0, 1);
}

Fragment shader:

1
2
3
4
5
6
7
precision mediump float;
uniform vec4 u_color;
void main() {
gl_FragColor = u_color;
}

初始化

以下是我的一些工具函数,参考了 WebGL Boilerplate 和一部分 WebGL – Less Code, More Fun

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function createAndCompileShader(gl, id) {
var element = document.getElementById(id);
var type = null;
switch (element.type) {
case 'x-shader/x-vertex':
type = gl.VERTEX_SHADER;
break;
case 'x-shader/x-fragment':
type = gl.FRAGMENT_SHADER;
break;
default:
throw 'Unknown shader type:' + element.type;
}
var source = element.textContent;
var shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
throw 'Error compiling shader:' + gl.getShaderInfoLog(shader);
}
return shader;
}
function createAndLinkProgram(gl, vertexShaderId, fragmentShaderId) {
var program = gl.createProgram();
gl.attachShader(program, createAndCompileShader(gl, vertexShaderId));
gl.attachShader(program, createAndCompileShader(gl, fragmentShaderId));
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw 'Error linking program:' + gl.getProgramInfoLog(program);
}
return program;
}

提供数据

以下是我自己写作的一些工具函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* @param {[float]} value
*/
function setUniformFv(gl, program, name, value) {
gl['uniform' + value.length + 'fv'](gl.getUniformLocation(program, name), value);
}
/**
* @param {string} color '#RRGGBB'
* @param {float=} alpha in [0, 1]
*/
function setUniformColor(gl, program, name, color, alpha) {
if (!/^#[0-9A-Fa-f]{6}$/.test(color)) {
throw 'Invalid color:' + color;
}
setUniformFv(gl, program, name, [
parseInt(color.substr(1, 2), 16) / 0xff,
parseInt(color.substr(3, 2), 16) / 0xff,
parseInt(color.substr(5, 2), 16) / 0xff,
alpha || 1
]);
}
function setUniformMatrix(gl, program, name, value) {
gl['uniformMatrix' + Math.sqrt(value.length) + 'fv'](gl.getUniformLocation(program, name), false, value);
}
/**
* @param {[[float]]} value
*/
function setAttributeArrayFva(gl, program, name, value) {
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
var data = new Float32Array(Array.prototype.concat.apply([], value));
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
var location = gl.getAttribLocation(program, name);
gl.enableVertexAttribArray(location);
var size = value[0].length;
gl.vertexAttribPointer(location, size, gl.FLOAT, false, 0, 0);
}

变换矩阵

以下代码参考了 WebGL 2D TranslationWebGL Orthographic 3D。需要注意的是,原教程的代码中有部分矩阵错误地以行优先而非列有限的方式进行表示或运算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
function makeIdentityMatrix() {
return [
1, 0, 0,
0, 1, 0,
0, 0, 1
];
}
function makeTranslationMatrix(tx, ty) {
return [
1, 0, 0,
0, 1, 0,
tx, ty, 1
];
}
function makeScalingMatrix(sx, sy) {
return [
sx, 0, 0,
0, sy, 0,
0, 0, 1
];
}
function degreeToRadian(angle) {
return angle / 180 * Math.PI;
}
function makeRotationMatrix(angle) {
angle = degreeToRadian(angle);
var cosa = Math.cos(angle);
var sina = Math.sin(angle);
return [
cosa, sina, 0,
-sina, cosa, 0,
0, 0, 1
];
}
function makeSkewXMatrix(angle) {
angle = degreeToRadian(angle);
var tana = Math.tan(angle);
return [
1, 0, 0,
tana, 1, 0,
0, 0, 1
];
}
function makeSkewYMatrix(angle) {
angle = degreeToRadian(angle);
var tana = Math.tan(angle);
return [
1, tana, 0,
0, 1, 0,
0, 0, 1
];
}
function makeProjectionMatrix(width, height) {
return [
2 / width, 0, 0,
0, -2 / height, 0,
-1, 1, 1
];
}
function multiplyMatrix(a, b) {
// A cdot B = (B^T cdot A^T)^T
var a00 = a[0], a01 = a[1], a02 = a[2];
var a10 = a[3], a11 = a[4], a12 = a[5];
var a20 = a[6], a21 = a[7], a22 = a[8];
var b00 = b[0], b01 = b[1], b02 = b[2];
var b10 = b[3], b11 = b[4], b12 = b[5];
var b20 = b[6], b21 = b[7], b22 = b[8];
return [
b00 * a00 + b01 * a10 + b02 * a20,
b00 * a01 + b01 * a11 + b02 * a21,
b00 * a02 + b01 * a12 + b02 * a22,
b10 * a00 + b11 * a10 + b12 * a20,
b10 * a01 + b11 * a11 + b12 * a21,
b10 * a02 + b11 * a12 + b12 * a22,
b20 * a00 + b21 * a10 + b22 * a20,
b20 * a01 + b21 * a11 + b22 * a21,
b20 * a02 + b21 * a12 + b22 * a22
];
}

近似于 OpenGL 的 API 封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
var _gl = null;
var _program = null;
function initialize(gl, program) {
_gl = gl;
_program = program;
}
function setColor(color, alpha) {
setUniformColor(_gl, _program, 'u_color', color, alpha);
}
var _transformations = [];
function getTransformation() {
return _transformations[_transformations.length - 1];
}
function _setTransformation(transformation) {
setUniformMatrix(_gl, _program, 'u_transformation', transformation);
}
function setTransformation(transformation) {
_setTransformation(transformation);
_transformations = [transformation];
}
function pushTransformation(transformation) {
transformation = multiplyMatrix(getTransformation(), transformation);
_setTransformation(transformation);
_transformations.push(transformation);
}
function popTransformation() {
_transformations.pop();
_setTransformation(getTransformation());
}
function drawTriangles(positions) {
setAttributeArrayFva(_gl, _program, 'a_position', positions);
_gl.drawArrays(_gl.TRIANGLES, 0, positions.length);
}
function drawRectangle(x, y, width, height) {
var x1 = x;
var y1= y;
var x2 = x + width;
var y2 = y + height;
drawTriangles([
[x1, y1],
[x2, y1],
[x1, y2],
[x1, y2],
[x2, y1],
[x2, y2]
]);
}

绘制和应对大小改变

以下代码参考了 WebGL Resizing the CanvasWebGL Anti-Patterns

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function draw(gl) {
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
gl.clearColor(0xde / 0xff, 0x29 / 0xff, 0x10 / 0xff, 0xff / 0xff);
gl.clear(gl.COLOR_BUFFER_BIT);
var program = createAndLinkProgram(gl, 'vertex-shader', 'fragment-shader');
gl.useProgram(program);
initialize(gl, program);
setColor('#ffdd00');
setTransformation(makeProjectionMatrix(1000, 800));
// Draw here.
gl.flush();
}
function main() {
var canvas = document.getElementById('canvas');
var webgl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!webgl) {
alert("Error initializing WebGL. Your browser may not support it.");
return;
}
var needToDraw = true;
function resizeAndDraw() {
var resized = canvas.width !== canvas.clientWidth || canvas.height !== canvas.clientWidth * 0.8;
if (resized) {
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientWidth * 0.8;
}
if (resized || needToDraw) {
needToDraw = false;
draw(webgl);
}
requestAnimationFrame(resizeAndDraw);
}
requestAnimationFrame(resizeAndDraw);
}
main();

结语

作为一个复杂度适中的示例,我的作业代码可以在 这里 看到。

String 类

String 类表示一个字符串。 在设计上, 字符串类存储的字符串是不可变的(immutable)。想要修改一个字符串的内容只能通过新建一个新的字符串实例来实现。 且对于两个相同的字符串字面量而言, 他们指向的是同一个字符串对象。

并且String类相对于其他类, 具有以下特殊性:

  • String类是Java中唯一的具有重载运算符的类。 String重载了+运算符, 使得对于一个加号左右两边的String能够被连接起来。

成员变量

String类具有以下的关键成员变量:

final char[] value

存储组成这个字符串的字符数组。 因为是final声明, 所以在类被创建之后是不可修改的。

final int count

存储在value[]数组中的字符个数, 在大多数情况下与value.length中的值相同。

final int offset

存储在value[]数组中字符的起始位置。 此变量的创建目的是为了substring()方法提供便利

私有类

private static final class CaseInsensitiveComparator

String类私有类, 其具有compare()方法, 用于提供对于字符串之间的大小写不敏感的比较。

构造器

public String ()

默认构造器, 创建一个空的字符串, 设置value为"",offset为0,count为0。

public String (String str)

拷贝构造器, 根据传入的参数str,新建一个新的字符串实例。但由于字符串是不可变的, 所以拷贝是以浅拷贝的方式执行的。

public String (char[] data)

根据传入的data字符数组, 初始化一个新的字符串实例。

public String (char[] data, int offset, int count)

根据传入的data字符数组, offset偏移量和count数据, 来初始化一个新的字符串实例。

public String (byte[] ascii, int hibyte, int offset, int count)

使用二进制的方式初始化一个字符串, 其中ascii数组中存放每一个字符的ascii码。 并且最终每一位的值c = (char)(((hibyte & 0xff) << 8) | (b & 0xff))

即用hibyte填充最终字符的最高八位,以及用ascii数组的值填充低八位。

public String (byte[] ascii, int hibyte)

使用String (byte[] ascii, int hibyte, int offset, int count) 作为 Delegate Constructor, 默认offset为0, count为ascii.length

public String (byte[] data, int offset, String encoding)

使用encoding参数所规定的方式来解码由data数组传入的字符, 并依此新建一个字符串。

public String (StringBuffer buffer)

使用StringBuffer中存储的字符序列构造一个新的字符串。StringBuffer中字符的改动并不会影响通过此构造器构建的字符串。

public String (StringBuilder buffer)

使用StringBuilder中的字符序列构造一个新的字符串, StringBuilder中字符的改动并不会影响通过此构造器构建的字符串。

StringBuffer 类

StringBuffer类表示一个 可变的(changeable) 字符串, 它提供了一系列成员方法来对StringBuffer实例进行修改, 包括insert, replace, delete, appendreverse等。

StringBuffer实例是变长的, 所以尽管在定义时, 可能是以固定长度定义的, 但它的长度都可以进行不断正常。

出于某种特定的原因, 编译器一般使用StringBuffer来重载+运算符对于String类型的运算。 即表达式str_a+str_b, 等价于
new StringBuffer().append(str_a).append(str_b).toString()

成员变量

StringBuffer类拥有以下的关键成员变量:

int count

下一个可用buffer位置的索引, 同时也是当前字符串内容的大小。 注意此处count是public权限, 即程序可以从外部访问该变量。

char[] value

存储StringBuffer中的字符串Buffer本身。

boolean shared

标记Buffer是否与其他对象共享, 如果为真, 则在对字符串进行写操作时, 需要先将其复制一份。

构造器

public StringBuffer()

默认构造器, 在默认情况下, 会构建一个以DEFAULT_CAPACITY为大小的StringBuffer,在JDK1.2中, 这个值为16.

public StringBuffer(int capacity)

这个构造器会新建一个大小为capacitychar数组, 并且将其作为成员变量value的当前值。

public StringBuffer(String str)

这个构造器接受一个String参数,并将其值赋予countvalue,需要注意的是,通过此构造器得到的字符串Buffer的大小是原字符串的大小加上DEFAULT_CAPACITY。但是字符串的大小(length)依然是原字符串

StringBuilder类

StringBuilder类表示一个 可变的(changeable) 字符串, 与StringBuffer类似, 它也提供了诸如insert,replace,delete, append, reverse之类的方法。 但是与StringBuilder不同的是,StringBuilder是不同步的, 只能在一个线程的情况下使用。

关键成员函数

int count

下一个可用buffer位置的索引, 同时也是当前字符串内容的大小。

char[] value

存储StringBuilder中的字符串Buffer本身。

构造器

public StringBuilder()

默认构造器, 创建构建一个以DEFAULT_CAPACITY为大小的StringBuilder

public StringBuilder(int capacity)

这个构造器会新建一个大小为capacity的char数组, 并且将其作为变量value的当前值。

public StringBuilder(String str)

这个构造器会根据传入的String参数,并将其值赋予countvalue。 StringBuilder通过此构造器得到的实例对象与StringBuffer一样, value数组的大小会空余一个DEFAULT_CAPACITY

public StringBuilder(CharSequence seq)

根据传入的CharSequence参数, 新建一个StringBuilder实例。 大小同样是有一个DEFAULT_CAPACITY的空余。

比较异同

String与(StringBuffer及StringBuilder)进行比较

相同点:

  1. 都表示了一个字符串序列
  2. 都是final类, 都不允许被继承
  3. 这个字符串序列都是支持Unicode的
  4. 都支持对于字符串序列内容的部分读取
  5. 都重载了加号运算符, 能够直接使用”+”来连接两个字符串

不同点:

  1. String类型的字符串是 不可变(immutable) 的, 即一个String实例在被创建之后, 其字符串的大小, 内容不能再被改变。 如果需要进行改变的话, 实际上是新建了一个String实例。
  2. 而对于StringBuffer和StringBuilder而言, 其本质上实现了一个变长数组(VLA),所以其字符串大小,内容都是可以进行变化的。在进行修改的时候, StringBuffer和StringBuilder各自实现了一个public void ensureCapacity(int minimumCapacity)的方法, 以确保时刻Buffer数组的大小要大于minimumCapacity, 否则就将数组扩充成其原来size的两倍再加二。
  3. String类型对于字符串本身的操作更多,诸如toLowerCase, toUpperCase这类大小写转换的方法, 以及matches, split,replace这类正则表达式方法, 以及trimconcat这类操作字符串的方法,都仅在String类中实现,而在StringBuffer与StringBuilder类中并没有提供以上接口。

StringBuffer 与 StringBuilder进行比较

相同点:

  1. 前面与String类型共有的相同点不赘述
  2. StringBuffer类与StringBuilder类都实现了一个 可变的(changeable) 字符串对象, 可以通过一系列方法对字符串内容进行诸如增加,删减,插入,替换,翻转等操作。
  3. 实现的字符串buffer的本质都是一个变长数组(VLA), 在任何时刻都尽量保证数组不越界, 如果接近了其边界, 就将数组的大小扩充成原来大小的两倍加二。
  4. 默认的buffer大小DEFAULT_CAPACITY都为16

不同点:

  1. StringBuilder类相较StringBuffer, 多了一个基于CharSequence建立的构造器。
  2. StringBuffer比StringBuilder类多了一个shared指示,以标记buffer内容是否被多个对象所引用。如果被多个引用的话,在进行操作的时候就要将这个buffer再复制一份, 以保证其单引用的性质。 这一点其实是为了保证操作内容的同步化进行的。
  3. 正如之前所说, StringBuffer和StringBuilder的最大不同点在于其操作的同步性。 StringBuffer在其大多数成员方法前都加上了synchronized关键字, 以保证其线程安全。 也就是说, 在同一时间, 只有一个线程能够对同一个StringBuffer对象进行操作,这种限制避免了线程之间读写的冲突。 而对于StringBuilder来说, 就没有这一层限制。故其有可能会引起线程之间的冲突, 但从另一个角度而言, 也减少了诸如线程锁之类的可能的系统开销。 故相比StringBuffer而言, StringBuilder的速度可能会更快一些。 故两者需要权衡使用。

前言

Reveal.js 是一个美观实用 HTML 演示文稿框架,只需要你决定内容,就可以方便地产出外观优雅的演示文稿。你可以在线查看 Reveal.js 的 Demo

为了分享已经制作好的演示文稿,可以使用 GitHub Pages 进行部署,同时也能够不用安装依赖地开启演讲者视图。以下是我建立 slides.zhanghai.me 的过程。

基本

为了建立一个 GitHub Pages 站点,我们需要准备以下一些文件:

  • 404.html:GitHub Pages 将使用此页面作为默认 404 页面的替代,一般可以换成一个符合自己站点风格的页面。
  • CNAME:对于绑定自定义域名的 GitHub Pages,可以使用这个文件指定自己的自定义域名。
  • .nojekyll:我们的站点不需要 Jekyll 的特性干预,因此将它关闭来避免可能的问题。

然后在 GitHub 上建立仓库,执行以下的命令:

1
2
3
4
5
6
7
git init
# 将默认的分支名 master 改为 gh-pages
git symbolic-ref HEAD refs/heads/gh-pages
git commit -am 'Initial commit.'
# 将 your/repo/origin 替换为你的仓库地址
git remote add origin your/repo/origin
git push --set-upstream origin gh-pages

当然,使用自定义域名的话还要配置一下 DNS 记录。

演示文稿

为了部署 Reveal.js 的演示文稿,可以利用 Git 的 Submodule 来完成对 Reveal.js 项目本身的引用。如此以来,我们的站点项目的 Git 仓库就不用跟踪 Reveal.js 中各个文件的状态,又可以记住所引用的 Reveal.js 仓库当时的版本(Commit SHA1)。

比较幸运的是,GitHub Pages 的构建过程是可以支持 Submodule 的。需要注意的是,Submodule 的仓库地址必须采用 https 而非 ssh 协议(否则会收到构建失败的邮件通知)。以下是将 Reveal.js 作为 Submodule 引入的命令:

1
git submodule add https://github.com/hakimel/reveal.js.git

之后将你的 index.html 中对于 Reveal.js 文件的应用都加上 reveal.js/ 的目录前缀即可。

其他详情可以参考我的站点仓库 DreaminginCodeZH/slides.zhanghai.me

结语

Reveal.js 是一个易用而优雅的 HTML 演示文稿框架,借助于它,我已经多年没有用过 PowerPoint 了。至于利用 GitHub Pages 将它放置在网页端,则可以在免运维的情况下可靠地分享自己的演示文稿,同时也可以不用安装大量 npm 依赖来开启演讲者视图,算是一个简单又有用的小技巧。

因此,将我的配置过程和结果记录在此,希望能对他人有所帮助。

前言

Travis CI 是 GitHub 上开源项目采用持续集成的常见选择。为了给 豆芽 提供持续集成版本用于公开测试,我配置了 Travis CI,并自己编写了脚本将构建结果发布到另一个空项目的 Release 中,并将其间的过程和遇到的问题记录于此。

Travis CI 构建 Android 项目的时间较长,因此调试配置时十分耗时。希望我的经验能对他人有所帮助。

Travis CI

Travis CI 分为免费版(travis-ci.org)和付费版(travis-ci.com),两者之间没有相互的链接或说明,第一次配置时容易混淆。开源项目选择免费版即可。配置过程可以参考官方的 Getting StartedAndroid 项目配置

关于 Android 项目有一些较为微妙的配置问题,我自己调试并查阅了一些 Issue 方才解决。

  1. 为了能够找到并下载最新的 Build Tools,需要启用最新版本的 Tools(- tools)。
  2. Lint 过程中如果 Platform Tools 版本低于 SDK 版本则会报错,需要启用最新版本的 Platform Tools(-platform-tools)。
  3. 为了能够找到并下载最新的 Platform Tools,需要已有最新的 Tools,因此与官方给出的样例不同,需要将 - tools 放置在 - platform-tools 之前。

因此我的 Android 部分最终配置如下:

1
android:
  components:
    - tools
    - platform-tools
    - build-tools-24.0.1
    - android-24
    - extra-android-m2repository

详细配置可以参考我的 .travis.yml

启用构建缓存

Gradle 是一个为缓存优化过的工具,因此官方也提供了相应的 开启缓存的方法

1
before_cache:
  - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
cache:
  directories:
    - $HOME/.gradle/caches/
    - $HOME/.gradle/wrapper/

应用签名

为了给 CI 版本的 APK 签名,需要提供相应的 keystore 和密码。Travis CI 提供了 在设置中定义环境变量 的方式来传递低敏感级的信息。

基于以上文档和一些搜索结果,并参考过 Shadowsocks-Android 的配置方式,我将我的签名配置编写为了从 signing.properties、环境变量、终端输入三个层级进行获取的方式。

以下是我的 signing.gradle

1
2
3
4
5
6
7
8
9
10
11
12
android {
signingConfigs {
release {
Properties signingProperties = new Properties()
signingProperties.load(rootProject.file("signing.properties").newDataInputStream())
storeFile rootProject.file(signingProperties.get("storeFile"))
storePassword signingProperties.get("storePassword") ?: System.getenv("STORE_PASSWORD") ?: System.console()?.readLine("nStore password:")
keyAlias signingProperties.get("keyAlias")
keyPassword signingProperties.get("keyPassword") ?: System.getenv("KEY_PASSWORD") ?: System.console()?.readLine("nKey password:")
}
}
}

在 Android Studio 中进行 Gradle 同步时,System.console() 返回 null,因而密码均为 null,不会中断同步过程,也不影响调试版本的构建。

我创建了 signing.propertiessigning.properties.travis 两个文件,在前者中填写 keystore 的路径并加入 .gitignore,而将后者在 .travis.ymlbefore_script 中复制为 signing.properties

而在 Travis CI 的设置中,添加 STORE_PASSWORDKEY_PASSWORD 两个环境变量即可。建议在环境变量值的两端加上单引号以避免特殊字符被 shell 错误处理。

获取版本信息

直接在 .travis.yml 中调用 git describegit log 等命令是无法成功的,因为 Travis CI 采用了 git clone --depth=50 来进行 clone。相应地,需要先执行 git fetch --unshallow 来完成 clone。

我采用了 语义化版本(Semver)来命名版本。因此,我的版本名称采用了如下方式获取:

1
version="$(git describe --long --tags | sed's/^v//;s/-([0-9]+)-g([0-9a-f]+)/+1.2/')"

例如,在名为 1.0.0-alpha 的 tag 后第 227 次短哈希值为 886f8ce 的 commit,对应的版本名即为 1.0.0-alpha.1+227.886f8ce

然后使用 sed -i 's/versionName .*/versionName "'"${version}"'"/' app/build.gradle 即可更新 build.gradle 中的 versionName 字段。

上传至 Release

GitHub 提供了在 Release 中上传二进制构建输出的功能。然而,如果直接在项目仓库中为每次 commit 添加 Release(和相应的 tag)则未免过于杂乱,因此我选择了新建 一个只有 README 的仓库,并将所有持续集成版本的 Release 创建在此仓库中。

为了实现此功能,我选择了通过 curl 调用 GitHub API 的方式来完成。经过查阅文档和大量的调试,我的脚本最终是如下编写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#!/bin/bash

set -e

repo="$1"
shift
echo "Repo: ${repo}" >&2

version="$1"
shift
echo "Version: ${version}" >&2

tag="v${version}"
echo "Tag: ${tag}" >&2

body="$1"
shift
echo "Body: ${body}" >&2

# Get old release by tag
echo "Getting old release by tag..." >&2
response="$(curl -H"Authorization: token ${GITHUB_ACCESS_TOKEN}""https://api.github.com/repos/${repo}/releases/tags/${tag}")"
echo "${response}" >&2
old_release_id="$(echo "${response}" | jq -r '.id')"

if [["${old_release_id}" != "null" ]]; then

# Delete old release
echo "Deleting old release..." >&2
response="$(curl -X 'DELETE' -H "Authorization: token ${GITHUB_ACCESS_TOKEN}" "https://api.github.com/repos/${repo}/releases/${old_release_id}")"
echo "${response}" >&2
fi

# Create release
echo "Creating release..." >&2
response="$(curl -H "Authorization: token ${GITHUB_ACCESS_TOKEN}" -H 'Content-Type: application/json' --data "{ "tag_name": $(echo -n"${tag}"| jq -s -R -r @json), "name": $(echo -n"${tag}"| jq -s -R -r @json), "body": $(echo -n"${body}"| jq -s -R -r @json) }" "https://api.github.com/repos/${repo}/releases")"
echo"${response}">&2
upload_url="$(echo "${response}" | jq -r '.upload_url' | sed 's/{?name,label}$//g')"
echo"Upload url: ${upload_url}">&2

for file in"$@"; do
# Upload file
echo"Uploading file: ${file}">&2
name="$(basename "${file}")"
response="$(curl -H "Authorization: token ${GITHUB_ACCESS_TOKEN}" -H "Content-Type: $(file -b --mime-type"${file}")" --data-binary "@${file}" "${upload_url}?name=$(echo -n"${name}"| jq -s -R -r @uri)")"
echo"${response}">&2
done

而设置 GITHUB_ACCESS_TOKEN 环境变量后的用法则如:

1
./upload-to-releases.sh 'DreaminginCodeZH/DouyaCiBuilds' "${version}" "${description}" "douya-ci-${version}.apk"

其他

如果无法确定错误原因,就多加些 echo 或者 cat 吧。

如果希望在 Lint 失败时查看输出,可以在 after_failure 中加入 cat app/build/outputs/lint-results-*.html

我所采用的配置都可以在 豆芽 中找到。

结语

为 Android 项目使用 Travis CI 的过程还算简单,但是也有一些微妙的问题需要解决,这令我花费了不少时间。而将每次的构建输出上传至另一个仓库的 Release 则是我考虑了一段时间后得出的方案,之前没见到过这种方式,用 curl 调用 GitHub API 也是第一次,同时再次感受到了 bash 的得心应手,总体上是一次十分有趣的体验。

因此,将我的配置过程和结果记录在此,希望对其他开发者有所帮助。

这章教程承接第三章结束的地方, 我们将继续那个网络民调应用, 并且将关注与简单的表单处理与精简我们的代码。

创造一个简单的表单

让我们来升级一下上个教程中创建的问题详细内容模板(polls/detail.html), 让它包含一个HTML的<form>元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--polls/templates/polls/detail.html-->
<h1>{ { question.question_text } }</h1>

{ % if error_message % }<p><strong>{ { error_message } }</strong></p>{ % endif % }

<form action="{ % url 'polls:vote' question.id % }" method="post">
{ % csrf_token % }
{ % for choice in question.choice_set.all % }
<input type="radio" name="choice" id="choice{ { forloop.counter } }" value="{ { choice.id } }" />
<label for="choice{ { forloop.counter } }">{ { choice.choice_text } }</label><br />
{ % endfor % }
<input type="submit" value="Vote" />
</form>

快速浏览一下上面的代码:

  • 上面的这个模板给每一个选项显示了一个单选按钮。 每一个单选按钮的值与每一个选项的id相联系。 每一个单选按钮的名字都是choice. 这意味着, 当某人选择了这些单选按钮之一然后提交这个页面的时候, 系统会使用POST传递一个数据choice=#, 此处的#,代表选中选项的id。 这是HTML表单的基本概念。

  • 我们把这个表单的动作属性设置为{ %url 'polls:vote' question.id% }, 然后我们设置动作方法为post。 此处使用method='post'(与之相对的是method='get')十分重要, 因为此处提交表单的这个动作将会出发服务器端的数据操作。 只要当你创建一个需要服务器端数据操作的表单的时候, 使用post总是最佳选择。 这个建议并不仅仅真对于Django, 这是Web开发的实际经验。

  • forloop.counter记录了for标签内循环的次数。

  • 既然我们创建了一个POST表单(拥有着操作数据的功能),我们就需要担心一下跨站请求伪造(Cross Site Request Forgeries)的问题。 但是谢天谢地,你并不需要太过于担心这个, 因为Django创造了一个非常易用的系统来避免这个问题。 简而言之, 所有使用POST提交,并且目标是一个内链URL的表单, 都需要有一个{ % CSRF token % }标签。

现在, 让我们来创建一个能够处理提交的数据的Django视图吧。 别忘了, 在第三章教程中, 我们为这个民调应用创建了一个URL设置, 包含着如下代码:

url(r'^(?P<question_id>d+)/vote/$', views.vote, name='vote')

我们之前也为vote视图创造了一个简单版本, 现在让我们来写一个完整版的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#polls/views.py

from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect, HttpResponse
from django.core.urlresolvers import reverse

form polls.models import Choice, Question
#...
def vote(request, question_id):
p = get_object_or_404(Question, pk=question_id)
try:
selected_choice = p.choice_set.get(pk = request.POST['choice'])
except (KeyError, Choice.DoesNotExist):
#重新显示问题投票表单
return render(request, 'polls/detail.html', {
'question': p,
'error_message': "You didn't select a choice.",
})
else:
selected_choice.votes += 1
selected_choice.save()
#当成功提交一个POST数据之后, 要记得返回一个
#HttpResponseRedirect, 不然当用户点击后退
#按钮的时候, 同样的数据会被提交多次。
return HttpResponseRedirect(reverse('polls:results', args=(p.id, )))

以上的代码包含一些我们之前没有提过的东西:

  • request.POST是一个字典型对象。 它允许你通过键值对来访问POST传递的数据。 在上文的代码中, request.POST['choice']以字符串的形式,返回了选中的选项的ID。 request.POST的值总是以字符串表示的。

    Django同样提供request.GET来以同样的方式访问GET数据。 在我们的代码中,我们只使用request.POST, 来确保数据只由POST请求来操作。

  • choice没有提供一个POST数据的话,request.POST['choice']会引发一个KeyError错误。 上面的代码检查了KeyError错误发生的可能性, 并在choice没有提供POST数据的时候重新显示表单页面,并且返回一个错误信息。
  • 在添加了投票统计之后, 代码返回了一个HttpResponseRedirect而非一个普通的HttpResponseHttpResponseRedirect只需要一个参数: 被重定向的目标URL。

    正如代码中的注释提到的那样, 你应当总是返回一个HttpResponseRedirect, 这不仅仅是对于Django而言。 这是一个优秀的编程实践经验。

  • 我们在本例的HttpResponseRedirect构造器中使用了一个reverse()函数。 这个函数帮助我们避免了在视图函数中硬编码写入一个URL。 给定我们想要传递到的那个视图的名字与这个URL模式所需的参数,在这个例子中, 使用我们在第三章教程中创建的URL设置, 这个reverse()函数会返回如下字符串:

    '/polls/3/results/'
    

    这里的3是p.id的值。 这个重定向的URL会调用result视图来显示最终的页面。

正如我们在第三章教程中提到的那样, request是一个HttpRequest对象。

在某人为某个问题投票之后, vote()视图重定向到这个问题的最终视图中, 它的视图代码如下:

1
2
3
4
5
6
from django.shortcuts import get_object_or_404, render


def results(request, question_id):
question = get_object_or_404(Question, pk=question_id)
return render(request, 'polls/results.html',{'question':question})

它与我们在第三章写的detail视图几乎完全一样, 仅有的不同之处在于模板的名字。 我们将会在稍后修改这处冗余。

现在, 创建一个polls/results.html模板

1
2
3
4
5
6
7
8
9
10
<!--polls/templates/polls/results.html-->
<h1>{ { question.question_text } }</h1>

<ul>
{ % for choice in question.choice_set.all % }
<li>{ { choice.choice_text } } -- { { choice.votes } } vote{ { choice.votes|pluralize } }</li>
{ % endfor % }
</ul>

<a href="{ % url 'polls:detail' question.id % }">Vote again?</a>

现在在你的浏览器中访问/polls/1来为这个问题投票。 你将见到一个随着你每次投票都有变化的结果页面。 如果你没有选择一个选项就提交了的话, 你还将见到一个错误页面。

使用基础视图: 代码越少越好

之前的detail()result()视图都极其简单, 以及, 像之前提过的那样—— 冗赘。 而那个显示着问题列表的index()视图, 也是一样。

这些视图体现着网页开发的一个基本模式:根据URL中传递的一个参数来从数据库中获取数据, 读取一个模板然后返回渲染之后的页面。 因为这实在是太平常了, Django提供了一个快捷表示, 叫做基础视图(generic views)(或者叫泛型视图).

基础视图将程序抽象表示, 以至于你甚至不需要书写Python代码就能够创建一个应用。

然我们把民调应用转化成使用基础视图系统, 所以我们就可以删去一大堆无用的代码。 我们只需做一下这几步就可以完成转化:

  • 改变URL设置
  • 删除一些旧的,不需要的视图
  • 引入基于基础视图系统的新的视图

修改URL设置

首先, 打开polls/urls.py, 并作如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
#polls/urls.py

from django.conf.urls import patterns, url

from polls import views

urlpatterns = patterns('',
url(r'^$',views.IndexView.as_view(), name ='index'),
url(r'^(?P<pk>d+)/$', views.DetailView.as_view(), name='detail'),
url(r'^(?P<pk>d+)/results/$', views.ResultsView.as_view(), name='results'),
url(r'^(?P<question_id>d+)/vote/$', views.vote, name='vote'),
)

注意第二个和第三个正则表达式匹配参数名已经从<question_id>变成了<pk>

修改视图

接下来, 我们要对旧的index, detailresult视图做一些修改, 并且使用之前提到的Django的基础视图。 打开polls/views.py, 并作如下修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#polls/views.py

from django.shortcuts import get_object_or_404, render
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.views import generic

from polls.models import Choice, Question

class IndexView(generic.ListView):
template_name = 'polls/index.html'
context_object_name = 'latest_question_list'

def get_queryset(self):
return Question.objects.order_by('-pub_date')[:5]

class DetailView(generic.DetailView):
model = Question
template_name = 'polls/detail.html'

class ResultsView(generic.DetailView):
model = Question
template_name = 'polls/result.html'

def vote(request, question_id):
...#与之前相同

我们使用了两个基础视图: ListViewDetailView。 这两个视图分别抽象了“显示一列对象”和“显示某个特定对象的详细内容”两个概念。

  • 每一个抽象视图都需要直到它所作用的数据模型。 这个信息由model属性传递
  • DetailView基础视图需要由URL中匹配得到被称为pk的主键键值, 所以我们在URL设置中把question_id改名作pk

默认情况下, DetailView基础视图使用叫做<app name>/<model name>_detail.html的模板。 在本例中, 它使用的是polls/question_detail.html。 这里的template_name属性被用来告诉Django去使用一个特定的模板名称, 而非Django自动默认的模板。 我们还为result视图指定了一个模板名称 – 这确保了result视图和detail视图在渲染时会有不同的显示, 尽管他们都是基于DetailView

同样的, ListView使用默认的<app name>/<model name>_list.html 我们使用template_name来告诉ListView使用我们存在的polls/index.html模板

在之前的教程中, 我们将一个包含着questionlatest_question_list的内容对象传递到模板中。 对于DetailView来说, 这些信息都是自动传递的。 因为我们使用了一个Django的视图。 Django能够自己为这个内容决定一个合适的名字。 但是, 对于ListView,自动生成的内容变量是question_list。 为了覆盖这个, 我们提供了context_object_name属性, 特殊指定了我们想要替代性地使用latest_question_list。 作为一个可选的方法, 你应该更改你的模板来匹配新的默认内容变量 – 但是让Django使用你想要的变量会更简单。

运行服务器,你就会看见基于基础视图的全新的民意调查应用。