├── .gitignore ├── LICENSE ├── Makefile ├── build.zig ├── build.zig.zon ├── changelog.md ├── config.json.example ├── control.deb ├── docs ├── _config.yml ├── img │ ├── zoot-shot1.png │ └── zootlogo.png └── index.html ├── img ├── gears.svg ├── logo.vox ├── reload-icon.svg ├── zootdeck.svg └── zootlogo.png ├── ragel ├── lang.c.rl └── lang.h ├── readme.md └── src ├── config.zig ├── db ├── file.zig └── lmdb.zig ├── filter.zig ├── gui.zig ├── gui ├── column.glade ├── gtk3.zig ├── libui.zig.disable ├── qt.zig ├── theme.css ├── toot.glade └── zootdeck.glade ├── heartbeat.zig ├── html.zig ├── ipc ├── epoll.zig └── nng.zig ├── main.zig ├── net.zig ├── oauth.zig ├── simple_buffer.zig ├── statemachine.zig ├── tests.zig ├── thread.zig ├── toot.zig ├── toot_list.zig └── util.zig /.gitignore: -------------------------------------------------------------------------------- 1 | /tmp 2 | /config.json 3 | /.gdb_history 4 | /db/ 5 | /.zig-cache/ 6 | /zig-out/ 7 | /cache/ 8 | /ragel/*.c 9 | /ragel/*.o 10 | /*.deb 11 | .zigmod 12 | deps.zig 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Don Park 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | 11 | https://www.tldrlegal.com/l/mit 12 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GITEPOCH=$(shell git log -1 --format="%at") 2 | TODAY=$(shell date +%Y.%m.%d -d @${GITEPOCH}) 3 | DIST=zootdeck-linux-`uname -i`-${TODAY} 4 | DISTDEB=zootdeck_0.6.1-1 5 | ZIG=zig 6 | 7 | build: 8 | ${ZIG} build -freference-trace 9 | 10 | format: 11 | ${ZIG} fmt src 12 | 13 | run: build 14 | ./zig-out/bin/zootdeck 15 | 16 | push: 17 | pijul push donpdonp@nest.pijul.com:donpdonp/tootdeck 18 | 19 | test: 20 | zig build test 21 | 22 | test-each: 23 | find src -name \*zig -print -exec zig test {} \; 24 | 25 | dist: 26 | mkdir ${DIST} 27 | cp -r ./zig-out/bin/zootdeck img glade ${DIST}/ 28 | cp config.json.example ${DIST}/config.json 29 | tar czf ${DIST}.tar.gz ${DIST} 30 | ls -l ${DIST} 31 | file ${DIST}.tar.gz 32 | 33 | deb: 34 | mkdir -p ${DISTDEB}/opt/ 35 | cp -r img glade ${DISTDEB}/opt/ 36 | mkdir -p ${DISTDEB}/usr/bin 37 | cp -r zig-out/bin/zootdeck ${DISTDEB}/usr/bin/ 38 | mkdir -p ${DISTDEB}/DEBIAN 39 | cp control.deb ${DISTDEB}/DEBIAN/control 40 | ls -lR ${DISTDEB} 41 | dpkg-deb --build ${DISTDEB} 42 | mv ${DISTDEB}.deb ${DISTDEB}-amd64.deb 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | // Although this function looks imperative, note that its job is to 4 | // declaratively construct a build graph that will be executed by an external 5 | // runner. 6 | pub fn build(b: *std.Build) void { 7 | // Standard target options allows the person running `zig build` to choose 8 | // what target to build for. Here we do not override the defaults, which 9 | // means any target is allowed, and the default is native. Other options 10 | // for restricting supported target set are available. 11 | const target = b.standardTargetOptions(.{}); 12 | 13 | // Standard optimization options allow the person running `zig build` to select 14 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not 15 | // set a preferred release mode, allowing the user to decide how to optimize. 16 | const optimize = b.standardOptimizeOption(.{}); 17 | 18 | // This declares intent for the library to be installed into the standard 19 | // location when the user invokes the "install" step (the default step when 20 | // running `zig build`). 21 | // b.installArtifact(lib); 22 | 23 | const exe = b.addExecutable(.{ 24 | .name = "zootdeck", 25 | .root_source_file = b.path("src/main.zig"), 26 | .target = target, 27 | .optimize = optimize, 28 | }); 29 | 30 | enhance_executable(b, exe); 31 | 32 | // This declares intent for the executable to be installed into the 33 | // standard location when the user invokes the "install" step (the default 34 | // step when running `zig build`). 35 | b.installArtifact(exe); 36 | 37 | // This *creates* a Run step in the build graph, to be executed when another 38 | // step is evaluated that depends on it. The next line below will establish 39 | // such a dependency. 40 | const run_cmd = b.addRunArtifact(exe); 41 | 42 | // By making the run step depend on the install step, it will be run from the 43 | // installation directory rather than directly from within the cache directory. 44 | // This is not necessary, however, if the application depends on other installed 45 | // files, this ensures they will be present and in the expected location. 46 | run_cmd.step.dependOn(b.getInstallStep()); 47 | 48 | // This allows the user to pass arguments to the application in the build 49 | // command itself, like this: `zig build run -- arg1 arg2 etc` 50 | if (b.args) |args| { 51 | run_cmd.addArgs(args); 52 | } 53 | 54 | // This creates a build step. It will be visible in the `zig build --help` menu, 55 | // and can be selected like this: `zig build run` 56 | // This will evaluate the `run` step rather than the default, which is "install". 57 | const run_step = b.step("run", "Run the app"); 58 | run_step.dependOn(&run_cmd.step); 59 | 60 | const test_step = b.step("test", "Run unit tests"); 61 | const unit_test = b.addTest(.{ .root_source_file = b.path("src/tests.zig") }); 62 | enhance_executable(b, unit_test); 63 | const run_unit_tests = b.addRunArtifact(unit_test); 64 | test_step.dependOn(&run_unit_tests.step); 65 | } 66 | 67 | fn enhance_executable(b: *std.Build, exe: *std.Build.Step.Compile) void { 68 | exe.linkLibC(); 69 | exe.addIncludePath(b.path(".")); 70 | exe.addIncludePath(.{ .cwd_relative = "/usr/include/gtk-3.0" }); 71 | exe.addIncludePath(.{ .cwd_relative = "/usr/include/glib-2.0" }); 72 | exe.addIncludePath(.{ .cwd_relative = "/usr/include/pango-1.0" }); 73 | exe.addIncludePath(.{ .cwd_relative = "/usr/include/gdk-pixbuf-2.0" }); 74 | exe.addIncludePath(.{ .cwd_relative = "/usr/include/atk-1.0" }); 75 | exe.addIncludePath(.{ .cwd_relative = "/usr/include/harfbuzz" }); 76 | exe.addIncludePath(.{ .cwd_relative = "/usr/include/cairo" }); 77 | exe.addIncludePath(.{ .cwd_relative = "/usr/lib/x86_64-linux-gnu/glib-2.0/include" }); 78 | exe.linkSystemLibrary("gtk-3"); 79 | exe.linkSystemLibrary("gdk-3"); 80 | exe.linkSystemLibrary("curl"); 81 | exe.linkSystemLibrary("lmdb"); 82 | exe.linkSystemLibrary("gumbo"); 83 | 84 | const gen_ragel = b.addSystemCommand(&.{ "ragel", "-o", "ragel/lang.c", "ragel/lang.c.rl" }); 85 | exe.addCSourceFile(.{ .file = b.path("ragel/lang.c") }); 86 | exe.step.dependOn(&gen_ragel.step); 87 | } 88 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .zootdeck, 3 | .fingerprint = 0xbe547c96eb134a93, 4 | // This is a [Semantic Version](https://semver.org/). 5 | // In a future version of Zig it will be used for package deduplication. 6 | .version = "0.1.0", 7 | 8 | // This field is optional. 9 | // This is currently advisory only; Zig does not yet do anything 10 | // with this value. 11 | .minimum_zig_version = "0.13.0", 12 | 13 | // This field is optional. 14 | // Each dependency must either provide a `url` and `hash`, or a `path`. 15 | // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. 16 | // Once all dependencies are fetched, `zig build` no longer requires 17 | // internet connectivity. 18 | .dependencies = .{ 19 | // See `zig fetch --save ` for a command-line interface for adding dependencies. 20 | //.example = .{ 21 | // // When updating this field to a new URL, be sure to delete the corresponding 22 | // // `hash`, otherwise you are communicating that you expect to find the old hash at 23 | // // the new URL. 24 | // .url = "https://example.com/foo.tar.gz", 25 | // 26 | // // This is computed from the file contents of the directory of files that is 27 | // // obtained after fetching `url` and applying the inclusion rules given by 28 | // // `paths`. 29 | // // 30 | // // This field is the source of truth; packages do not come from a `url`; they 31 | // // come from a `hash`. `url` is just one of many possible mirrors for how to 32 | // // obtain a package matching this `hash`. 33 | // // 34 | // // Uses the [multihash](https://multiformats.io/multihash/) format. 35 | // .hash = "...", 36 | // 37 | // // When this is provided, the package is found in a directory relative to the 38 | // // build root. In this case the package's hash is irrelevant and therefore not 39 | // // computed. This field and `url` are mutually exclusive. 40 | // .path = "foo", 41 | //}, 42 | }, 43 | 44 | // Specifies the set of files and directories that are included in this package. 45 | // Only files and directories listed here are included in the `hash` that 46 | // is computed for this package. 47 | // Paths are relative to the build root. Use the empty string (`""`) to refer to 48 | // the build root itself. 49 | // A directory listed here means that all files within, recursively, are included. 50 | .paths = .{ 51 | // This makes *all* files, recursively, included in this package. It is generally 52 | // better to explicitly list the files and directories instead, to insure that 53 | // fetching from tarballs, file system paths, and version control all result 54 | // in the same contents hash. 55 | "", 56 | // For example... 57 | //"build.zig", 58 | //"build.zig.zon", 59 | //"src", 60 | //"LICENSE", 61 | //"README.md", 62 | }, 63 | } 64 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ### Change Log 2 | * 20250304 (0.6.1) 3 | * fix sort order 4 | * better use of cache 5 | * 20250301 (0.6.0) 6 | * rework for zig 0.3 7 | * 20191010 ragel filter language parsing 8 | * 20190710 image only filter 9 | * 20190706 profile image loading 10 | * 20190613 oauth steps in config panel 11 | * 20190604 network status indicator for each column 12 | * better json escape handling 13 | * append api path automatically 14 | * 20190529 First release 15 | 16 | -------------------------------------------------------------------------------- /config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | "win_x" : 800, 3 | "win_y" : 600, 4 | "columns" : [ 5 | { 6 | "title" : "", 7 | "filter" : "radical.town", 8 | "token" : null, 9 | "img_only" : false}, 10 | { 11 | "title" : "", 12 | "filter" : "mastodon.art", 13 | "token" : null, 14 | "img_only" : false}] 15 | } 16 | -------------------------------------------------------------------------------- /control.deb: -------------------------------------------------------------------------------- 1 | Package: zootdeck 2 | Version: 0.6.0-1 3 | Section: base 4 | Priority: optional 5 | Architecture: amd64 6 | Depends: libgumbo2 (>= 0.12.0), liblmdb0 (>=0.9.31), libcurl3t64-gnutls | libcurl4t64, libgtk-3-0t64 7 | Maintainer: Don Park 8 | Description: Linux Desktop Reader for the Fediverse 9 | A linux/gtk3 app to read the newsfeeds from multiple mastodon servers. 10 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-modernist -------------------------------------------------------------------------------- /docs/img/zoot-shot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donpdonp/zootdeck/17db6ccabb76ff012d0f5fa7beef29cca2544c15/docs/img/zoot-shot1.png -------------------------------------------------------------------------------- /docs/img/zootlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donpdonp/zootdeck/17db6ccabb76ff012d0f5fa7beef29cca2544c15/docs/img/zootlogo.png -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 |
15 |
16 | 17 | zootdeck 18 |
19 |
20 | The linux desktop fediverse reader. 21 |
22 |
23 | 24 |
25 |
  • Visit public timeline of any mastodon server quickly and easily 26 |
  • Filter posts by tag 27 |
  • Image-only mode 28 |
  • Open Source, linux native, GTK3 29 |
  • 30 | 31 | 34 | 35 |
    36 | 37 |
    38 | 39 | 40 | -------------------------------------------------------------------------------- /img/gears.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 28 | 48 | 56 | 61 | 66 | 67 | -------------------------------------------------------------------------------- /img/logo.vox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donpdonp/zootdeck/17db6ccabb76ff012d0f5fa7beef29cca2544c15/img/logo.vox -------------------------------------------------------------------------------- /img/reload-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 55 | 56 | 61 | 66 | 72 | 73 | 75 | 76 | 77 | image/svg+xml 78 | 80 | 82 | 83 | 85 | Openclipart 86 | 87 | 88 | Reload icon 89 | 2012-07-06T14:30:15 90 | simple reload / recycle icon 91 | https://openclipart.org/detail/171074/reload-icon-by-mlampret-171074 92 | 93 | 94 | mlampret 95 | 96 | 97 | 98 | 99 | black 100 | free 101 | icon 102 | icons 103 | recycle 104 | reload 105 | simple 106 | 107 | 108 | 109 | 111 | 113 | 115 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /img/zootdeck.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 56 | 62 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /img/zootlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/donpdonp/zootdeck/17db6ccabb76ff012d0f5fa7beef29cca2544c15/img/zootlogo.png -------------------------------------------------------------------------------- /ragel/lang.c.rl: -------------------------------------------------------------------------------- 1 | #include 2 | #include "lang.h" 3 | 4 | %%{ 5 | machine uri; 6 | 7 | action scheme { urlp->scheme_pos = p-uri; } 8 | action loc { urlp->loc_pos = p-uri; } 9 | action item { } 10 | action query { } 11 | action last { } 12 | action nothing { } 13 | 14 | main := 15 | # Scheme machine. This is ambiguous with the item machine. We commit 16 | # to the scheme machine on colon. 17 | ( [^:/?#]+ ':' @(colon,1) @scheme )? 18 | 19 | # Location machine. This is ambiguous with the item machine. We remain 20 | # ambiguous until a second slash, at that point and all points after 21 | # we place a higher priority on staying in the location machine over 22 | # moving into the item machine. 23 | ( ( '/' ( '/' [^/?#]* ) $(loc,1) ) %loc %/loc )? 24 | 25 | # Item machine. Ambiguous with both scheme and location, which both 26 | # get a higher priority on the characters causing ambiguity. 27 | ( ( [^?#]+ ) $(loc,0) $(colon,0) %item %/item )? 28 | 29 | # Last two components, the characters that initiate these machines are 30 | # not supported in any previous components, therefore there are no 31 | # ambiguities introduced by these parts. 32 | ( '?' [^#]* %query %/query)? 33 | ( '#' any* %/last )?; 34 | }%% 35 | 36 | %% write data; 37 | 38 | struct urlPoints* url(char* uri, struct urlPoints* urlp) { 39 | char *p = uri, *pe = uri + strlen( uri ); 40 | char* eof = pe; 41 | int cs; 42 | 43 | %% write init; 44 | %% write exec; 45 | 46 | return urlp; 47 | } 48 | 49 | /* 50 | int main(int argc, char**argv) 51 | { 52 | char* arg = argv[1]; 53 | struct urlPoints it; 54 | 55 | printf("parsing %s\n", arg); 56 | url(arg, &it); 57 | 58 | char scratch[512]; 59 | strncpy(scratch, arg, it.scheme_pos); 60 | printf("scheme %s\n", scratch); 61 | strncpy(scratch, arg+it.scheme_pos+1, it.loc_pos - it.scheme_pos); 62 | printf("scheme %s\n", scratch); 63 | } 64 | */ 65 | -------------------------------------------------------------------------------- /ragel/lang.h: -------------------------------------------------------------------------------- 1 | struct urlPoints { 2 | int scheme_pos; 3 | int loc_pos; 4 | }; 5 | 6 | struct urlPoints* url(char* uri, struct urlPoints* urlp); 7 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Zootdeck fediverse desktop reader 2 | https://donpdonp.github.io/zootdeck/ 3 | 4 | ### Features 5 | * Any number of columns 6 | * Column Sources: Mastodon account, Mastodon public feed 7 | * per-column filtering on specific tags, or image-only mode 8 | * native linux/GTK+3 app written in zig 9 | 10 | ## Column specifiers 11 | * `@mastodon.server` 12 | * `@username@mastodon.server` 13 | * Public feed 14 | * Option to do oauth sign-in to read your own feed 15 | * `!newsgroup@lemmy.server` 16 | 17 | 18 | ### Roadmap 19 | * initial QT support 20 | * initial lemmy support 21 | * create a post 22 | * css themes: overall, per-tag, per-host 23 | * osx/windows 24 | 25 | ### Build instructions 26 | ``` 27 | $ sudo apt install ragel libgtk-3-dev libcurl4-openssl-dev libgumbo-dev 28 | 29 | $ git clone https://github.com/donpdonp/zootdeck 30 | Cloning into 'zootdeck'... 31 | 32 | $ cd zootdeck 33 | 34 | $ make 35 | zig build 36 | 37 | $ ./zig-out/bin/zootdeck 38 | zootdeck linux x86_64 tid 7f565d1caf00 39 | ``` 40 | `zig-out/bin/zootdeck` is a stand-alone binary that can be copied to `/usr/local/bin/` or where ever you like. 41 | 42 | -------------------------------------------------------------------------------- /src/config.zig: -------------------------------------------------------------------------------- 1 | // config.zig 2 | const std = @import("std"); 3 | const warn = util.log; 4 | const Allocator = std.mem.Allocator; 5 | const util = @import("./util.zig"); 6 | const filter_lib = @import("./filter.zig"); 7 | const toot_lib = @import("./toot.zig"); 8 | const toot_list = @import("./toot_list.zig"); 9 | var allocator: Allocator = undefined; 10 | 11 | const c = @cImport({ 12 | @cInclude("time.h"); 13 | }); 14 | 15 | pub const Time = c.time_t; 16 | 17 | // contains runtime-only values 18 | pub const Settings = struct { 19 | win_x: i64, 20 | win_y: i64, 21 | columns: std.ArrayList(*ColumnInfo), 22 | config_path: []const u8, 23 | }; 24 | 25 | // on-disk config 26 | pub const ConfigFile = struct { 27 | win_x: i64, 28 | win_y: i64, 29 | columns: []*ColumnConfig, 30 | }; 31 | 32 | pub const ColumnInfo = struct { 33 | config: *ColumnConfig, 34 | filter: *filter_lib.ptree, 35 | toots: toot_list.TootList, 36 | refreshing: bool, 37 | last_check: Time, 38 | inError: bool, 39 | account: ?std.json.ObjectMap, 40 | oauthClientId: ?[]const u8, 41 | oauthClientSecret: ?[]const u8, 42 | 43 | const Self = @This(); 44 | 45 | pub fn reset(self: *Self) *Self { 46 | self.account = null; 47 | self.oauthClientId = null; 48 | self.oauthClientSecret = null; 49 | return self; 50 | } 51 | 52 | pub fn parseFilter(self: *const Self, filter: []const u8) void { 53 | self.filter = filter_lib.parse(filter); 54 | } 55 | 56 | pub fn makeTitle(column: *ColumnInfo) []const u8 { 57 | var out: []const u8 = column.filter.host(); 58 | if (column.config.token) |_| { 59 | var addon: []const u8 = undefined; 60 | if (column.account) |account| { 61 | addon = account.get("acct").?.string; 62 | } else { 63 | addon = "_"; 64 | } 65 | out = std.fmt.allocPrint(allocator, "{s}@{s}", .{ addon, column.filter.host() }) catch unreachable; 66 | } 67 | return out; 68 | } 69 | }; 70 | 71 | pub const ColumnConfig = struct { 72 | title: []const u8, 73 | filter: []const u8, 74 | token: ?[]const u8, 75 | img_only: bool, 76 | 77 | const Self = @This(); 78 | 79 | pub fn reset(self: *Self) *Self { 80 | self.title = ""; 81 | self.filter = "mastodon.example.com"; 82 | self.token = null; 83 | return self; 84 | } 85 | }; 86 | 87 | pub const LoginInfo = struct { 88 | username: []const u8, 89 | password: []const u8, 90 | }; 91 | 92 | pub const HttpInfo = struct { 93 | url: []const u8, 94 | verb: enum { 95 | get, 96 | post, 97 | }, 98 | token: ?[]const u8, 99 | post_body: []const u8, 100 | body: []const u8, 101 | media_id: ?[]const u8, 102 | content_type: []const u8, 103 | response_code: c_long, 104 | tree: std.json.Parsed(std.json.Value), 105 | column: *ColumnInfo, 106 | toot: *toot_lib.Type, 107 | 108 | pub fn response_ok(self: *HttpInfo) bool { 109 | return self.response_code >= 200 and self.response_code < 300; 110 | } 111 | 112 | pub fn content_type_json(self: *HttpInfo) bool { 113 | return std.mem.eql(u8, self.content_type, "application/json; charset=utf-8"); 114 | } 115 | }; 116 | 117 | pub const ColumnAuth = struct { 118 | code: []const u8, 119 | column: *ColumnInfo, 120 | }; 121 | 122 | const ConfigError = error{MissingParams}; 123 | 124 | pub fn init(alloc: Allocator) !void { 125 | allocator = alloc; 126 | } 127 | 128 | pub fn config_file_path() []const u8 { 129 | const home_path = std.posix.getenv("HOME").?; 130 | const home_dir = std.fs.openDirAbsolute(home_path, .{}) catch unreachable; 131 | const config_dir_path = std.fs.path.join(allocator, &.{ home_path, ".config", "zootdeck" }) catch unreachable; 132 | home_dir.makePath(config_dir_path) catch unreachable; // try every time 133 | const config_dir = std.fs.openDirAbsolute(config_dir_path, .{}) catch unreachable; 134 | const filename = "config.json"; 135 | const config_path = std.fs.path.join(allocator, &.{ config_dir_path, filename }) catch unreachable; 136 | config_dir.access(filename, .{}) catch |err| switch (err) { 137 | error.FileNotFound => { 138 | warn("Warning: creating new {s}", .{config_path}); 139 | config_dir.writeFile(.{ .sub_path = filename, .data = "{}\n" }) catch unreachable; 140 | }, 141 | else => { 142 | warn("readfile err {any}", .{err}); 143 | unreachable; 144 | }, 145 | }; 146 | return config_path; 147 | } 148 | 149 | pub fn readfile(filename: []const u8) !*Settings { 150 | util.log("config file {s}", .{filename}); 151 | const json = try std.fs.cwd().readFileAlloc(allocator, filename, std.math.maxInt(usize)); 152 | var settings = try read(json); 153 | settings.config_path = filename; 154 | return settings; 155 | } 156 | 157 | pub fn read(json: []const u8) !*Settings { 158 | const value_tree = try std.json.parseFromSlice(std.json.Value, allocator, json, .{}); 159 | var root = value_tree.value.object; 160 | var settings = allocator.create(Settings) catch unreachable; 161 | settings.columns = std.ArrayList(*ColumnInfo).init(allocator); 162 | 163 | if (root.get("win_x")) |w| { 164 | settings.win_x = w.integer; 165 | } else { 166 | settings.win_x = 800; 167 | } 168 | if (root.get("win_y")) |h| { 169 | settings.win_y = h.integer; 170 | } else { 171 | settings.win_y = 600; 172 | } 173 | if (root.get("columns")) |columns| { 174 | for (columns.array.items) |value| { 175 | var colInfo = allocator.create(ColumnInfo) catch unreachable; 176 | _ = colInfo.reset(); 177 | colInfo.toots = toot_list.TootList.init(); 178 | const colconfig = allocator.create(ColumnConfig) catch unreachable; 179 | colInfo.config = colconfig; 180 | const title = value.object.get("title").?.string; 181 | colInfo.config.title = title; 182 | const filter = value.object.get("filter").?.string; 183 | colInfo.config.filter = filter; 184 | colInfo.filter = filter_lib.parse(allocator, filter); 185 | const tokenTag = value.object.get("token"); 186 | if (tokenTag) |tokenKV| { 187 | if (@TypeOf(tokenKV) == []const u8) { 188 | colInfo.config.token = tokenKV.value.string; 189 | } else { 190 | colInfo.config.token = null; 191 | } 192 | } else { 193 | colInfo.config.token = null; 194 | } 195 | const img_only = value.object.get("img_only").?.bool; 196 | colInfo.config.img_only = img_only; 197 | settings.columns.append(colInfo) catch unreachable; 198 | } 199 | } 200 | return settings; 201 | } 202 | 203 | pub fn writefile(settings: *Settings, filename: []const u8) void { 204 | var configFile = allocator.create(ConfigFile) catch unreachable; 205 | configFile.win_x = settings.win_x; 206 | configFile.win_y = settings.win_y; 207 | var column_infos = std.ArrayList(*ColumnConfig).init(allocator); 208 | for (settings.columns.items) |column| { 209 | warn("config.writefile writing column {s}", .{column.config.title}); 210 | column_infos.append(column.config) catch unreachable; 211 | } 212 | configFile.columns = column_infos.items; 213 | 214 | if (std.fs.cwd().createFile(filename, .{ .truncate = true })) |*file| { 215 | std.json.stringify(configFile, std.json.StringifyOptions{}, file.writer()) catch unreachable; 216 | warn("config saved. {s} {} bytes", .{ filename, file.getPos() catch unreachable }); 217 | file.close(); 218 | } else |err| { 219 | warn("config save fail. {!}", .{err}); 220 | } // existing file is OK 221 | } 222 | 223 | pub fn now() Time { 224 | var t: Time = undefined; 225 | _ = c.time(&t); 226 | return t; 227 | } 228 | 229 | test "read" { 230 | try init(std.testing.allocator); 231 | const ret = read("{\"url\":\"abc\"}"); 232 | if (ret) |settings| { 233 | try std.testing.expectEqual(800, settings.win_x); 234 | std.testing.allocator.destroy(settings); 235 | } else |err| { 236 | warn("warn: {!}", .{err}); 237 | try std.testing.expect(false); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/db/file.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const util = @import("../util.zig"); 4 | const warn = util.log; 5 | const Allocator = std.mem.Allocator; 6 | 7 | const cache_dir_name = "cache"; 8 | var cache_path: []const u8 = undefined; 9 | 10 | pub fn init(alloc: Allocator) !void { 11 | const cwd = std.fs.cwd(); 12 | const cwd_path = std.fs.Dir.realpathAlloc(cwd, alloc, ".") catch unreachable; 13 | const parts: []const []const u8 = &[_][]const u8{ cwd_path, cache_dir_name }; 14 | cache_path = std.fs.path.join(alloc, parts) catch unreachable; 15 | std.posix.mkdir(cache_path, 0o755) catch |err| { 16 | if (err != error.PathAlreadyExists) return err; 17 | }; 18 | warn("cache dir {s}", .{cache_path}); 19 | } 20 | 21 | pub fn has(namespaces: []const []const u8, key: []const u8, allocator: Allocator) bool { 22 | var namespace_paths = std.ArrayList([]const u8).init(allocator); 23 | namespace_paths.append(cache_path) catch unreachable; 24 | namespace_paths.appendSlice(namespaces) catch unreachable; 25 | namespace_paths.append(key) catch unreachable; 26 | const keypath = std.fs.path.join(allocator, namespace_paths.items) catch unreachable; 27 | var found = false; 28 | if (std.fs.cwd().access(keypath, .{ .mode = .read_only })) { 29 | warn("db_file.has found {s}", .{keypath}); 30 | found = true; 31 | } else |err| { 32 | warn("db_file.has did not find {s} {!}", .{ keypath, err }); 33 | } 34 | return found; 35 | } 36 | 37 | pub fn read(namespaces: []const []const u8, allocator: Allocator) ![]const u8 { 38 | var namespace_paths = std.ArrayList([]const u8).init(allocator); 39 | namespace_paths.append(cache_path) catch unreachable; 40 | namespace_paths.appendSlice(namespaces) catch unreachable; 41 | const filename = std.fs.path.join(allocator, namespace_paths.items) catch unreachable; 42 | warn("db_file.read {s}", .{filename}); 43 | return try std.fs.cwd().readFileAlloc(allocator, filename, std.math.maxInt(usize)); 44 | } 45 | 46 | pub fn write(namespaces: []const []const u8, key: []const u8, value: []const u8, allocator: Allocator) ![]const u8 { 47 | const namespace = std.fs.path.join(allocator, namespaces) catch unreachable; 48 | const cache_dir = std.fs.openDirAbsolute(cache_path, .{ .access_sub_paths = true }) catch unreachable; 49 | std.fs.Dir.makePath(cache_dir, namespace) catch unreachable; 50 | 51 | var namespace_paths = std.ArrayList([]const u8).init(allocator); 52 | namespace_paths.append(cache_path) catch unreachable; 53 | namespace_paths.appendSlice(namespaces) catch unreachable; 54 | const dirpath = std.fs.path.join(allocator, namespace_paths.items) catch unreachable; 55 | var dir = std.fs.Dir.makeOpenPath(std.fs.cwd(), dirpath, .{}) catch unreachable; 56 | defer dir.close(); 57 | if (dir.createFile(key, .{ .truncate = true })) |*file| { 58 | _ = try file.write(value); 59 | file.close(); 60 | warn("db_file.wrote {s}/{s}", .{ dirpath, key }); 61 | const filename = std.fs.path.join(allocator, &.{ dirpath, key }) catch unreachable; 62 | return filename; 63 | } else |err| { 64 | warn("db_file.write( {s} {s} ) {any}", .{ dirpath, key, err }); 65 | return err; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/db/lmdb.zig: -------------------------------------------------------------------------------- 1 | // db.zig 2 | const std = @import("std"); 3 | const Allocator = std.mem.Allocator; 4 | const util = @import("../util.zig"); 5 | const warn = util.log; 6 | 7 | const c = @cImport({ 8 | @cInclude("lmdb.h"); 9 | }); 10 | 11 | var env: *c.MDB_env = undefined; 12 | const dbpath = "./db"; 13 | 14 | pub fn init(allocator: Allocator) !void { 15 | var mdb_ret: c_int = 0; 16 | mdb_ret = c.mdb_env_create(@as([*c]?*c.MDB_env, @ptrCast(&env))); 17 | if (mdb_ret != 0) { 18 | warn("mdb_env_create failed {}\n", .{mdb_ret}); 19 | return error.BadValue; 20 | } 21 | mdb_ret = c.mdb_env_set_mapsize(env, 250 * 1024 * 1024); 22 | if (mdb_ret != 0) { 23 | warn("mdb_env_set_mapsize failed {}\n", .{mdb_ret}); 24 | return error.BadValue; 25 | } 26 | std.posix.mkdir(dbpath, 0o0755) catch {}; 27 | mdb_ret = c.mdb_env_open(env, util.sliceToCstr(allocator, dbpath), 0, 0o644); 28 | if (mdb_ret != 0) { 29 | warn("mdb_env_open failed {}\n", .{mdb_ret}); 30 | return error.BadValue; 31 | } 32 | stats(); 33 | } 34 | 35 | pub fn stats() void { 36 | var mdbStat: c.MDB_stat = undefined; 37 | _ = c.mdb_env_stat(env, &mdbStat); 38 | util.log("lmdb cache {} entries", .{mdbStat.ms_entries}); 39 | } 40 | 41 | pub fn txn_open() !?*c.MDB_txn { 42 | var txn: ?*c.MDB_txn = undefined; 43 | const ret = c.mdb_txn_begin(env, null, 0, &txn); 44 | if (ret == 0) { 45 | return txn; 46 | } else { 47 | warn("mdb_txn_begin ERR {}", .{ret}); 48 | return error.mdb_txn_begin; 49 | } 50 | } 51 | 52 | pub fn txn_commit(txn: ?*c.MDB_txn) !void { 53 | const ret = c.mdb_txn_commit(txn); 54 | if (ret == 0) { 55 | return; 56 | } else { 57 | warn("mdb_txn_commit ERR {}", .{ret}); 58 | return error.mdb_txn_commit; 59 | } 60 | } 61 | 62 | pub fn dbi_open(txn: ?*c.struct_MDB_txn) !c.MDB_dbi { 63 | var dbi_ptr: c.MDB_dbi = 0; 64 | const ret = c.mdb_dbi_open(txn, null, c.MDB_CREATE, @ptrCast(&dbi_ptr)); 65 | if (ret == 0) { 66 | return dbi_ptr; 67 | } else { 68 | warn("mdb_dbi_open ERR {}", .{ret}); 69 | return error.mdb_dbi_open; 70 | } 71 | } 72 | 73 | pub fn csr_open(txn: ?*c.struct_MDB_txn, dbi: c.MDB_dbi) !?*c.MDB_cursor { 74 | var csr_ptr: ?*c.MDB_cursor = undefined; 75 | const ret = c.mdb_cursor_open(txn, dbi, &csr_ptr); 76 | if (ret == 0) { 77 | return csr_ptr; 78 | } else { 79 | warn("mdb_dbi_open ERR {}", .{ret}); 80 | return error.mdb_dbi_open; 81 | } 82 | } 83 | 84 | pub const Key = struct { 85 | parts: []const []const u8, 86 | 87 | const This = @This(); 88 | const Separator = ':'; 89 | 90 | pub fn init(parts: []const []const u8) This { 91 | return .{ .parts = parts }; 92 | } 93 | 94 | pub fn toString(self: *const This, allocator: Allocator) []const u8 { 95 | return util.strings_join_separator(self.parts, Separator, allocator); 96 | } 97 | 98 | pub fn lessLevel(self: *const This) Key { 99 | const less_level = self.parts[0 .. self.parts.len - 1]; 100 | return This.init(less_level); 101 | } 102 | }; 103 | 104 | pub fn scan(key: Key, descending: bool, allocator: Allocator) ![]const []const u8 { 105 | var answers = std.ArrayList([]const u8).init(allocator); 106 | const txn = try txn_open(); 107 | const dbi = try dbi_open(txn); 108 | const csr = try csr_open(txn, dbi); 109 | try scanInner(csr, &answers, key, descending, allocator); 110 | try txn_commit(txn); 111 | warn("scan returning {} answers", .{answers.items.len}); 112 | return answers.toOwnedSlice(); 113 | } 114 | 115 | fn scanInner(csr: ?*c.MDB_cursor, answers: *std.ArrayList([]const u8), key: Key, descending: bool, allocator: Allocator) !void { 116 | warn("lmdb.scanInner {s} {}", .{ key.toString(allocator), descending }); 117 | const fullkey = key.toString(allocator); 118 | const mdb_key = sliceToMdbVal(fullkey, allocator); 119 | const mdb_value = sliceToMdbVal("", allocator); 120 | var ret = c.mdb_cursor_get(csr, mdb_key, mdb_value, c.MDB_SET_RANGE); 121 | var ret_key = mdbValToBytes(mdb_key); 122 | var ret_value = mdbValToBytes(mdb_value); 123 | if (ret == c.MDB_NOTFOUND) { 124 | warn("lmdb.scanInner cursor_get first_fail {s} set_range err#{}", .{ fullkey, ret }); 125 | ret = c.mdb_cursor_get(csr, mdb_key, mdb_value, c.MDB_LAST); 126 | if (ret != 0) { 127 | warn("lmdb.scanInner first last fail {}", .{ret}); 128 | } 129 | ret_key = mdbValToBytes(mdb_key); 130 | ret_value = mdbValToBytes(mdb_value); 131 | warn("lmdb.scanInner cursor_get_last {s} {s} key \"{s}\" val \"{s}\"", .{ if (descending) "descending" else "ascending", fullkey, ret_key, ret_value }); 132 | } else { 133 | warn("lmdb.scanInner cursor_set_range {s} {s} key \"{s}\" val \"{s}\"", .{ if (descending) "descending" else "ascending", fullkey, ret_key, ret_value }); 134 | if (descending) { 135 | ret = c.mdb_cursor_get(csr, mdb_key, mdb_value, c.MDB_PREV); 136 | if (ret != 0) { 137 | warn("lmdb.scanInner first prev fail {}", .{ret}); 138 | } 139 | ret_key = mdbValToBytes(mdb_key); 140 | ret_value = mdbValToBytes(mdb_value); 141 | warn("lmdb.scanInner first prev {s} {s} key \"{s}\" val \"{s}\"", .{ if (descending) "descending" else "ascending", fullkey, ret_key, ret_value }); 142 | } 143 | } 144 | while (ret == 0 and prefix_match(key.lessLevel(), ret_key, allocator)) { 145 | if (answers.items.len < 10) { 146 | try answers.append(ret_value); 147 | } else break; 148 | ret = c.mdb_cursor_get(csr, mdb_key, mdb_value, if (descending) c.MDB_PREV else c.MDB_NEXT); 149 | ret_value = mdbValToBytes(mdb_value); 150 | ret_key = mdbValToBytes(mdb_key); 151 | warn("lmdb.scanInner {s} {s} key \"{s}\" val \"{s}\" {} answers", .{ if (descending) "descending" else "ascending", fullkey, ret_key, ret_value, answers.items.len }); 152 | } 153 | } 154 | 155 | test scan { 156 | try init(std.testing.allocator); 157 | try std.testing.expect(true); 158 | } 159 | 160 | fn prefix_match(key: Key, body: []const u8, allocator: Allocator) bool { 161 | return std.mem.startsWith(u8, body, key.toString(allocator)); 162 | } 163 | 164 | test prefix_match { 165 | try std.testing.expect(prefix_match(&.{"A"}, "AB", std.testing.allocator)); 166 | try std.testing.expect(prefix_match(&.{"A:"}, "A:", std.testing.allocator)); 167 | try std.testing.expect(prefix_match(&.{""}, "A:", std.testing.allocator)); 168 | try std.testing.expect(!prefix_match(&.{"B"}, "AB", std.testing.allocator)); 169 | try std.testing.expect(!prefix_match(&.{"BB"}, "B", std.testing.allocator)); 170 | } 171 | 172 | pub fn write(namespace: []const u8, key: []const u8, value: []const u8, allocator: Allocator) !void { 173 | const txn = try txn_open(); 174 | const dbi = try dbi_open(txn); 175 | // TODO: seperator issue. perhaps 2 byte invalid utf8 sequence 176 | const fullkey = util.strings_join_separator(&.{ namespace, key }, ':', allocator); 177 | warn("lmdb.write {s}={s}", .{ fullkey, value }); 178 | const mdb_key = sliceToMdbVal(fullkey, allocator); 179 | const mdb_value = sliceToMdbVal(value, allocator); 180 | const ret = c.mdb_put(txn, dbi, mdb_key, mdb_value, 0); 181 | if (ret == 0) { 182 | try txn_commit(txn); 183 | } else { 184 | warn("mdb_put ERR {}\n", .{ret}); 185 | return error.mdb_put; 186 | } 187 | } 188 | 189 | fn sliceToMdbVal(data: []const u8, allocator: Allocator) *c.MDB_val { 190 | var mdb_val = allocator.create(c.MDB_val) catch unreachable; 191 | mdb_val.mv_size = data.len; 192 | mdb_val.mv_data = @constCast(@ptrCast(data.ptr)); 193 | return mdb_val; 194 | } 195 | 196 | fn mdbValToBytes(mdb_val: *c.MDB_val) []const u8 { 197 | var ret_key: []const u8 = undefined; 198 | ret_key.len = mdb_val.mv_size; 199 | ret_key.ptr = @as([*]const u8, @ptrCast(mdb_val.mv_data)); 200 | return ret_key; 201 | } 202 | -------------------------------------------------------------------------------- /src/filter.zig: -------------------------------------------------------------------------------- 1 | // filter.zig 2 | const std = @import("std"); 3 | const Allocator = std.mem.Allocator; 4 | 5 | const util = @import("util.zig"); 6 | const warn = util.log; 7 | const string = []const u8; 8 | const toot_lib = @import("toot.zig"); 9 | 10 | const c = @cImport({ 11 | @cInclude("ragel/lang.h"); 12 | }); 13 | 14 | pub const ptree = struct { 15 | hostname: string, 16 | tags: *toot_lib.Type.TagList, 17 | 18 | const Self = @This(); 19 | 20 | pub fn host(self: *const Self) []const u8 { 21 | return self.hostname; 22 | } 23 | 24 | pub fn match(self: *const Self, toot: *toot_lib.Type) bool { 25 | if (self.tags.items.len == 0) { 26 | return true; 27 | } else { 28 | for (self.tags.items) |filter_tag| { 29 | for (toot.tagList.items) |toot_tag| { 30 | if (std.mem.order(u8, filter_tag, toot_tag) == std.math.Order.eq) { 31 | return true; 32 | } 33 | } 34 | } 35 | return false; 36 | } 37 | } 38 | }; 39 | 40 | pub fn parse(allocator: Allocator, lang: []const u8) *ptree { 41 | var ragel_points = c.urlPoints{ .scheme_pos = 0, .loc_pos = 0 }; 42 | const clang = util.sliceToCstr(allocator, lang); 43 | _ = c.url(clang, &ragel_points); 44 | warn("Ragel parse \"{s}\"", .{lang}); 45 | 46 | var newTree = allocator.create(ptree) catch unreachable; 47 | newTree.tags = allocator.create(toot_lib.Type.TagList) catch unreachable; 48 | newTree.tags.* = toot_lib.Type.TagList.init(allocator); 49 | var spaceParts = std.mem.tokenizeSequence(u8, lang, " "); 50 | var idx: usize = 0; 51 | while (spaceParts.next()) |part| { 52 | idx += 1; 53 | if (idx == 1) { 54 | newTree.hostname = part; 55 | warn("filter set host={s}", .{part}); 56 | } 57 | if (idx > 1) { 58 | newTree.tags.append(part) catch unreachable; 59 | warn("filter set tag #{} {s}", .{ newTree.tags.items.len, part }); 60 | } 61 | } 62 | return newTree; 63 | } 64 | -------------------------------------------------------------------------------- /src/gui.zig: -------------------------------------------------------------------------------- 1 | // gui.zig 2 | const std = @import("std"); 3 | const util = @import("./util.zig"); 4 | const warn = util.log; 5 | const Allocator = std.mem.Allocator; 6 | 7 | const config = @import("./config.zig"); 8 | const toot_lib = @import("./toot.zig"); 9 | const thread = @import("./thread.zig"); 10 | 11 | const guilib = @import("./gui/gtk3.zig"); 12 | 13 | const GUIError = error{Init}; 14 | const Column = guilib.Column; 15 | var columns: std.ArrayList(*Column) = undefined; 16 | var allocator: Allocator = undefined; 17 | var settings: *config.Settings = undefined; 18 | 19 | pub fn init(alloca: Allocator, set: *config.Settings) !void { 20 | warn("GUI init()", .{}); 21 | settings = set; 22 | allocator = alloca; 23 | columns = std.ArrayList(*Column).init(allocator); 24 | try guilib.init(alloca, set); 25 | } 26 | 27 | var myActor: *thread.Actor = undefined; 28 | var stop = false; 29 | 30 | pub fn go(data: ?*anyopaque) callconv(.C) ?*anyopaque { 31 | warn("GUI {s} mainloop", .{guilib.libname()}); 32 | myActor = @as(*thread.Actor, @ptrCast(@alignCast(data))); 33 | if (guilib.gui_setup(myActor)) { 34 | // mainloop 35 | //var then = std.time.milliTimestamp(); 36 | while (!stop) { 37 | stop = guilib.mainloop(); 38 | //var now = std.time.milliTimestamp(); 39 | //warn("{}ms pause gui mainloop", .{now - then}); 40 | //then = now; 41 | } 42 | warn("final mainloop {}", .{guilib.mainloop()}); 43 | guilib.gui_end(); 44 | } else |err| { 45 | warn("gui error {}", .{err}); 46 | } 47 | return null; 48 | } 49 | pub fn schedule(func: ?*const fn (?*anyopaque) callconv(.C) c_int, param: ?*anyopaque) void { 50 | guilib.schedule(func, param); 51 | } 52 | 53 | pub fn show_main_schedule(in: ?*anyopaque) callconv(.C) c_int { 54 | return guilib.show_main_schedule(in); 55 | } 56 | 57 | pub fn add_column_schedule(in: ?*anyopaque) callconv(.C) c_int { 58 | return guilib.add_column_schedule(in); 59 | } 60 | 61 | pub fn column_remove_schedule(in: ?*anyopaque) callconv(.C) c_int { 62 | return guilib.column_remove_schedule(in); 63 | } 64 | 65 | pub fn column_config_oauth_url_schedule(in: ?*anyopaque) callconv(.C) c_int { 66 | return guilib.column_config_oauth_url_schedule(in); 67 | } 68 | 69 | pub fn update_column_config_oauth_finalize_schedule(in: ?*anyopaque) callconv(.C) c_int { 70 | return guilib.update_column_config_oauth_finalize_schedule(in); 71 | } 72 | 73 | pub fn update_column_ui_schedule(in: ?*anyopaque) callconv(.C) c_int { 74 | return guilib.update_column_ui_schedule(in); 75 | } 76 | 77 | pub fn update_column_netstatus_schedule(in: ?*anyopaque) callconv(.C) c_int { 78 | return guilib.update_column_netstatus_schedule(in); 79 | } 80 | 81 | pub fn update_column_toots_schedule(in: ?*anyopaque) callconv(.C) c_int { 82 | return guilib.update_column_toots_schedule(in); 83 | } 84 | 85 | pub fn update_author_photo_schedule(in: ?*anyopaque) callconv(.C) c_int { 86 | return guilib.update_author_photo_schedule(in); 87 | } 88 | 89 | pub const TootPic = guilib.TootPic; 90 | pub fn toot_media_schedule(in: ?*anyopaque) callconv(.C) c_int { 91 | return guilib.toot_media_schedule(in); 92 | } 93 | -------------------------------------------------------------------------------- /src/gui/column.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | True 7 | False 8 | vertical 9 | 10 | 11 | True 12 | False 13 | vertical 14 | 15 | 16 | True 17 | False 18 | 19 | 20 | True 21 | False 22 | 23 | 24 | 25 | 26 | True 27 | False 28 | Top 29 | 32 | 33 | 34 | 35 | 36 | True 37 | True 38 | 0 39 | 40 | 41 | 42 | 43 | True 44 | False 45 | 46 | 47 | 48 | True 49 | False 50 | Refresh now 51 | 5 52 | ../img/reload-icon.svg 53 | 54 | 55 | 56 | 57 | False 58 | True 59 | 1 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | False 68 | True 69 | 0 70 | 71 | 72 | 73 | 74 | True 75 | True 76 | 77 | 80 | 81 | 82 | False 83 | True 84 | 1 85 | 86 | 87 | 88 | 89 | False 90 | True 91 | 0 92 | 93 | 94 | 95 | 96 | True 97 | True 98 | never 99 | always 100 | in 101 | 0 102 | 0 103 | 104 | 105 | True 106 | False 107 | 108 | 109 | True 110 | False 111 | False 112 | vertical 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | True 123 | True 124 | 1 125 | 126 | 127 | 128 | 129 | True 130 | False 131 | 132 | 133 | True 134 | False 135 | netstatus 136 | 137 | 138 | False 139 | True 140 | 0 141 | 142 | 143 | 144 | 145 | True 146 | False 147 | - empty - 148 | 151 | 152 | 153 | True 154 | True 155 | 1 156 | 157 | 158 | 159 | 160 | True 161 | False 162 | 10 163 | 3 164 | 165 | 166 | True 167 | False 168 | column settings 169 | 170 | 171 | 172 | True 173 | False 174 | ../img/gears.svg 175 | 176 | 177 | 178 | 179 | False 180 | True 181 | 0 182 | 183 | 184 | 185 | 186 | True 187 | False 188 | images only mode 189 | 190 | 191 | 192 | True 193 | False 194 | gtk-select-color 195 | 196 | 197 | 198 | 199 | False 200 | True 201 | 1 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | False 210 | True 211 | 3 212 | 213 | 214 | 217 | 218 | 219 | False 220 | True 221 | 2 222 | 223 | 224 | 227 | 228 | 229 | False 230 | 231 | 232 | 233 | 234 | 235 | True 236 | False 237 | vertical 238 | 239 | 240 | True 241 | False 242 | column config 243 | 244 | 245 | False 246 | True 247 | 0 248 | 249 | 250 | 251 | 252 | True 253 | False 254 | vertical 255 | 256 | 257 | True 258 | False 259 | 260 | 261 | True 262 | False 263 | server hostname 264 | 267 | 268 | 269 | True 270 | True 271 | 0 272 | 273 | 274 | 275 | 276 | True 277 | False 278 | 279 | 280 | 281 | True 282 | False 283 | request authorization 284 | gtk-dialog-authentication 285 | 286 | 287 | 288 | 289 | False 290 | True 291 | 1 292 | 293 | 294 | 295 | 296 | False 297 | True 298 | 0 299 | 300 | 301 | 302 | 303 | True 304 | True 305 | 306 | 309 | 310 | 311 | False 312 | True 313 | 1 314 | 315 | 316 | 319 | 320 | 321 | False 322 | True 323 | 2 324 | 325 | 326 | 327 | 328 | True 329 | False 330 | 331 | 332 | True 333 | False 334 | oauth token 335 | 336 | 337 | True 338 | True 339 | 0 340 | 341 | 342 | 343 | 344 | True 345 | False 346 | gtk-missing-image 347 | 348 | 349 | False 350 | True 351 | 2 352 | 353 | 354 | 355 | 356 | False 357 | True 358 | 3 359 | 360 | 361 | 362 | 363 | remove column 364 | True 365 | True 366 | True 367 | True 368 | center 369 | 370 | 373 | 374 | 375 | False 376 | True 377 | 4 378 | 379 | 380 | 383 | 384 | 385 | 386 | 387 | True 388 | False 389 | vertical 390 | 391 | 392 | True 393 | False 394 | oauth url 395 | 396 | 397 | False 398 | True 399 | 0 400 | 401 | 402 | 403 | 404 | True 405 | False 406 | vertical 407 | 408 | 409 | True 410 | False 411 | authorization code 412 | 413 | 414 | False 415 | True 416 | 0 417 | 418 | 419 | 420 | 421 | True 422 | True 423 | 424 | 425 | 426 | False 427 | True 428 | 1 429 | 430 | 431 | 432 | 433 | False 434 | True 435 | 1 436 | 437 | 438 | 439 | 440 | -------------------------------------------------------------------------------- /src/gui/gtk3.zig: -------------------------------------------------------------------------------- 1 | // GTK+ 2 | const std = @import("std"); 3 | const util = @import("../util.zig"); 4 | const warn = util.log; 5 | const config = @import("../config.zig"); 6 | const SimpleBuffer = @import("../simple_buffer.zig"); 7 | const toot_lib = @import("../toot.zig"); 8 | const thread = @import("../thread.zig"); 9 | const filter_lib = @import("../filter.zig"); 10 | 11 | const theme_css = @embedFile("theme.css"); 12 | const glade_zootdeck = @embedFile("zootdeck.glade"); 13 | const glade_column = @embedFile("column.glade"); 14 | const glade_toot = @embedFile("toot.glade"); 15 | 16 | const c = @cImport({ 17 | @cInclude("gtk/gtk.h"); 18 | }); 19 | 20 | const GUIError = error{ 21 | GtkInit, 22 | GladeLoad, 23 | }; 24 | 25 | var allocator: std.mem.Allocator = undefined; 26 | var settings: *config.Settings = undefined; 27 | pub const queue = std.ArrayList(u8).init(allocator); 28 | var myActor: *thread.Actor = undefined; 29 | var myAllocation = c.GtkAllocation{ .x = -1, .y = -1, .width = 0, .height = 0 }; 30 | 31 | pub const Column = struct { 32 | builder: *c.GtkBuilder, 33 | columnbox: *c.GtkWidget, 34 | config_window: *c.GtkWidget, 35 | main: *config.ColumnInfo, 36 | guitoots: std.StringHashMap(*c.GtkBuilder), 37 | }; 38 | 39 | var columns: std.ArrayList(*Column) = undefined; 40 | var myBuilder: *c.GtkBuilder = undefined; 41 | var myCssProvider: *c.GtkCssProvider = undefined; 42 | 43 | pub fn libname() []const u8 { 44 | return "GTK"; 45 | } 46 | 47 | pub fn init(alloca: std.mem.Allocator, set: *config.Settings) !void { 48 | warn("{s} init()", .{libname()}); 49 | settings = set; 50 | allocator = alloca; 51 | columns = std.ArrayList(*Column).init(allocator); 52 | var argc: c_int = undefined; 53 | const argv: ?[*]?[*]?[*]u8 = null; 54 | const tf = c.gtk_init_check(@as([*]c_int, @ptrCast(&argc)), argv); 55 | if (tf != 1) return GUIError.GtkInit; 56 | } 57 | 58 | pub fn gui_setup(actor: *thread.Actor) !void { 59 | myActor = actor; 60 | // GtkCssProvider *cssProvider = gtk_css_provider_new(); 61 | myCssProvider = c.gtk_css_provider_new(); 62 | _ = c.gtk_css_provider_load_from_data(myCssProvider, theme_css, theme_css.len, null); 63 | c.gtk_style_context_add_provider_for_screen(c.gdk_screen_get_default(), @ptrCast(myCssProvider), c.GTK_STYLE_PROVIDER_PRIORITY_USER); 64 | 65 | myBuilder = c.gtk_builder_new(); 66 | const ret = c.gtk_builder_add_from_string(myBuilder, glade_zootdeck, glade_zootdeck.len, @as([*c][*c]c._GError, @ptrFromInt(0))); 67 | if (ret == 0) { 68 | warn("builder file fail", .{}); 69 | return GUIError.GladeLoad; 70 | } 71 | 72 | // Callbacks 73 | _ = c.gtk_builder_add_callback_symbol(myBuilder, "actionbar.add", actionbar_add); 74 | _ = c.gtk_builder_add_callback_symbol(myBuilder, "zoot_drag", zoot_drag); 75 | // captures all keys oh no 76 | // _ = c.gtk_builder_add_callback_symbol(myBuilder, "zoot.keypress", 77 | // @ptrCast(?extern fn() void, zoot_keypress)); 78 | _ = c.gtk_builder_add_callback_symbol(myBuilder, "main_check_resize", @ptrCast(&main_check_resize)); 79 | _ = c.gtk_builder_connect_signals(myBuilder, null); 80 | 81 | // set main size before resize callback happens 82 | const main_window = builder_get_widget(myBuilder, "main"); 83 | const w = @as(c.gint, @intCast(settings.win_x)); 84 | const h = @as(c.gint, @intCast(settings.win_y)); 85 | // c.gtk_widget_set_size_request(main_window, w, h); 86 | c.gtk_window_resize(@as(*c.GtkWindow, @ptrCast(main_window)), w, h); 87 | _ = g_signal_connect(main_window, "destroy", gtk_quit, null); 88 | } 89 | 90 | fn builder_get_widget(builder: *c.GtkBuilder, name: [*]const u8) *c.GtkWidget { 91 | const gobject = @as(*c.GTypeInstance, @ptrCast(c.gtk_builder_get_object(builder, name))); 92 | const gwidget = @as(*c.GtkWidget, @ptrCast(c.g_type_check_instance_cast(gobject, c.gtk_widget_get_type()))); 93 | return gwidget; 94 | } 95 | 96 | pub fn schedule(func: c.GSourceFunc, param: ?*anyopaque) void { 97 | _ = c.gdk_threads_add_idle(func, param); 98 | } 99 | 100 | fn hide_column_config(column: *Column) void { 101 | c.gtk_widget_hide(column.config_window); 102 | } 103 | 104 | pub fn show_login_schedule(in: *anyopaque) callconv(.C) c_int { 105 | _ = in; 106 | const login_window = builder_get_widget(myBuilder, "login"); 107 | c.gtk_widget_show(login_window); 108 | return 0; 109 | } 110 | 111 | pub fn show_main_schedule(in: ?*anyopaque) callconv(.C) c_int { 112 | _ = in; 113 | const main_window = builder_get_widget(myBuilder, "main"); 114 | c.gtk_widget_show(main_window); 115 | return 0; 116 | } 117 | 118 | pub fn column_config_oauth_url_schedule(in: ?*anyopaque) callconv(.C) c_int { 119 | const column = @as(*config.ColumnInfo, @ptrCast(@alignCast(in))); 120 | column_config_oauth_url(column); 121 | return 0; 122 | } 123 | 124 | pub fn update_author_photo_schedule(in: ?*anyopaque) callconv(.C) c_int { 125 | const cAcct = @as([*]const u8, @ptrCast(@alignCast(in))); 126 | const acct = util.cstrToSliceCopy(allocator, cAcct); 127 | update_author_photo(acct); 128 | return 0; 129 | } 130 | 131 | pub fn update_column_ui_schedule(in: ?*anyopaque) callconv(.C) c_int { 132 | const columnInfo = @as(*config.ColumnInfo, @ptrCast(@alignCast(in))); 133 | const column = findColumnByInfo(columnInfo); 134 | update_column_ui(column); 135 | return 0; 136 | } 137 | 138 | pub const TootPic = struct { 139 | toot: *toot_lib.Type, 140 | img: toot_lib.Img, 141 | }; 142 | 143 | pub fn toot_media_schedule(in: ?*anyopaque) callconv(.C) c_int { 144 | const tootpic = @as(*TootPic, @ptrCast(@alignCast(in))); 145 | const toot = tootpic.toot; 146 | if (findColumnByTootId(toot.id())) |column| { 147 | const builder = column.guitoots.get(toot.id()).?; 148 | toot_media(column, builder, toot, tootpic.img.bytes); 149 | } 150 | return 0; 151 | } 152 | 153 | pub fn add_column_schedule(in: ?*anyopaque) callconv(.C) c_int { 154 | const column = @as(*config.ColumnInfo, @ptrCast(@alignCast(in))); 155 | add_column(column); 156 | return 0; 157 | } 158 | 159 | pub fn add_column(colInfo: *config.ColumnInfo) void { 160 | const container = builder_get_widget(myBuilder, "ZootColumns"); 161 | const column = allocator.create(Column) catch unreachable; 162 | column.builder = c.gtk_builder_new_from_string(glade_column, glade_column.len); 163 | column.columnbox = builder_get_widget(column.builder, "column"); 164 | column.main = colInfo; 165 | //var line_buf: []u8 = allocator.alloc(u8, 255) catch unreachable; 166 | column.config_window = builder_get_widget(column.builder, "column_config"); 167 | c.gtk_window_resize(@as(*c.GtkWindow, @ptrCast(column.config_window)), 600, 200); 168 | column.guitoots = std.StringHashMap(*c.GtkBuilder).init(allocator); 169 | columns.append(column) catch unreachable; 170 | columns_resize(); 171 | warn("gtk3.add_column {s}", .{util.json_stringify(column.main.makeTitle())}); 172 | const filter = builder_get_widget(column.builder, "column_filter"); 173 | const cFilter = util.sliceToCstr(allocator, column.main.config.filter); 174 | c.gtk_entry_set_text(@as(*c.GtkEntry, @ptrCast(filter)), cFilter); 175 | //const footer = builder_get_widget(column.builder, "column_footer"); 176 | const config_icon = builder_get_widget(column.builder, "column_config_icon"); 177 | c.gtk_misc_set_alignment(@as(*c.GtkMisc, @ptrCast(config_icon)), 1, 0); 178 | 179 | update_column_ui(column); 180 | 181 | c.gtk_grid_attach_next_to(@as(*c.GtkGrid, @ptrCast(container)), column.columnbox, null, c.GTK_POS_RIGHT, 1, 1); 182 | 183 | _ = c.gtk_builder_add_callback_symbol(column.builder, "column.title", @ptrCast(&column_top_label_title)); 184 | _ = c.gtk_builder_add_callback_symbol(column.builder, "column.config", @ptrCast(&column_config_btn)); 185 | _ = c.gtk_builder_add_callback_symbol(column.builder, "column.reload", @ptrCast(&column_reload)); 186 | _ = c.gtk_builder_add_callback_symbol(column.builder, "column.imgonly", @ptrCast(&column_imgonly)); 187 | _ = c.gtk_builder_add_callback_symbol(column.builder, "column.filter_done", @ptrCast(&column_filter_done)); 188 | _ = c.gtk_builder_add_callback_symbol(column.builder, "column_config.done", @ptrCast(&column_config_done)); 189 | _ = c.gtk_builder_add_callback_symbol(column.builder, "column_config.remove", @ptrCast(&column_remove_btn)); 190 | _ = c.gtk_builder_add_callback_symbol(column.builder, "column_config.oauth_btn", @ptrCast(&column_config_oauth_btn)); 191 | _ = c.gtk_builder_add_callback_symbol(column.builder, "column_config.oauth_auth_enter", @ptrCast(&column_config_oauth_activate)); 192 | _ = c.gtk_builder_add_callback_symbol(column.builder, "zoot_drag", zoot_drag); 193 | _ = c.gtk_builder_connect_signals(column.builder, null); 194 | c.gtk_widget_show_all(container); 195 | } 196 | 197 | pub fn update_author_photo(acct: []const u8) void { 198 | // upodate all toots in all columns for this author 199 | for (columns.items) |column| { 200 | const toots = column.main.toots.author(acct, allocator); 201 | for (toots) |toot| { 202 | warn("update_author_photo column:{s} author:{s} toot#{s}", .{ column.main.filter.host(), acct, toot.id() }); 203 | const tootbuilderMaybe = column.guitoots.get(toot.id()); 204 | if (tootbuilderMaybe) |kv| { 205 | photo_refresh(acct, kv); 206 | } 207 | } 208 | } 209 | } 210 | 211 | pub fn columns_resize() void { 212 | if (columns.items.len > 0) { 213 | const container = builder_get_widget(myBuilder, "ZootColumns"); 214 | const app_width = c.gtk_widget_get_allocated_width(container); 215 | const avg_col_width = @divTrunc(app_width, @as(c_int, @intCast(columns.items.len))); 216 | warn("columns_resize app_width {} col_width {} columns {}", .{ app_width, avg_col_width, columns.items.len }); 217 | for (columns.items) |col| { 218 | c.gtk_widget_get_allocation(col.columnbox, &myAllocation); 219 | } 220 | } 221 | } 222 | 223 | pub fn update_column_ui(column: *Column) void { 224 | const label = builder_get_widget(column.builder, "column_top_label"); 225 | //var topline_null: []u8 = undefined; 226 | const title_null = util.sliceAddNull(allocator, column.main.makeTitle()); 227 | c.gtk_label_set_text(@as(*c.GtkLabel, @ptrCast(label)), title_null.ptr); 228 | } 229 | 230 | pub fn column_remove_schedule(in: ?*anyopaque) callconv(.C) c_int { 231 | column_remove(@as(*config.ColumnInfo, @ptrCast(@alignCast(in)))); 232 | return 0; 233 | } 234 | 235 | pub fn column_remove(colInfo: *config.ColumnInfo) void { 236 | warn("gui.column_remove {s}\n", .{colInfo.config.title}); 237 | const container = builder_get_widget(myBuilder, "ZootColumns"); 238 | const column = findColumnByInfo(colInfo); 239 | hide_column_config(column); 240 | c.gtk_container_remove(@as(*c.GtkContainer, @ptrCast(container)), column.columnbox); 241 | } 242 | 243 | //pub const GCallback = ?extern fn() void; 244 | fn column_top_label_title(p: *anyopaque) callconv(.C) void { 245 | warn("column_top_label_title {}\n", .{p}); 246 | } 247 | 248 | pub fn update_column_toots_schedule(in: ?*anyopaque) callconv(.C) c_int { 249 | const c_column = @as(*config.ColumnInfo, @ptrCast(@alignCast(in))); 250 | const columnMaybe = find_gui_column(c_column); 251 | if (columnMaybe) |column| { 252 | update_column_toots(column); 253 | } 254 | return 0; 255 | } 256 | 257 | pub fn update_column_config_oauth_finalize_schedule(in: ?*anyopaque) callconv(.C) c_int { 258 | const c_column = @as(*config.ColumnInfo, @ptrCast(@alignCast(in))); 259 | const columnMaybe = find_gui_column(c_column); 260 | if (columnMaybe) |column| { 261 | column_config_oauth_finalize(column); 262 | } 263 | return 0; 264 | } 265 | 266 | pub fn update_column_netstatus_schedule(in: ?*anyopaque) callconv(.C) c_int { 267 | const http = @as(*config.HttpInfo, @ptrCast(@alignCast(in))); 268 | const columnMaybe = find_gui_column(http.column); 269 | if (columnMaybe) |column| { 270 | update_netstatus_column(http, column); 271 | } 272 | return 0; 273 | } 274 | 275 | fn find_gui_column(c_column: *config.ColumnInfo) ?*Column { 276 | //var column: *Column = undefined; 277 | for (columns.items) |col| { 278 | if (col.main == c_column) return col; 279 | } 280 | return null; 281 | } 282 | 283 | pub fn update_column_toots(column: *Column) void { 284 | warn("update_column_toots title: {s} toot count: {} guitoots: {} {s}", .{ 285 | util.json_stringify(column.main.makeTitle()), 286 | column.main.toots.count(), 287 | column.guitoots.count(), 288 | if (column.main.inError) @as([]const u8, "INERROR") else @as([]const u8, ""), 289 | }); 290 | const column_toot_zone = builder_get_widget(column.builder, "toot_zone"); 291 | var current = column.main.toots.first(); 292 | var idx: c_int = 0; 293 | if (current != null) { 294 | while (current) |node| { 295 | const toot: *toot_lib.Toot() = node.data; 296 | warn("update_column_toots building {*} #{s}", .{ toot, toot.id() }); 297 | const tootbuilderMaybe = column.guitoots.get(toot.id()); 298 | if (column.main.filter.match(toot)) { 299 | if (tootbuilderMaybe) |kv| { 300 | const builder = kv; 301 | destroyTootBox(builder); 302 | warn("update_column_toots destroyTootBox toot #{s} {*} {*}", .{ toot.id(), toot, builder }); 303 | } 304 | const tootbuilder = makeTootBox(toot, column); 305 | const tootbox = builder_get_widget(tootbuilder, "tootbox"); 306 | _ = column.guitoots.put(toot.id(), tootbuilder) catch unreachable; 307 | c.gtk_box_pack_start(@as(*c.GtkBox, @ptrCast(column_toot_zone)), tootbox, c.gtk_true(), c.gtk_true(), 0); 308 | c.gtk_box_reorder_child(@as(*c.GtkBox, @ptrCast(column_toot_zone)), tootbox, idx); 309 | } else { 310 | if (tootbuilderMaybe) |kv| { 311 | const builder = kv; 312 | const tootbox = builder_get_widget(builder, "tootbox"); 313 | c.gtk_widget_hide(tootbox); 314 | warn("update_column_toots hide toot #{s} {*} {*}", .{ toot.id(), toot, builder }); 315 | } 316 | } 317 | 318 | current = node.next; 319 | idx += 1; 320 | } 321 | } else { 322 | // help? logo? 323 | } 324 | const column_footer_count_label = builder_get_widget(column.builder, "column_footer_count"); 325 | const tootword = if (column.main.config.img_only) "images" else "posts"; 326 | const countStr = std.fmt.allocPrint(allocator, "{} {s}", .{ column.main.toots.count(), tootword }) catch unreachable; 327 | const cCountStr = util.sliceToCstr(allocator, countStr); 328 | c.gtk_label_set_text(@ptrCast(column_footer_count_label), cCountStr); 329 | } 330 | 331 | pub fn update_netstatus_column(http: *config.HttpInfo, column: *Column) void { 332 | warn("update_netstatus_column {s} {s} status: {}", .{ column.main.filter.hostname, http.url, http.response_code }); 333 | const column_footer_netstatus = builder_get_widget(column.builder, "column_footer_netstatus"); 334 | const gtk_context_netstatus = c.gtk_widget_get_style_context(column_footer_netstatus); 335 | if (http.response_code == 0) { // active is a special case 336 | c.gtk_style_context_remove_class(gtk_context_netstatus, "net_error"); 337 | c.gtk_style_context_add_class(gtk_context_netstatus, "net_active"); 338 | c.gtk_label_set_text(@as(*c.GtkLabel, @ptrCast(column_footer_netstatus)), "GET"); 339 | } else { 340 | c.gtk_style_context_remove_class(gtk_context_netstatus, "net_active"); 341 | } 342 | if (http.response_code >= 200 and http.response_code < 300) { 343 | c.gtk_label_set_text(@as(*c.GtkLabel, @ptrCast(column_footer_netstatus)), "OK"); 344 | } else if (http.response_code >= 300 and http.response_code < 400) { 345 | c.gtk_label_set_text(@as(*c.GtkLabel, @ptrCast(column_footer_netstatus)), "redirect"); 346 | } else if (http.response_code >= 400 and http.response_code < 500) { 347 | column.main.inError = true; 348 | if (http.response_code == 401) { 349 | c.gtk_label_set_text(@as(*c.GtkLabel, @ptrCast(column_footer_netstatus)), "token bad"); 350 | } else if (http.response_code == 404) { 351 | c.gtk_label_set_text(@as(*c.GtkLabel, @ptrCast(column_footer_netstatus)), "404 err"); 352 | } else { 353 | c.gtk_label_set_text(@as(*c.GtkLabel, @ptrCast(column_footer_netstatus)), "4xx err"); 354 | } 355 | } else if (http.response_code >= 500 and http.response_code < 600) { 356 | column.main.inError = true; 357 | c.gtk_label_set_text(@as(*c.GtkLabel, @ptrCast(column_footer_netstatus)), "5xx err"); 358 | } else if (http.response_code >= 1000 and http.response_code < 1100) { 359 | column.main.inError = true; 360 | c.gtk_label_set_text(@as(*c.GtkLabel, @ptrCast(column_footer_netstatus)), "json err"); 361 | } else if (http.response_code == 2100) { 362 | column.main.inError = true; 363 | c.gtk_label_set_text(@as(*c.GtkLabel, @ptrCast(column_footer_netstatus)), "DNS err"); 364 | } else if (http.response_code == 2200) { 365 | column.main.inError = true; 366 | c.gtk_label_set_text(@as(*c.GtkLabel, @ptrCast(column_footer_netstatus)), "timeout"); 367 | } 368 | if (column.main.inError) { 369 | c.gtk_style_context_add_class(gtk_context_netstatus, "net_error"); 370 | } else { 371 | c.gtk_style_context_remove_class(gtk_context_netstatus, "net_error"); 372 | } 373 | } 374 | 375 | fn widget_destroy(widget: *c.GtkWidget, userdata: ?*anyopaque) callconv(.C) void { 376 | //warn("destroying {*}\n", widget); 377 | _ = userdata; 378 | c.gtk_widget_destroy(widget); 379 | } 380 | 381 | pub fn destroyTootBox(builder: *c.GtkBuilder) void { 382 | const tootbox = builder_get_widget(builder, "tootbox"); 383 | c.gtk_widget_destroy(tootbox); 384 | c.g_object_unref(builder); 385 | } 386 | 387 | pub fn makeTootBox(toot: *toot_lib.Type, column: *Column) *c.GtkBuilder { 388 | warn("maketootbox toot #{s} {*} gui building {} images", .{ toot.id(), toot, toot.imgList.items.len }); 389 | const builder = c.gtk_builder_new_from_string(glade_toot, glade_toot.len); 390 | 391 | //const id = toot.get("id").?.string; 392 | const account = toot.get("account").?.object; 393 | const author_acct = account.get("acct").?.string; 394 | 395 | const author_name = account.get("display_name").?.string; 396 | const author_url = account.get("url").?.string; 397 | const created_at = toot.get("created_at").?.string; 398 | 399 | warn("makeTootBox author_name {s}", .{author_name}); 400 | const name_label = builder_get_widget(builder, "toot_author_name"); 401 | labelBufPrint(@ptrCast(name_label), "{s}", .{author_name}); 402 | const url_label = builder_get_widget(builder, "toot_author_url"); 403 | labelBufPrint(@ptrCast(url_label), "{s}", .{author_url}); 404 | const author_url_minimode_label = builder_get_widget(builder, "toot_author_url_minimode"); 405 | labelBufPrint(@ptrCast(author_url_minimode_label), "{s}", .{author_url}); 406 | const date_label = builder_get_widget(builder, "toot_date"); 407 | labelBufPrint(@ptrCast(date_label), "{s}", .{created_at}); 408 | photo_refresh(author_acct, builder); 409 | 410 | const hDecode = util.htmlEntityDecode(toot.content(), allocator) catch unreachable; 411 | const html_trim = util.htmlTagStrip(hDecode, allocator) catch unreachable; 412 | //var line_limit = 50 / columns.items.len; 413 | //const html_wrapped = hardWrap(html_trim, line_limit) catch unreachable; 414 | const cText = util.sliceToCstr(allocator, html_trim); 415 | 416 | const toottext_label = builder_get_widget(builder, "toot_text"); 417 | c.gtk_label_set_line_wrap_mode(@as(*c.GtkLabel, @ptrCast(toottext_label)), c.PANGO_WRAP_WORD_CHAR); 418 | c.gtk_label_set_line_wrap(@as(*c.GtkLabel, @ptrCast(toottext_label)), 1); 419 | c.gtk_label_set_text(@as(*c.GtkLabel, @ptrCast(toottext_label)), cText); 420 | 421 | const tagBox = builder_get_widget(builder, "tag_flowbox"); 422 | //var tagidx: usize = 0; 423 | for (toot.tagList.items) |tag| { 424 | const tag_len = if (tag.len > 40) 40 else tag.len; 425 | const cTag = util.sliceToCstr(allocator, tag[0..tag_len]); 426 | const tagLabel = c.gtk_label_new(cTag); 427 | const labelContext = c.gtk_widget_get_style_context(tagLabel); 428 | c.gtk_style_context_add_class(labelContext, "toot_tag"); 429 | c.gtk_container_add(@as(*c.GtkContainer, @ptrCast(tagBox)), tagLabel); 430 | c.gtk_widget_show(tagLabel); 431 | } 432 | 433 | // show/hide parts to put widget into full or imgonly display 434 | const id_row = builder_get_widget(builder, "toot_id_row"); 435 | const toot_separator = builder_get_widget(builder, "toot_separator"); 436 | if (column.main.config.img_only) { 437 | c.gtk_widget_hide(toottext_label); 438 | c.gtk_widget_hide(id_row); 439 | if (toot.imgCount() == 0) { 440 | c.gtk_widget_hide(date_label); 441 | c.gtk_widget_hide(toot_separator); 442 | } else { 443 | c.gtk_widget_show(author_url_minimode_label); 444 | c.gtk_widget_show(date_label); 445 | } 446 | } 447 | 448 | for (toot.imgList.items) |img| { 449 | warn("toot #{s} rebuilding with img", .{toot.id()}); 450 | toot_media(column, builder, toot, img.bytes); 451 | } 452 | 453 | return builder; 454 | } 455 | 456 | fn photo_refresh(acct: []const u8, builder: *c.GtkBuilder) void { 457 | const avatar = builder_get_widget(builder, "toot_author_avatar"); 458 | const avatar_path = std.fmt.allocPrint(allocator, "./cache/accounts/{s}/photo", .{acct}) catch unreachable; 459 | const pixbuf = c.gdk_pixbuf_new_from_file_at_scale(util.sliceToCstr(allocator, avatar_path), 50, -1, 1, null); 460 | c.gtk_image_set_from_pixbuf(@ptrCast(avatar), pixbuf); 461 | } 462 | 463 | fn toot_media(column: *Column, builder: *c.GtkBuilder, toot: *toot_lib.Type, pic: []const u8) void { 464 | const imageBox = builder_get_widget(builder, "image_box"); 465 | c.gtk_widget_get_allocation(column.columnbox, &myAllocation); 466 | const loader = c.gdk_pixbuf_loader_new(); 467 | // todo: size-prepared signal 468 | const colWidth = @as(c_int, @intFromFloat(@as(f32, @floatFromInt(myAllocation.width)) / @as(f32, @floatFromInt(columns.items.len)) * 0.9)); 469 | const colHeight: c_int = -1; // seems to work 470 | const colWidth_ptr = allocator.create(c_int) catch unreachable; 471 | colWidth_ptr.* = colWidth; 472 | _ = g_signal_connect(loader, "size-prepared", pixloaderSizePrepared, colWidth_ptr); 473 | const loadYN = c.gdk_pixbuf_loader_write(loader, pic.ptr, pic.len, null); 474 | if (loadYN == c.gtk_true()) { 475 | const pixbuf = c.gdk_pixbuf_loader_get_pixbuf(loader); 476 | //const account = toot.get("account").?.Object; 477 | //const acct = account.get("acct").?.String; 478 | const pixbufWidth = c.gdk_pixbuf_get_width(pixbuf); 479 | warn("toot_media #{s} {} images. win {}x{} col {}x{}px pixbuf {}px", .{ 480 | toot.id(), 481 | toot.imgCount(), 482 | myAllocation.width, 483 | myAllocation.height, 484 | colWidth, 485 | colHeight, 486 | pixbufWidth, 487 | }); 488 | _ = c.gdk_pixbuf_loader_close(loader, null); 489 | if (pixbuf != null) { 490 | const new_img = c.gtk_image_new_from_pixbuf(pixbuf); 491 | c.gtk_box_pack_start(@as(*c.GtkBox, @ptrCast(imageBox)), new_img, c.gtk_false(), c.gtk_false(), 0); 492 | c.gtk_widget_show(new_img); 493 | } else { 494 | warn("toot_media img from pixbuf FAILED", .{}); 495 | } 496 | } else { 497 | warn("pixbuf load FAILED of {} bytes", .{pic.len}); 498 | } 499 | } 500 | 501 | fn pixloaderSizePrepared(loader: *c.GdkPixbufLoader, img_width: c.gint, img_height: c.gint, data_ptr: *anyopaque) void { 502 | if (img_width > 0 and img_width < 65535 and img_height > 0 and img_height < 65535) { 503 | const colWidth = @as(*c_int, @ptrCast(@alignCast(data_ptr))).*; 504 | var scaleWidth = img_width; 505 | var scaleHeight = img_height; 506 | if (img_width > colWidth) { 507 | scaleWidth = colWidth; 508 | //const scale_factor = @divFloor(img_width, colWidth); 509 | const scale_factor = @as(f32, @floatFromInt(colWidth)) / @as(f32, @floatFromInt(img_width)); 510 | scaleHeight = @as(c_int, @intFromFloat(@as(f32, @floatFromInt(img_height)) * scale_factor)); 511 | } 512 | warn("toot_media pixloaderSizePrepared col width {}px img {}x{} scaled {}x{}", .{ colWidth, img_width, img_height, scaleWidth, scaleHeight }); 513 | c.gdk_pixbuf_loader_set_size(loader, scaleWidth, scaleHeight); 514 | } else { 515 | warn("pixloaderSizePrepared img {}x{} was out of bounds", .{ img_width, img_height }); 516 | } 517 | } 518 | 519 | fn hardWrap(str: []const u8, limit: usize) ![]const u8 { 520 | var wrapped = try SimpleBuffer.SimpleU8.initSize(allocator, 0); 521 | const short_lines = str.len / limit; 522 | const extra_bytes = str.len % limit; 523 | var idx: usize = 0; 524 | while (idx < short_lines) : (idx += 1) { 525 | try wrapped.append(str[limit * idx .. limit * (idx + 1)]); 526 | try wrapped.append("\n"); 527 | } 528 | try wrapped.append(str[limit * idx .. (limit * idx) + extra_bytes]); 529 | return wrapped.toSliceConst(); 530 | } 531 | 532 | fn escapeGtkString(str: []const u8) []const u8 { 533 | const str_esc_null = c.g_markup_escape_text(str.ptr, @as(c_long, @intCast(str.len))); 534 | const str_esc = util.cstrToSlice(allocator, str_esc_null); 535 | return str_esc; 536 | } 537 | 538 | pub fn labelBufPrint(label: *c.GtkLabel, comptime format: []const u8, text: anytype) void { 539 | const str = std.fmt.allocPrint(allocator, format, text) catch unreachable; 540 | const cstr = util.sliceToCstr(allocator, str); 541 | c.gtk_label_set_text(label, @ptrCast(cstr)); 542 | } 543 | 544 | fn column_config_btn(columnptr: ?*anyopaque) callconv(.C) void { 545 | const columnbox = @as(*c.GtkWidget, @ptrCast(@alignCast(columnptr))); 546 | const column: *Column = findColumnByBox(columnbox); 547 | 548 | columnConfigWriteGui(column); 549 | 550 | c.gtk_widget_show(column.config_window); 551 | } 552 | 553 | fn findColumnByTootId(toot_id: []const u8) ?*Column { 554 | for (columns.items) |column| { 555 | const kvMaybe = column.guitoots.get(toot_id); 556 | if (kvMaybe) |_| { 557 | return column; 558 | } 559 | } 560 | return null; 561 | } 562 | 563 | fn findColumnByInfo(info: *config.ColumnInfo) *Column { 564 | for (columns.items) |col| { 565 | if (col.main == info) { 566 | return col; 567 | } 568 | } 569 | unreachable; 570 | } 571 | 572 | fn findColumnByBox(box: *c.GtkWidget) *Column { 573 | for (columns.items) |col| { 574 | if (col.columnbox == box) { 575 | return col; 576 | } 577 | } 578 | unreachable; 579 | } 580 | 581 | fn findColumnByConfigWindow(widget: *c.GtkWidget) *Column { 582 | const parent = c.gtk_widget_get_toplevel(widget); 583 | for (columns.items) |col| { 584 | if (col.config_window == parent) { 585 | return col; 586 | } 587 | } 588 | warn("Config window not found for widget {*} parent {*}\n", .{ widget, parent }); 589 | unreachable; 590 | } 591 | 592 | fn main_check_resize(selfptr: *anyopaque) callconv(.C) void { 593 | const self = @as(*c.GtkWidget, @ptrCast(@alignCast(selfptr))); 594 | var h: c.gint = undefined; 595 | var w: c.gint = undefined; 596 | c.gtk_window_get_size(@as(*c.GtkWindow, @ptrCast(self)), &w, &h); 597 | if (w != settings.win_x) { 598 | warn("main_check_resize() win_x {} != gtk_width {}\n", .{ settings.win_x, w }); 599 | settings.win_x = w; 600 | var verb = allocator.create(thread.CommandVerb) catch unreachable; 601 | verb.idle = undefined; 602 | var command = allocator.create(thread.Command) catch unreachable; 603 | command.id = 10; 604 | command.verb = verb; 605 | warn("main_check_resize() verb {*}\n", .{verb}); 606 | thread.signal(myActor, command); 607 | } 608 | if (h != settings.win_y) { 609 | warn("main_check_resize, win_y {} != gtk_height {}\n", .{ settings.win_x, w }); 610 | settings.win_y = h; 611 | var verb = allocator.create(thread.CommandVerb) catch unreachable; 612 | verb.idle = undefined; 613 | var command = allocator.create(thread.Command) catch unreachable; 614 | command.id = 10; 615 | command.verb = verb; 616 | thread.signal(myActor, command); 617 | } 618 | } 619 | 620 | fn actionbar_add() callconv(.C) void { 621 | warn("actionbar_add()", .{}); 622 | var verb = allocator.create(thread.CommandVerb) catch unreachable; 623 | verb.idle = undefined; 624 | var command = allocator.create(thread.Command) catch unreachable; 625 | command.id = 3; 626 | command.verb = verb; 627 | thread.signal(myActor, command); 628 | } 629 | 630 | fn zoot_drag() callconv(.C) void { 631 | warn("zoot_drag\n", .{}); 632 | } 633 | 634 | // rebulid half of GdkEventKey, avoiding bitfield 635 | const EventKey = packed struct { 636 | _type: i32, 637 | window: *c.GtkWindow, 638 | send_event: i8, 639 | time: u32, 640 | state: u32, 641 | keyval: u32, 642 | }; 643 | 644 | fn zoot_keypress(widgetptr: *anyopaque, evtptr: *EventKey) callconv(.C) void { 645 | _ = widgetptr; 646 | warn("zoot_keypress {}\n", .{evtptr}); 647 | } 648 | 649 | fn column_reload(columnptr: *anyopaque) callconv(.C) void { 650 | const column_widget = @as(*c.GtkWidget, @ptrCast(@alignCast(columnptr))); 651 | const column: *Column = findColumnByBox(column_widget); 652 | warn("column reload found {s}\n", .{column.main.config.title}); 653 | 654 | // signal crazy 655 | var command = allocator.create(thread.Command) catch unreachable; 656 | var verb = allocator.create(thread.CommandVerb) catch unreachable; 657 | verb.column = column.main; 658 | command.id = 2; 659 | command.verb = verb; 660 | thread.signal(myActor, command); 661 | } 662 | 663 | fn column_imgonly(columnptr: *anyopaque) callconv(.C) void { 664 | const column_widget = @as(*c.GtkWidget, @ptrCast(@alignCast(columnptr))); 665 | const column: *Column = findColumnByBox(column_widget); 666 | 667 | // signal crazy 668 | var command = allocator.create(thread.Command) catch unreachable; 669 | var verb = allocator.create(thread.CommandVerb) catch unreachable; 670 | verb.column = column.main; 671 | command.id = 9; //imgonly button press 672 | command.verb = verb; 673 | thread.signal(myActor, command); 674 | } 675 | 676 | fn column_remove_btn(selfptr: *anyopaque) callconv(.C) void { 677 | const self = @as(*c.GtkWidget, @ptrCast(@alignCast(selfptr))); 678 | const column: *Column = findColumnByConfigWindow(self); 679 | 680 | // signal crazy 681 | var command = allocator.create(thread.Command) catch unreachable; 682 | var verb = allocator.create(thread.CommandVerb) catch unreachable; 683 | verb.column = column.main; 684 | command.id = 5; // col remove button press 685 | command.verb = verb; 686 | thread.signal(myActor, command); 687 | } 688 | 689 | fn column_config_oauth_btn(selfptr: *anyopaque) callconv(.C) void { 690 | const self = @as(*c.GtkWidget, @ptrCast(@alignCast(selfptr))); 691 | var column: *Column = findColumnByConfigWindow(self); 692 | 693 | const oauth_box = builder_get_widget(column.builder, "column_config_oauth_box"); 694 | const host_box = builder_get_widget(column.builder, "column_config_host_box"); 695 | c.gtk_box_pack_end(@as(*c.GtkBox, @ptrCast(host_box)), oauth_box, 1, 0, 0); 696 | const oauth_label = builder_get_widget(column.builder, "column_config_oauth_label"); 697 | c.gtk_label_set_markup(@as(*c.GtkLabel, @ptrCast(oauth_label)), "contacting server..."); 698 | 699 | columnConfigReadGui(column); 700 | column.main.filter = filter_lib.parse(allocator, column.main.config.filter); 701 | 702 | // signal crazy 703 | var command = allocator.create(thread.Command) catch unreachable; 704 | var verb = allocator.create(thread.CommandVerb) catch unreachable; 705 | verb.column = column.main; 706 | command.id = 6; 707 | command.verb = verb; 708 | warn("column_config_oauth_btn cmd vrb {*}\n", .{command.verb.column}); 709 | thread.signal(myActor, command); 710 | } 711 | 712 | pub fn column_config_oauth_url(colInfo: *config.ColumnInfo) void { 713 | warn("gui.column_config_oauth_url {s}\n", .{colInfo.config.title}); 714 | //const container = builder_get_widget(myBuilder, "ZootColumns"); 715 | const column = findColumnByInfo(colInfo); 716 | 717 | //var oauth_box = builder_get_widget(column.builder, "column_config_oauth_box"); 718 | 719 | //const oauth_label = builder_get_widget(column.builder, "column_config_oauth_label"); 720 | var oauth_url_buf = SimpleBuffer.SimpleU8.initSize(allocator, 0) catch unreachable; 721 | oauth_url_buf.append("https://") catch unreachable; 722 | oauth_url_buf.append(column.main.filter.host()) catch unreachable; 723 | oauth_url_buf.append("/oauth/authorize") catch unreachable; 724 | oauth_url_buf.append("?client_id=") catch unreachable; 725 | oauth_url_buf.append(column.main.oauthClientId.?) catch unreachable; 726 | oauth_url_buf.append("&scope=read+write") catch unreachable; 727 | oauth_url_buf.append("&response_type=code") catch unreachable; 728 | oauth_url_buf.append("&redirect_uri=urn:ietf:wg:oauth:2.0:oob") catch unreachable; 729 | 730 | const oauth_label = builder_get_widget(column.builder, "column_config_oauth_label"); 731 | const markupBuf = allocator.alloc(u8, 512) catch unreachable; 732 | const markup = std.fmt.bufPrint(markupBuf, "{s} oauth", .{ oauth_url_buf.toSliceConst(), column.main.filter.host() }) catch unreachable; 733 | const cLabel = util.sliceToCstr(allocator, markup); 734 | c.gtk_label_set_markup(@ptrCast(oauth_label), cLabel); 735 | } 736 | 737 | fn column_config_oauth_activate(selfptr: *anyopaque) callconv(.C) void { 738 | const self = @as(*c.GtkWidget, @ptrCast(@alignCast(selfptr))); 739 | const column: *Column = findColumnByConfigWindow(self); 740 | 741 | const token_entry = builder_get_widget(column.builder, "column_config_authorization_entry"); 742 | const cAuthorization = c.gtk_entry_get_text(@as(*c.GtkEntry, @ptrCast(token_entry))); 743 | const authorization = util.cstrToSliceCopy(allocator, cAuthorization); 744 | 745 | // signal crazy 746 | var command = allocator.create(thread.Command) catch unreachable; 747 | var verb = allocator.create(thread.CommandVerb) catch unreachable; 748 | var auth = allocator.create(config.ColumnAuth) catch unreachable; 749 | auth.code = authorization; 750 | auth.column = column.main; 751 | verb.auth = auth; 752 | command.id = 7; 753 | command.verb = verb; 754 | thread.signal(myActor, command); 755 | } 756 | 757 | pub fn column_config_oauth_finalize(column: *Column) void { 758 | const oauth_box = builder_get_widget(column.builder, "column_config_oauth_box"); 759 | const host_box = builder_get_widget(column.builder, "column_config_host_box"); 760 | c.gtk_container_remove(@as(*c.GtkContainer, @ptrCast(host_box)), oauth_box); 761 | columnConfigWriteGui(column); 762 | update_column_ui(column); 763 | } 764 | 765 | pub fn columnConfigWriteGui(column: *Column) void { 766 | const url_entry = builder_get_widget(column.builder, "column_config_url_entry"); 767 | const cUrl = util.sliceToCstr(allocator, column.main.filter.host()); 768 | c.gtk_entry_set_text(@as(*c.GtkEntry, @ptrCast(url_entry)), cUrl); 769 | 770 | const token_image = builder_get_widget(column.builder, "column_config_token_image"); 771 | var icon_name: []const u8 = undefined; 772 | if (column.main.config.token) |_| { 773 | icon_name = "gtk-apply"; 774 | } else { 775 | icon_name = "gtk-close"; 776 | } 777 | c.gtk_image_set_from_icon_name(@as(*c.GtkImage, @ptrCast(token_image)), util.sliceToCstr(allocator, icon_name), c.GTK_ICON_SIZE_BUTTON); 778 | } 779 | 780 | pub fn columnReadFilter(column: *Column) []const u8 { 781 | const filter_entry = builder_get_widget(column.builder, "column_filter"); 782 | const cFilter = c.gtk_entry_get_text(@as(*c.GtkEntry, @ptrCast(filter_entry))); 783 | const filter = util.cstrToSliceCopy(allocator, cFilter); // edit in guithread-- 784 | warn("columnReadFilter: ({}){s} ({}){s}\n", .{ column.main.config.title.len, column.main.config.title, filter.len, filter }); 785 | return filter; 786 | } 787 | 788 | pub fn columnConfigReadGui(column: *Column) void { 789 | const url_entry = builder_get_widget(column.builder, "column_config_url_entry"); 790 | const cUrl = c.gtk_entry_get_text(@as(*c.GtkEntry, @ptrCast(url_entry))); 791 | const newFilter = util.cstrToSliceCopy(allocator, cUrl); // edit in guithread-- 792 | column.main.filter = filter_lib.parse(allocator, newFilter); 793 | } 794 | 795 | fn column_filter_done(selfptr: *anyopaque) callconv(.C) void { 796 | const self = @as(*c.GtkWidget, @ptrCast(@alignCast(selfptr))); 797 | var column: *Column = findColumnByBox(self); 798 | 799 | column.main.config.filter = columnReadFilter(column); 800 | column.main.filter = filter_lib.parse(allocator, column.main.config.filter); 801 | update_column_ui(column); 802 | 803 | // signal crazy 804 | var command = allocator.create(thread.Command) catch unreachable; 805 | var verb = allocator.create(thread.CommandVerb) catch unreachable; 806 | verb.column = column.main; 807 | command.id = 4; // save config 808 | command.verb = verb; 809 | thread.signal(myActor, command); 810 | 811 | // signal crazy 812 | command = allocator.create(thread.Command) catch unreachable; 813 | verb = allocator.create(thread.CommandVerb) catch unreachable; 814 | verb.column = column.main; 815 | command.id = 8; // update column UI 816 | command.verb = verb; 817 | thread.signal(myActor, command); 818 | } 819 | 820 | fn column_config_done(selfptr: *anyopaque) callconv(.C) void { 821 | const self = @as(*c.GtkWidget, @ptrCast(@alignCast(selfptr))); 822 | var column: *Column = findColumnByConfigWindow(self); 823 | 824 | columnConfigReadGui(column); 825 | column.main.filter = filter_lib.parse(allocator, column.main.config.filter); 826 | hide_column_config(column); 827 | 828 | // signal crazy 829 | var command = allocator.create(thread.Command) catch unreachable; 830 | var verb = allocator.create(thread.CommandVerb) catch unreachable; 831 | verb.column = column.main; 832 | command.id = 4; // save config 833 | command.verb = verb; 834 | thread.signal(myActor, command); 835 | // signal crazy 836 | command = allocator.create(thread.Command) catch unreachable; 837 | verb = allocator.create(thread.CommandVerb) catch unreachable; 838 | verb.column = column.main; 839 | command.id = 8; // update column UI 840 | command.verb = verb; 841 | thread.signal(myActor, command); 842 | } 843 | 844 | fn g_signal_connect(instance: anytype, signal_name: []const u8, callback: anytype, data: anytype) c.gulong { 845 | // pub extern fn g_signal_connect_data(instance: gpointer, 846 | // detailed_signal: [*]const gchar, 847 | // c_handler: GCallback, 848 | // data: gpointer, 849 | // destroy_data: GClosureNotify, 850 | // connect_flags: GConnectFlags) gulong; 851 | // connect_flags: GConnectFlags) gulong; 852 | // typedef void* gpointer; 853 | const signal_name_null: []const u8 = util.sliceAddNull(allocator, signal_name); 854 | const data_ptr: ?*anyopaque = data; 855 | const thing = @as(c.gpointer, @ptrCast(instance)); 856 | return c.g_signal_connect_data(thing, signal_name_null.ptr, @ptrCast(&callback), data_ptr, null, c.G_CONNECT_AFTER); 857 | } 858 | 859 | pub fn mainloop() bool { 860 | var stop = false; 861 | //warn("gtk pending {}\n", .{c.gtk_events_pending()}); 862 | //warn("gtk main level {}\n", .{c.gtk_main_level()}); 863 | const exitcode = c.gtk_main_iteration(); 864 | //warn("gtk main interaction return {}\n", .{exitcode}); 865 | //if(c.gtk_events_pending() != 0) { 866 | if (exitcode == 0) { 867 | stop = true; 868 | } 869 | return stop; 870 | } 871 | 872 | pub fn gtk_quit() callconv(.C) void { 873 | warn("gtk signal destroy called.\n", .{}); 874 | c.g_object_unref(myBuilder); 875 | var verb = allocator.create(thread.CommandVerb) catch unreachable; 876 | verb.idle = undefined; 877 | var command = allocator.create(thread.Command) catch unreachable; 878 | command.id = 11; 879 | command.verb = verb; 880 | thread.signal(myActor, command); 881 | } 882 | 883 | pub fn gui_end() void { 884 | warn("gui ended\n", .{}); 885 | } 886 | -------------------------------------------------------------------------------- /src/gui/libui.zig.disable: -------------------------------------------------------------------------------- 1 | // libui.zig 2 | const std = @import("std"); 3 | const builtin = @import("builtin"); 4 | const warn = std.debug.print; 5 | const Allocator = std.mem.Allocator; 6 | 7 | const thread = @import("../thread.zig"); 8 | const config = @import("../config.zig"); 9 | 10 | const c = @cImport({ 11 | @cInclude("stdio.h"); 12 | @cInclude("ui.h"); 13 | }); 14 | 15 | const GUIError = error{ Init, Setup }; 16 | var columnbox: *c.uiBox = undefined; 17 | 18 | pub const Column = struct { 19 | columnbox: [*c]c.uiControl, 20 | // config_window: [*c]c.GtkWidget, 21 | main: *config.ColumnInfo, 22 | }; 23 | 24 | var myActor: *thread.Actor = undefined; 25 | 26 | pub fn libname() []const u8 { 27 | return "libui"; 28 | } 29 | 30 | pub fn init(allocator: *Allocator, set: *config.Settings) !void { 31 | _ = allocator; 32 | _ = set; 33 | const tf = usize(1); 34 | if (tf != 1) return GUIError.Init; 35 | } 36 | 37 | pub fn gui_setup(actor: *thread.Actor) !void { 38 | myActor = actor; 39 | var uiInitOptions = c.uiInitOptions{ .Size = 0 }; 40 | const err = c.uiInit(&uiInitOptions); 41 | 42 | if (err == 0) { 43 | build(); 44 | } else { 45 | warn("libui init failed {!}\n", err); 46 | return GUIError.Init; 47 | } 48 | } 49 | 50 | fn build() void { 51 | const window = c.uiNewWindow("Zootdeck", 320, 240, 0); 52 | c.uiWindowSetMargined(window, 1); 53 | const f: ?fn (*c.uiWindow, *anyopaque) callconv(.C) c_int = onClosing; 54 | c.uiWindowOnClosing(window, f, null); 55 | 56 | const hbox = c.uiNewHorizontalBox(); 57 | c.uiWindowSetChild(window, @as(*c.uiControl, @ptrCast(@alignCast(hbox)))); 58 | //columnbox = @ptrCast(*c.uiControl, @alignCast(8, hbox)); 59 | if (hbox) |hb| { 60 | columnbox = hb; 61 | } 62 | 63 | const controls_vbox = c.uiNewVerticalBox(); 64 | c.uiBoxAppend(hbox, @as(*c.uiControl, @ptrCast(@alignCast(controls_vbox))), 0); 65 | 66 | const addButton = c.uiNewButton("+"); 67 | c.uiBoxAppend(controls_vbox, @as(*c.uiControl, @ptrCast(@alignCast(addButton))), 0); 68 | 69 | c.uiControlShow(@as(*c.uiControl, @ptrCast(@alignCast(window)))); 70 | } 71 | 72 | pub fn mainloop() void { 73 | c.uiMain(); 74 | } 75 | 76 | pub fn gui_end() void {} 77 | 78 | export fn onClosing(w: *c.uiWindow, data: *anyopaque) c_int { 79 | _ = w; 80 | _ = data; 81 | warn("ui quitting\n"); 82 | c.uiQuit(); 83 | return 1; 84 | } 85 | 86 | pub fn schedule(funcMaybe: ?fn (*anyopaque) callconv(.C) c_int, param: *anyopaque) void { 87 | _ = param; 88 | if (funcMaybe) |func| { 89 | warn("schedule FUNC {}\n", func); 90 | _ = func(@as(*anyopaque, @ptrCast(&"w"))); 91 | } 92 | } 93 | 94 | pub fn show_main_schedule(in: *anyopaque) callconv(.C) c_int { 95 | _ = in; 96 | return 0; 97 | } 98 | 99 | pub fn add_column_schedule(in: *anyopaque) callconv(.C) c_int { 100 | _ = in; 101 | warn("libui add column\n"); 102 | const column_vbox = c.uiNewVerticalBox(); // crashes here 103 | const url_label = c.uiNewLabel("site.xyz"); 104 | c.uiBoxAppend(column_vbox, @as(*c.uiControl, @ptrCast(@alignCast(url_label))), 0); 105 | 106 | c.uiBoxAppend(columnbox, @as(*c.uiControl, @ptrCast(@alignCast(column_vbox))), 0); 107 | return 0; 108 | } 109 | 110 | pub fn column_remove_schedule(in: *anyopaque) callconv(.C) c_int { 111 | _ = in; 112 | return 0; 113 | } 114 | 115 | pub fn column_config_oauth_url_schedule(in: *anyopaque) callconv(.C) c_int { 116 | _ = in; 117 | return 0; 118 | } 119 | 120 | pub fn update_column_config_oauth_finalize_schedule(in: *anyopaque) callconv(.C) c_int { 121 | _ = in; 122 | return 0; 123 | } 124 | 125 | pub fn update_column_ui_schedule(in: *anyopaque) callconv(.C) c_int { 126 | _ = in; 127 | return 0; 128 | } 129 | 130 | pub fn update_column_netstatus_schedule(in: *anyopaque) callconv(.C) c_int { 131 | _ = in; 132 | return 0; 133 | } 134 | 135 | pub fn update_column_toots_schedule(in: *anyopaque) callconv(.C) c_int { 136 | _ = in; 137 | return 0; 138 | } 139 | -------------------------------------------------------------------------------- /src/gui/qt.zig: -------------------------------------------------------------------------------- 1 | // GTK+ 2 | const std = @import("std"); 3 | const warn = std.debug.print; 4 | const Allocator = std.mem.Allocator; 5 | 6 | const thread = @import("../thread.zig"); 7 | const config = @import("../config.zig"); 8 | 9 | const c = @cImport({ 10 | @cInclude("QtWidgets/qapplication.h"); 11 | }); 12 | 13 | const GUIError = error{ Init, Setup }; 14 | var myActor: *thread.Actor = undefined; 15 | var app = c.qApp; 16 | 17 | pub const Column = struct { 18 | // builder: [*c]c.GtkBuilder, 19 | // columnbox: [*c]c.GtkWidget, 20 | // config_window: [*c]c.GtkWidget, 21 | main: *config.ColumnInfo, 22 | }; 23 | 24 | pub fn libname() []const u8 { 25 | return "qt5"; 26 | } 27 | 28 | pub fn init(alloc: Allocator, set: *config.Settings) !void { 29 | _ = alloc; 30 | _ = set; 31 | const tf = 1; 32 | if (tf != 1) return GUIError.Init; 33 | } 34 | 35 | pub fn gui_setup(actor: *thread.Actor) !void { 36 | myActor = actor; 37 | c.qApp(); 38 | return GUIError.Setup; 39 | } 40 | 41 | pub fn mainloop() void {} 42 | 43 | pub fn gui_end() void { 44 | warn("gui ended\n"); 45 | } 46 | 47 | pub fn schedule(func: ?fn (*anyopaque) callconv(.C) c_int, param: *anyopaque) void { 48 | _ = func; 49 | _ = param; 50 | } 51 | 52 | pub fn show_main_schedule(in: *anyopaque) callconv(.C) c_int { 53 | _ = in; 54 | return 0; 55 | } 56 | 57 | pub fn add_column_schedule(in: *anyopaque) callconv(.C) c_int { 58 | _ = in; 59 | return 0; 60 | } 61 | 62 | pub fn column_remove_schedule(in: *anyopaque) callconv(.C) c_int { 63 | _ = in; 64 | return 0; 65 | } 66 | 67 | pub fn column_config_oauth_url_schedule(in: *anyopaque) callconv(.C) c_int { 68 | _ = in; 69 | return 0; 70 | } 71 | 72 | pub fn update_column_config_oauth_finalize_schedule(in: *anyopaque) callconv(.C) c_int { 73 | _ = in; 74 | return 0; 75 | } 76 | 77 | pub fn update_column_ui_schedule(in: *anyopaque) callconv(.C) c_int { 78 | _ = in; 79 | return 0; 80 | } 81 | 82 | pub fn update_column_netstatus_schedule(in: *anyopaque) callconv(.C) c_int { 83 | _ = in; 84 | return 0; 85 | } 86 | 87 | pub fn update_column_toots_schedule(in: *anyopaque) callconv(.C) c_int { 88 | _ = in; 89 | return 0; 90 | } 91 | -------------------------------------------------------------------------------- /src/gui/theme.css: -------------------------------------------------------------------------------- 1 | .zootdeck_box { 2 | background-color: #111; 3 | } 4 | 5 | .column_box { color: #aaa; } 6 | .column_top_label { 7 | font-weight: bold; 8 | padding: 0.2em 0; 9 | color: #fff; 10 | } 11 | .column_filter { } 12 | .column_footer_count { margin: 0 1em; } 13 | .column_config_box { padding: 0 1em } 14 | .column_config_url_label { padding-top: 1em } 15 | .column_config_url_entry { } 16 | .column_config_remove { margin-top: 1em } 17 | 18 | 19 | .toot_author_name { 20 | color: white; 21 | background-color: #555; 22 | } 23 | .toot_author_url { 24 | color: #aaa; 25 | background-color: #111; 26 | } 27 | .toot_author_url_minimode { 28 | color: #aaa; 29 | background-color: #111; 30 | } 31 | .toot_author_avatar { 32 | padding: 0.1em; 33 | } 34 | .toot_text { 35 | color: white; 36 | font-family: sans-serif; 37 | } 38 | .toot_date { 39 | background: #111; 40 | color: #777; 41 | } 42 | .toot_tag { 43 | color: #333; 44 | background: #DDD; 45 | padding: 1px 5px; 46 | border-radius: 15px; 47 | } 48 | 49 | .net_error { 50 | background-color: #a00; 51 | } 52 | .net_active { 53 | background-color: #070; 54 | } 55 | 56 | -------------------------------------------------------------------------------- /src/gui/toot.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | True 7 | False 8 | vertical 9 | 10 | 11 | True 12 | False 13 | vertical 14 | 15 | 16 | True 17 | False 18 | vertical 19 | 20 | 21 | True 22 | False 23 | 24 | 25 | True 26 | False 27 | vertical 28 | 29 | 30 | True 31 | False 32 | gtk-missing-image 33 | 36 | 37 | 38 | False 39 | True 40 | 0 41 | 42 | 43 | 44 | 45 | False 46 | True 47 | 0 48 | 49 | 50 | 51 | 52 | True 53 | False 54 | vertical 55 | 56 | 57 | True 58 | False 59 | author_name 60 | True 61 | 64 | 65 | 66 | True 67 | True 68 | 0 69 | 70 | 71 | 72 | 73 | True 74 | False 75 | author url 76 | True 77 | 80 | 81 | 82 | False 83 | True 84 | 1 85 | 86 | 87 | 88 | 89 | True 90 | False 91 | toot text 92 | True 93 | 96 | 97 | 98 | True 99 | True 100 | 2 101 | 102 | 103 | 104 | 105 | True 106 | False 107 | 108 | 109 | False 110 | True 111 | 4 112 | 113 | 114 | 115 | 116 | True 117 | True 118 | 1 119 | 120 | 121 | 124 | 125 | 126 | False 127 | True 128 | 0 129 | 130 | 131 | 134 | 135 | 136 | False 137 | True 138 | 0 139 | 140 | 141 | 142 | 143 | False 144 | author url mini 145 | True 146 | 149 | 150 | 151 | False 152 | True 153 | 1 154 | 155 | 156 | 157 | 158 | True 159 | False 160 | vertical 161 | 162 | 163 | 164 | 165 | 166 | True 167 | True 168 | 2 169 | 170 | 171 | 172 | 173 | True 174 | False 175 | date 176 | True 177 | 180 | 181 | 182 | False 183 | True 184 | 3 185 | 186 | 187 | 188 | 189 | True 190 | True 191 | 0 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | True 203 | False 204 | 205 | 206 | False 207 | True 208 | 5 209 | 3 210 | 211 | 212 | 213 | 214 | -------------------------------------------------------------------------------- /src/gui/zootdeck.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | False 7 | GDK_BUTTON_PRESS_MASK | GDK_STRUCTURE_MASK 8 | Zootdeck 20250304 9 | ../img/zootlogo.png 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | True 18 | False 19 | vertical 20 | 21 | 22 | True 23 | False 24 | vertical 25 | 26 | 27 | 28 | 29 | 30 | False 31 | True 32 | 0 33 | 34 | 35 | 36 | 37 | True 38 | False 39 | 40 | 41 | True 42 | False 43 | vertical 44 | 45 | 46 | 47 | + 48 | True 49 | True 50 | True 51 | 52 | 53 | 54 | 55 | False 56 | True 57 | 0 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | False 69 | True 70 | 0 71 | 72 | 73 | 74 | 75 | True 76 | False 77 | True 78 | True 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 | 109 | 110 | 111 | True 112 | True 113 | 2 114 | 115 | 116 | 117 | 118 | True 119 | True 120 | 1 121 | 122 | 123 | 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /src/heartbeat.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const util = @import("./util.zig"); 3 | const warn = std.debug.print; 4 | const thread = @import("./thread.zig"); 5 | const Allocator = std.mem.Allocator; 6 | var allocator: Allocator = undefined; 7 | 8 | const c = @cImport({ 9 | @cInclude("unistd.h"); 10 | }); 11 | 12 | pub fn init(myAllocator: Allocator) !void { 13 | allocator = myAllocator; 14 | } 15 | 16 | pub fn go(actor_ptr: ?*anyopaque) callconv(.C) ?*anyopaque { 17 | const actor = @as(*thread.Actor, @ptrCast(@alignCast(actor_ptr))); 18 | util.log("heartbeat init()", .{}); 19 | const sleep_seconds = 60; 20 | while (true) { 21 | _ = c.usleep(sleep_seconds * 1000000); 22 | // signal crazy 23 | var command = allocator.create(thread.Command) catch unreachable; 24 | var verb = allocator.create(thread.CommandVerb) catch unreachable; 25 | verb.idle = 0; 26 | command.id = 3; 27 | command.verb = verb; 28 | thread.signal(actor, command); 29 | } 30 | return null; 31 | } 32 | -------------------------------------------------------------------------------- /src/html.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const warn = std.debug.print; 4 | const Allocator = std.mem.Allocator; 5 | 6 | const Node = c.GumboNode; 7 | 8 | const c = @cImport({ 9 | @cInclude("gumbo.h"); 10 | }); 11 | 12 | pub fn parse(html: []const u8) *Node { 13 | // const GumboOptions kGumboDefaultOptions = {&malloc_wrapper, &free_wrapper, NULL, 8, false, -1, GUMBO_TAG_LAST, GUMBO_NAMESPACE_HTML}; 14 | var options = c.GumboOptions{ 15 | .allocator = c.kGumboDefaultOptions.allocator, 16 | .deallocator = c.kGumboDefaultOptions.deallocator, 17 | .userdata = null, 18 | .tab_stop = 8, 19 | .stop_on_first_error = false, 20 | .max_errors = -1, 21 | .fragment_context = c.GUMBO_TAG_BODY, 22 | .fragment_namespace = c.GUMBO_NAMESPACE_HTML, 23 | }; 24 | const doc = c.gumbo_parse_with_options(&options, html.ptr, html.len); 25 | const root = doc.*.root; 26 | //var tagType = root.*.type; 27 | //var tagName = root.*.v.element.tag; 28 | return root; 29 | } 30 | 31 | pub fn search(node: *Node) void { 32 | if (node.type == c.GUMBO_NODE_ELEMENT) { 33 | if (node.v.element.tag == c.GUMBO_TAG_A) { 34 | //warn("A TAG found\n"); 35 | } 36 | const children = node.v.element.children; 37 | var idx = @as(u32, @intCast(0)); 38 | while (idx < children.length) : (idx += 1) { 39 | const cnode = children.data[idx]; 40 | if (cnode) |chld| { 41 | search(@as(*Node, @ptrCast(@alignCast(chld)))); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/ipc/epoll.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const warn = std.debug.print; 3 | const Allocator = std.mem.Allocator; 4 | 5 | const c = @cImport({ 6 | @cInclude("unistd.h"); 7 | @cInclude("errno.h"); 8 | @cInclude("sys/epoll.h"); 9 | }); 10 | 11 | pub const SocketType = c_int; 12 | pub const Client = packed struct { 13 | readEvent: *c.epoll_event, 14 | readSocket: SocketType, 15 | writeSocket: SocketType, 16 | }; 17 | 18 | var epoll_instance: c_int = undefined; 19 | 20 | pub fn init() !void { 21 | epoll_instance = c.epoll_create(256); 22 | if (epoll_instance == -1) { 23 | return error.BadValue; 24 | } 25 | } 26 | 27 | pub fn newClient(allocator: Allocator) *Client { 28 | const client = allocator.create(Client) catch unreachable; 29 | const fds = allocator.alloc(SocketType, 2) catch unreachable; 30 | _ = c.pipe(fds.ptr); 31 | client.readSocket = fds[0]; 32 | client.writeSocket = fds[1]; 33 | allocator.free(fds); 34 | client.readEvent = allocator.create(c.epoll_event) catch unreachable; 35 | return client; 36 | } 37 | 38 | pub fn close(client: *Client) void { 39 | _ = c.close(client.readSocket); 40 | _ = c.close(client.writeSocket); 41 | } 42 | 43 | //pub fn listen(socket: u8, url: []u8) void { 44 | // _ = socket; 45 | // warn("epoll_listen\n", .{}); 46 | //} 47 | 48 | pub fn register(client: *Client, callback: fn (?*anyopaque) callconv(.C) void) void { 49 | _ = callback; 50 | _ = client; 51 | } 52 | 53 | pub fn dial(client: *Client, url: []u8) void { 54 | _ = url; 55 | //.events = u32(c_int(c.EPOLL_EVENTS.EPOLLIN))|u32(c_int(c.EPOLL_EVENTS.EPOLLET)), 56 | // IN=1, OUT=4, ET=-1 57 | client.readEvent.events = 0x001; 58 | client.readEvent.data.ptr = client; 59 | _ = c.epoll_ctl(epoll_instance, c.EPOLL_CTL_ADD, client.readSocket, client.readEvent); 60 | } 61 | 62 | pub fn wait() *Client { 63 | const max_fds = 1; 64 | var events_waiting: [max_fds]c.epoll_event = undefined; //[]c.epoll_event{.data = 1}; 65 | var nfds: c_int = -1; 66 | while (nfds < 0) { 67 | nfds = c.epoll_wait(epoll_instance, @as([*c]c.epoll_event, @ptrCast(&events_waiting)), max_fds, -1); 68 | if (nfds < 0) { 69 | const errnoPtr: [*c]c_int = c.__errno_location(); 70 | const errno = errnoPtr.*; 71 | warn("epoll_wait ignoring errno {}\n", .{errno}); 72 | } 73 | } 74 | const client = @as(*Client, @ptrCast(@alignCast(events_waiting[0].data.ptr))); 75 | return client; 76 | } 77 | 78 | pub fn read(client: *Client, buf: []u8) []u8 { 79 | const pkt_fixed_portion = 1; 80 | const readCountOrErr = c.read(client.readSocket, buf.ptr, pkt_fixed_portion); 81 | if (readCountOrErr >= pkt_fixed_portion) { 82 | const msglen: usize = buf[0]; 83 | var msgrecv = @as(usize, @intCast(readCountOrErr - pkt_fixed_portion)); 84 | if (msgrecv < msglen) { 85 | const msgleft = msglen - msgrecv; 86 | const r2ce = c.read(client.readSocket, buf.ptr, msgleft); 87 | if (r2ce >= 0) { 88 | msgrecv += @as(usize, @intCast(r2ce)); 89 | } else { 90 | warn("epoll read #2 ERR\n", .{}); 91 | } 92 | } 93 | if (msgrecv == msglen) { 94 | return buf[0..msgrecv]; 95 | } else { 96 | return buf[0..0]; 97 | } 98 | } else { 99 | warn("epoll client read starved. tried {} got {} bytes\n", .{ pkt_fixed_portion, readCountOrErr }); 100 | return buf[0..0]; 101 | } 102 | } 103 | 104 | pub fn send(client: *Client, buf: []const u8) void { 105 | var len8: u8 = @intCast(buf.len); 106 | var writecount = c.write(client.writeSocket, &len8, 1); // send the fixed-size portion 107 | writecount = writecount + c.write(client.writeSocket, buf.ptr, buf.len); 108 | if (writecount != buf.len + 1) { 109 | warn("epoll client send underrun. buf+1= {} sent= {}\n", .{ buf.len + 1, writecount }); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/ipc/nng.zig: -------------------------------------------------------------------------------- 1 | // net.zig 2 | const std = @import("std"); 3 | const warn = std.debug.print; 4 | const Allocator = std.mem.Allocator; 5 | const std_allocator = std.heap.c_allocator; // passing through pthread nope 6 | const util = @import("../util.zig"); 7 | 8 | const c = @cImport({ 9 | @cInclude("unistd.h"); 10 | @cInclude("nng/nng.h"); 11 | @cInclude("nng/protocol/pair0/pair.h"); 12 | @cInclude("nng/transport/ipc/ipc.h"); 13 | }); 14 | 15 | pub const sock = c.nng_socket; 16 | 17 | pub const Client = struct { srv: *sock, clnt: *sock }; 18 | 19 | const Url = "ipc:///tmp/nng-pair-"; 20 | 21 | pub fn init() void {} 22 | 23 | pub fn listen(socket: *sock, url: []u8) void { 24 | warn("nng master listen {} {}\n", socket, url); 25 | if (c.nng_listen(socket.*, util.sliceToCstr(std_allocator, url), @as([*c]c.struct_nng_listener_s, @ptrFromInt(0)), 0) != 0) { 26 | warn("nng_listen FAIL\n"); 27 | } 28 | } 29 | 30 | pub fn wait(client: *Client, callback: fn (?*anyopaque) callconv(.C) void) void { 31 | // special nng alloc call 32 | var myAio: ?*c.nng_aio = undefined; 33 | warn("wait master nng_aio {*}\n", &myAio); 34 | var message = std_allocator.alloc(u8, 4) catch unreachable; 35 | message[0] = 'H'; 36 | message[1] = 2; 37 | message[2] = 1; 38 | message[3] = 0; 39 | _ = c.nng_aio_alloc(&myAio, callback, @as(?*anyopaque, @ptrCast(&message))); 40 | warn("wait master nng_aio post {*}\n", myAio); 41 | 42 | warn("wait master nng_recv {}\n", client.srv); 43 | c.nng_recv_aio(client.srv.*, myAio); 44 | } 45 | 46 | pub fn dial(socket: *sock, url: []u8) void { 47 | warn("nng dial {} {s}\n", socket, util.sliceToCstr(std_allocator, url)); 48 | if (c.nng_dial(socket.*, util.sliceToCstr(std_allocator, url), @as([*c]c.struct_nng_dialer_s, @ptrFromInt(0)), 0) != 0) { 49 | warn("nng_pair0_dial FAIL\n"); 50 | } 51 | } 52 | 53 | pub fn newClient(allocator: *Allocator) *Client { 54 | const client = allocator.create(Client) catch unreachable; 55 | const socket = allocator.create(sock) catch unreachable; 56 | client.srv = socket; 57 | var nng_ret = c.nng_pair0_open(client.srv); 58 | if (nng_ret != 0) { 59 | warn("nng_pair0_open FAIL {}\n", nng_ret); 60 | } 61 | listen(client.srv, Url); 62 | 63 | const socket2 = allocator.create(sock) catch unreachable; 64 | client.clnt = socket2; 65 | nng_ret = c.nng_pair0_open(client.clnt); 66 | if (nng_ret != 0) { 67 | warn("nng_pair0_open FAIL {}\n", nng_ret); 68 | } 69 | dial(client.clnt, Url); 70 | return client; 71 | } 72 | 73 | pub fn send(client: *Client) void { 74 | const payload = "X"; 75 | warn("nng send {} {}\n", client, payload); 76 | if (c.nng_send(client.clnt.*, util.sliceToCstr(std_allocator, payload), payload.len, 0) != 0) { 77 | warn("nng send to master FAIL\n"); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | // main.zig 2 | const std = @import("std"); 3 | const builtin = @import("builtin"); 4 | const warn = util.log; 5 | var GeneralPurposeAllocator = std.heap.GeneralPurposeAllocator(.{}){}; 6 | const alloc = GeneralPurposeAllocator.allocator(); // take the ptr in a separate step 7 | 8 | const oauth = @import("./oauth.zig"); 9 | const gui = @import("./gui.zig"); 10 | const net = @import("./net.zig"); 11 | const heartbeat = @import("./heartbeat.zig"); 12 | const config = @import("./config.zig"); 13 | const thread = @import("./thread.zig"); 14 | const db_kv = @import("./db/lmdb.zig"); 15 | const db_file = @import("./db/file.zig"); 16 | const statemachine = @import("./statemachine.zig"); 17 | const util = @import("./util.zig"); 18 | const toot_list = @import("./toot_list.zig"); 19 | const toot_lib = @import("./toot.zig"); 20 | const html_lib = @import("./html.zig"); 21 | const filter_lib = @import("./filter.zig"); 22 | 23 | var settings: *config.Settings = undefined; 24 | 25 | pub fn main() !void { 26 | try thread.init(alloc); 27 | hello(); // wait for thread.init so log entry for main thread will have a name 28 | try initialize(alloc); 29 | 30 | if (config.readfile(config.config_file_path())) |config_data| { 31 | settings = config_data; 32 | try gui.init(alloc, settings); 33 | const dummy_payload = try alloc.create(thread.CommandVerb); 34 | _ = try thread.create("gui", gui.go, dummy_payload, guiback); 35 | _ = try thread.create("heartbeat", heartbeat.go, dummy_payload, heartback); 36 | while (true) { 37 | stateNext(alloc); 38 | util.log("thread.wait()/epoll", .{}); 39 | thread.wait(); // main ipc listener 40 | } 41 | } else |err| { 42 | warn("config error: {!}", .{err}); 43 | } 44 | } 45 | 46 | fn hello() void { 47 | util.log("zootdeck {s} {s} zig {s}", .{ @tagName(builtin.os.tag), @tagName(builtin.cpu.arch), builtin.zig_version_string }); 48 | } 49 | 50 | fn initialize(allocator: std.mem.Allocator) !void { 51 | try config.init(allocator); 52 | try heartbeat.init(allocator); 53 | try db_kv.init(allocator); 54 | try db_file.init(allocator); 55 | try statemachine.init(); 56 | } 57 | 58 | fn stateNext(allocator: std.mem.Allocator) void { 59 | if (statemachine.state == .Init) { 60 | statemachine.setState(.Setup); // transition 61 | gui.schedule(gui.show_main_schedule, null); 62 | for (settings.columns.items) |column| { 63 | if (column.config.token) |token| { 64 | _ = token; 65 | profileget(column, allocator); 66 | } 67 | } 68 | for (settings.columns.items) |column| { 69 | gui.schedule(gui.add_column_schedule, column); 70 | } 71 | } 72 | 73 | if (statemachine.state == .Setup) { 74 | statemachine.setState(.Running); // transition 75 | columns_net_freshen(allocator); 76 | for (settings.columns.items) |column| { 77 | column_db_sync(column, allocator); 78 | } 79 | } 80 | } 81 | 82 | fn columnget(column: *config.ColumnInfo, allocator: std.mem.Allocator) void { 83 | var httpInfo = allocator.create(config.HttpInfo) catch unreachable; 84 | httpInfo.url = util.mastodonExpandUrl(column.filter.host(), column.config.token != null, allocator); 85 | httpInfo.verb = .get; 86 | httpInfo.token = null; 87 | if (column.config.token) |tokenStr| { 88 | httpInfo.token = tokenStr; 89 | } 90 | httpInfo.column = column; 91 | httpInfo.response_code = 0; 92 | var verb = allocator.create(thread.CommandVerb) catch unreachable; 93 | verb.http = httpInfo; 94 | gui.schedule(gui.update_column_netstatus_schedule, @as(*anyopaque, @ptrCast(httpInfo))); 95 | if (thread.create("net", net.go, verb, netback)) |_| {} else |_| { 96 | //warn("columnget {!}", .{err}); 97 | } 98 | } 99 | 100 | fn profileget(column: *config.ColumnInfo, allocator: std.mem.Allocator) void { 101 | var verb = allocator.create(thread.CommandVerb) catch unreachable; 102 | var httpInfo = allocator.create(config.HttpInfo) catch unreachable; 103 | httpInfo.url = std.fmt.allocPrint(allocator, "https://{s}/api/v1/accounts/verify_credentials", .{column.filter.host()}) catch unreachable; 104 | httpInfo.verb = .get; 105 | httpInfo.token = null; 106 | if (column.config.token) |tokenStr| { 107 | httpInfo.token = tokenStr; 108 | } 109 | httpInfo.column = column; 110 | httpInfo.response_code = 0; 111 | verb.http = httpInfo; 112 | gui.schedule(gui.update_column_netstatus_schedule, @as(*anyopaque, @ptrCast(httpInfo))); 113 | _ = thread.create("net", net.go, verb, profileback) catch unreachable; 114 | } 115 | 116 | fn photoget(toot: *toot_lib.Type, url: []const u8, allocator: std.mem.Allocator) void { 117 | var verb = allocator.create(thread.CommandVerb) catch unreachable; 118 | var httpInfo = allocator.create(config.HttpInfo) catch unreachable; 119 | httpInfo.url = url; 120 | httpInfo.verb = .get; 121 | httpInfo.token = null; 122 | httpInfo.response_code = 0; 123 | httpInfo.toot = toot; 124 | verb.http = httpInfo; 125 | _ = thread.create("net", net.go, verb, photoback) catch unreachable; 126 | } 127 | 128 | fn mediaget(column: *config.ColumnInfo, toot: *toot_lib.Type, media_id: ?[]const u8, url: []const u8, allocator: std.mem.Allocator) void { 129 | var verb = allocator.create(thread.CommandVerb) catch unreachable; 130 | verb.http = allocator.create(config.HttpInfo) catch unreachable; 131 | verb.http.url = url; 132 | verb.http.verb = .get; 133 | verb.http.token = null; 134 | verb.http.response_code = 0; 135 | verb.http.toot = toot; 136 | verb.http.column = column; 137 | verb.http.media_id = media_id; 138 | warn("mediaget toot #{s} toot {*} verb.http.toot {*}", .{ toot.id(), toot, verb.http.toot }); 139 | _ = thread.create("net", net.go, verb, mediaback) catch unreachable; 140 | } 141 | 142 | fn netback(command: *thread.Command) void { 143 | warn("*netback cmd#{}", .{command.id}); 144 | if (command.id == 1) { 145 | gui.schedule(gui.update_column_netstatus_schedule, @ptrCast(command.verb.http)); 146 | var column = command.verb.http.column; 147 | column.refreshing = false; 148 | column.last_check = config.now(); 149 | if (http_json_parse(command.verb.http)) |json_response_object| { 150 | const items = json_response_object.value.array.items; 151 | warn("netback adding {} toots to column {s}", .{ items.len, util.json_stringify(column.makeTitle()) }); 152 | cache_save(column, items); 153 | column_db_sync(column, alloc); 154 | } else |_| { 155 | column.inError = true; 156 | } 157 | } 158 | } 159 | 160 | fn http_json_parse(http: *config.HttpInfo) !std.json.Parsed(std.json.Value) { 161 | if (http.response_ok()) { 162 | if (http.body.len > 0) { 163 | if (http.content_type.len == 0 or http.content_type_json()) { 164 | if (std.json.parseFromSlice(std.json.Value, alloc, http.body, .{ .allocate = .alloc_always })) |json_parsed| { 165 | switch (json_parsed.value) { 166 | .array => return json_parsed, 167 | .object => { 168 | if (json_parsed.value.object.get("error")) |err| { 169 | warn("netback mastodon err {s}", .{err.string}); 170 | return error.MastodonReponseErr; 171 | } else { 172 | warn("netback mastodon unknown response {}", .{json_parsed.value.object}); 173 | return error.MastodonReponseErr; 174 | } 175 | }, 176 | else => { 177 | warn("!netback json unknown root tagtype {!}", .{json_parsed.value}); 178 | return error.JSONparse; 179 | }, 180 | } 181 | } else |err| { 182 | warn("net json parse err {!}", .{err}); 183 | http.response_code = 1000; 184 | return error.JSONparse; 185 | } 186 | } else { 187 | return error.HTTPContentNotJson; 188 | } 189 | } else { // empty body 190 | return error.JSONparse; 191 | } 192 | } else { 193 | return error.HTTPResponseNot2xx; 194 | } 195 | } 196 | 197 | fn column_db_sync(column: *config.ColumnInfo, allocator: std.mem.Allocator) void { 198 | const last_day = "9999-99-99"; 199 | const post_ids = db_kv.scan(db_kv.Key.init(&.{ "posts", column.filter.hostname, last_day }), true, allocator) catch unreachable; 200 | warn("column_db_sync {s} scan found {} items", .{ column.makeTitle(), post_ids.len }); 201 | for (post_ids) |id| { 202 | if (db_file.read(&.{ "posts", column.filter.hostname, id, "json" }, allocator)) |post_json| { 203 | const parsed = std.json.parseFromSlice(std.json.Value, allocator, post_json, .{}) catch unreachable; 204 | const toot: *toot_lib.Type = toot_lib.Type.init(parsed.value, allocator); 205 | if (!column.toots.contains(toot)) { 206 | column.toots.sortedInsert(toot, alloc); 207 | if (toot.get("media_attachments")) |images| { // media is not cached (yet), fetch now 208 | media_attachments(column, toot, images.array); 209 | } 210 | warn("column_db_sync inserted {*} #{s} count {}", .{ toot, toot.id(), column.toots.count() }); 211 | const acct = toot.acct() catch unreachable; 212 | if (db_file.has(&.{ "accounts", acct }, "photo", allocator)) { 213 | const cAcct = util.sliceToCstr(alloc, acct); 214 | gui.schedule(gui.update_author_photo_schedule, @ptrCast(cAcct)); 215 | } 216 | } else { 217 | warn("column_db_sync ignored dupe #{s}", .{toot.id()}); 218 | } 219 | } else |err| { 220 | warn("!! column_db_sync file read error {}", .{err}); 221 | } 222 | } 223 | gui.schedule(gui.update_column_toots_schedule, @ptrCast(column)); 224 | } 225 | 226 | fn cache_save(column: *config.ColumnInfo, items: []std.json.Value) void { 227 | column.inError = false; 228 | warn("cache_load parsed count {} adding to {s}", .{ items.len, column.makeTitle() }); 229 | for (items) |json_value| { 230 | const toot = toot_lib.Type.init(json_value, alloc); 231 | cache_write_post(column, toot, alloc); 232 | } 233 | } 234 | 235 | fn media_attachments(column: *config.ColumnInfo, toot: *toot_lib.Type, images: std.json.Array) void { 236 | for (images.items) |image| { 237 | const img_url = image.object.get("preview_url").?.string; 238 | const img_id = image.object.get("id").?.string; 239 | warn("toot #{s} has media #{s} {s}", .{ toot.id(), img_id, img_url }); 240 | if (!toot.containsImg(img_id)) { 241 | const hostname = column.filter.hostname; 242 | if (db_file.has(&.{ "posts", hostname, toot.id(), "images" }, img_id, alloc)) { 243 | if (db_file.read(&.{ "posts", hostname, toot.id(), "images", img_id }, alloc)) |img_bytes| { 244 | const tootpic = alloc.create(gui.TootPic) catch unreachable; 245 | tootpic.toot = toot; 246 | const img = toot_lib.Img{ .id = img_id, .url = img_url, .bytes = img_bytes }; 247 | tootpic.img = img; 248 | toot.addImg(img); 249 | gui.schedule(gui.toot_media_schedule, @as(*anyopaque, @ptrCast(tootpic))); 250 | } else |err| { 251 | warn("!!media_attachments file read error {}", .{err}); 252 | } 253 | } else { 254 | // mediaget(column, toot, img_id, img_url, alloc); 255 | } 256 | } 257 | } 258 | } 259 | 260 | fn mediaback(command: *thread.Command) void { 261 | thread.destroy(command.actor); // TODO: thread one-shot 262 | const reqres = command.verb.http; 263 | if (db_file.write(&.{ "posts", reqres.column.filter.hostname, reqres.toot.id(), "images" }, reqres.media_id.?, reqres.body, alloc)) |filename| { 264 | warn("mediaback db_file wrote {s}", .{filename}); 265 | } else |err| { 266 | warn("mediaback write {}", .{err}); 267 | } 268 | 269 | const tootpic = alloc.create(gui.TootPic) catch unreachable; 270 | tootpic.toot = reqres.toot; 271 | const url_ram = alloc.dupe(u8, reqres.url) catch unreachable; 272 | const body_ram = alloc.dupe(u8, reqres.body) catch unreachable; 273 | const img = toot_lib.Img{ .id = reqres.media_id.?, .url = url_ram, .bytes = body_ram }; 274 | tootpic.img = img; 275 | warn("mediaback toot #{s} tootpic.toot {*} adding 1 img", .{ tootpic.toot.id(), tootpic.toot }); 276 | tootpic.toot.addImg(img); 277 | gui.schedule(gui.toot_media_schedule, @as(*anyopaque, @ptrCast(tootpic))); 278 | } 279 | 280 | fn photoback(command: *thread.Command) void { 281 | thread.destroy(command.actor); // TODO: thread one-shot 282 | const reqres = command.verb.http; 283 | var account = reqres.toot.get("account").?.object; 284 | const acct = account.get("acct").?.string; 285 | warn("photoback! acct {s} type {s} size {}", .{ acct, reqres.content_type, reqres.body.len }); 286 | const filename = db_file.write(&.{ "accounts", acct }, "photo", reqres.body, alloc) catch unreachable; 287 | warn("photoback wrote {s}", .{filename}); 288 | const cAcct = util.sliceToCstr(alloc, acct); 289 | gui.schedule(gui.update_author_photo_schedule, @as(*anyopaque, @ptrCast(cAcct))); 290 | } 291 | 292 | fn profileback(command: *thread.Command) void { 293 | thread.destroy(command.actor); // TODO: thread one-shot 294 | const reqres = command.verb.http; 295 | if (reqres.response_code >= 200 and reqres.response_code < 300) { 296 | reqres.column.account = reqres.tree.value.object; 297 | gui.schedule(gui.update_column_ui_schedule, @as(*anyopaque, @ptrCast(reqres.column))); 298 | } else { 299 | //warn("profile fail http status {!}", .{reqres.response_code}); 300 | } 301 | } 302 | 303 | fn cache_write_post(column: *config.ColumnInfo, toot: *toot_lib.Type, allocator: std.mem.Allocator) void { 304 | var account = toot.get("account").?.object; 305 | const host = column.filter.hostname; 306 | 307 | // index post by host and date 308 | const toot_created_at = toot.get("created_at").?.string; 309 | const posts_host_date = util.strings_join_separator(&.{ "posts", host }, ':', allocator); // todo 310 | db_kv.write(posts_host_date, toot_created_at, toot.id(), allocator) catch unreachable; 311 | // save post json 312 | const json = util.json_stringify(toot.hashmap); 313 | if (db_file.write(&.{ "posts", host, toot.id() }, "json", json, alloc)) |_| {} else |_| {} 314 | 315 | // index avatar url 316 | const avatar_url: []const u8 = account.get("avatar").?.string; 317 | const toot_acct = toot.acct() catch unreachable; 318 | const photos_acct = std.fmt.allocPrint(allocator, "photos:{s}", .{toot_acct}) catch unreachable; 319 | db_kv.write(photos_acct, "url", avatar_url, allocator) catch unreachable; 320 | 321 | // index display name 322 | const name: []const u8 = account.get("display_name").?.string; 323 | db_kv.write(photos_acct, "name", name, allocator) catch unreachable; 324 | 325 | if (!db_file.has(&.{ "accounts", toot_acct }, "photo", allocator)) { 326 | warn("cache_write_post photoget {s}", .{avatar_url}); 327 | photoget(toot, avatar_url, allocator); 328 | } 329 | 330 | if (toot.get("media_attachments")) |images| { 331 | for (images.array.items) |image| { 332 | const img_id = image.object.get("id").?.string; 333 | const img_url = image.object.get("preview_url").?.string; 334 | if (!db_file.has(&.{ "posts", host, toot.id(), "images" }, img_id, alloc)) { 335 | mediaget(column, toot, img_id, img_url, alloc); 336 | } 337 | } 338 | } 339 | } 340 | 341 | fn guiback(command: *thread.Command) void { 342 | warn("guiback cmd#{}", .{command.id}); 343 | if (command.id == 1) { 344 | var ram = alloc.alloc(u8, 1) catch unreachable; 345 | ram[0] = 1; 346 | gui.schedule(gui.show_main_schedule, @ptrCast(&ram)); 347 | } 348 | if (command.id == 2) { // refresh button 349 | const column = command.verb.column; 350 | column.inError = false; 351 | column.refreshing = false; 352 | column_refresh(column, alloc); 353 | } 354 | if (command.id == 3) { // add column 355 | var colInfo = alloc.create(config.ColumnInfo) catch unreachable; 356 | _ = colInfo.reset(); 357 | colInfo.toots = toot_list.TootList.init(); 358 | colInfo.last_check = 0; 359 | settings.columns.append(colInfo) catch unreachable; 360 | warn("add column: settings.columns.len {}", .{settings.columns.items.len}); 361 | const colConfig = alloc.create(config.ColumnConfig) catch unreachable; 362 | colInfo.config = colConfig.reset(); 363 | colInfo.filter = filter_lib.parse(alloc, colInfo.config.filter); 364 | gui.schedule(gui.add_column_schedule, @as(*anyopaque, @ptrCast(colInfo))); 365 | config.writefile(settings, config.config_file_path()); 366 | } 367 | if (command.id == 4) { // save config params 368 | const column = command.verb.column; 369 | warn("guiback save config column title: ({d}){s}", .{ column.config.title.len, column.config.title }); 370 | column.inError = false; 371 | column.refreshing = false; 372 | config.writefile(settings, config.config_file_path()); 373 | } 374 | if (command.id == 5) { // column remove 375 | const column = command.verb.column; 376 | warn("gui col remove {s}", .{column.config.title}); 377 | //var colpos: usize = undefined; 378 | for (settings.columns.items, 0..) |col, idx| { 379 | if (col == column) { 380 | _ = settings.columns.orderedRemove(idx); 381 | break; 382 | } 383 | } 384 | config.writefile(settings, config.config_file_path()); 385 | gui.schedule(gui.column_remove_schedule, @as(*anyopaque, @ptrCast(column))); 386 | } 387 | if (command.id == 6) { //oauth 388 | const column = command.verb.column; 389 | if (column.oauthClientId) |id| { 390 | _ = id; 391 | gui.schedule(gui.column_config_oauth_url_schedule, @as(*anyopaque, @ptrCast(column))); 392 | } else { 393 | oauthcolumnget(column, alloc); 394 | } 395 | } 396 | if (command.id == 7) { //oauth activate 397 | const myAuth = command.verb.auth.*; 398 | warn("oauth authorization {s}", .{myAuth.code}); 399 | oauthtokenget(myAuth.column, myAuth.code, alloc); 400 | } 401 | if (command.id == 8) { //column config changed 402 | const column = command.verb.column; 403 | warn("guiback: column config changed for column title ({d}){s}", .{ column.config.title.len, column.config.title }); 404 | // partial reset 405 | column.oauthClientId = null; 406 | column.oauthClientSecret = null; 407 | gui.schedule(gui.update_column_ui_schedule, @as(*anyopaque, @ptrCast(column))); 408 | gui.schedule(gui.update_column_toots_schedule, @as(*anyopaque, @ptrCast(column))); 409 | // throw out toots in the toot list not from the new host 410 | column_refresh(column, alloc); 411 | } 412 | if (command.id == 9) { // image-only button 413 | const column = command.verb.column; 414 | column.config.img_only = !column.config.img_only; 415 | config.writefile(settings, config.config_file_path()); 416 | gui.schedule(gui.update_column_toots_schedule, @as(*anyopaque, @ptrCast(column))); 417 | } 418 | if (command.id == 10) { // window size changed 419 | config.writefile(settings, config.config_file_path()); 420 | } 421 | if (command.id == 11) { // Quit 422 | warn("byebye...", .{}); 423 | std.posix.exit(0); 424 | } 425 | } 426 | 427 | fn heartback(command: *thread.Command) void { 428 | warn("heartback() on tid {} received {}", .{ thread.self(), command.verb }); 429 | //columns_net_freshen(alloc); 430 | } 431 | 432 | fn columns_net_freshen(allocator: std.mem.Allocator) void { 433 | warn("columns_net_freshen", .{}); 434 | for (settings.columns.items) |column| { 435 | const now = config.now(); 436 | const refresh = 60; 437 | const since = now - column.last_check; 438 | if (since > refresh) { 439 | column_refresh(column, allocator); 440 | } else { 441 | //warn("col {} is fresh for {} sec", column.makeTitle(), refresh-since); 442 | } 443 | } 444 | } 445 | 446 | fn column_refresh(column: *config.ColumnInfo, allocator: std.mem.Allocator) void { 447 | if (column.refreshing) { 448 | warn("column {s} in {s} Ignoring request.", .{ column.config.title, if (column.inError) @as([]const u8, "error!") else @as([]const u8, "progress.") }); 449 | } else { 450 | warn("column_refresh http get for title: {s}", .{util.json_stringify(column.makeTitle())}); 451 | column.refreshing = true; 452 | columnget(column, allocator); 453 | } 454 | } 455 | 456 | fn oauthcolumnget(column: *config.ColumnInfo, allocator: std.mem.Allocator) void { 457 | var verb = allocator.create(thread.CommandVerb) catch unreachable; 458 | var httpInfo = allocator.create(config.HttpInfo) catch unreachable; 459 | oauth.clientRegisterUrl(allocator, httpInfo, column.filter.host()); 460 | httpInfo.token = null; 461 | httpInfo.column = column; 462 | httpInfo.response_code = 0; 463 | httpInfo.verb = .post; 464 | verb.http = httpInfo; 465 | gui.schedule(gui.update_column_netstatus_schedule, @as(*anyopaque, @ptrCast(httpInfo))); 466 | _ = thread.create("net", net.go, verb, oauthback) catch unreachable; 467 | } 468 | 469 | fn oauthtokenget(column: *config.ColumnInfo, code: []const u8, allocator: std.mem.Allocator) void { 470 | var verb = allocator.create(thread.CommandVerb) catch unreachable; 471 | var httpInfo = allocator.create(config.HttpInfo) catch unreachable; 472 | oauth.tokenUpgradeUrl(allocator, httpInfo, column.filter.host(), code, column.oauthClientId.?, column.oauthClientSecret.?); 473 | httpInfo.token = null; 474 | httpInfo.column = column; 475 | httpInfo.response_code = 0; 476 | httpInfo.verb = .post; 477 | verb.http = httpInfo; 478 | gui.schedule(gui.update_column_netstatus_schedule, @as(*anyopaque, @ptrCast(httpInfo))); 479 | _ = thread.create("net", net.go, verb, oauthtokenback) catch unreachable; 480 | } 481 | 482 | fn oauthtokenback(command: *thread.Command) void { 483 | //warn("*oauthtokenback tid {x} {}", .{ thread.self(), command }); 484 | const column = command.verb.http.column; 485 | const http = command.verb.http; 486 | if (http.response_code >= 200 and http.response_code < 300) { 487 | if (std.json.parseFromSlice(std.json.Value, command.actor.allocator, http.body, .{ .allocate = .alloc_always })) |json_parsed| { 488 | if (json_parsed.value == .object) { 489 | if (json_parsed.value.object.get("access_token")) |cid| { 490 | column.config.token = cid.string; 491 | config.writefile(settings, config.config_file_path()); 492 | column.last_check = 0; 493 | profileget(column, alloc); 494 | gui.schedule(gui.update_column_config_oauth_finalize_schedule, @as(*anyopaque, @ptrCast(column))); 495 | } 496 | } else { 497 | warn("*oauthtokenback json err body {s}", .{http.body}); 498 | } 499 | } else |err| { 500 | warn("oauthtokenback json parse err {}", .{err}); 501 | } 502 | } else { 503 | warn("oauthtokenback net err {d}", .{http.response_code}); 504 | } 505 | } 506 | 507 | fn oauthback(command: *thread.Command) void { 508 | //warn("*oauthback tid {x} {}", .{ thread.self(), command }); 509 | const column = command.verb.http.column; 510 | const http = command.verb.http; 511 | if (http.response_code >= 200 and http.response_code < 300) { 512 | if (std.json.parseFromSlice(std.json.Value, command.actor.allocator, http.body, .{ .allocate = .alloc_always })) |json_parsed| { 513 | if (json_parsed.value == .object) { 514 | if (json_parsed.value.object.get("client_id")) |cid| { 515 | column.oauthClientId = cid.string; 516 | } 517 | if (json_parsed.value.object.get("client_secret")) |cid| { 518 | column.oauthClientSecret = cid.string; 519 | } 520 | //warn("*oauthback client id {s} secret {s}", .{ column.oauthClientId, column.oauthClientSecret }); 521 | gui.schedule(gui.column_config_oauth_url_schedule, @as(*anyopaque, @ptrCast(column))); 522 | } else { 523 | warn("*oauthback json type err {} {s}", .{ json_parsed.value, http.body }); 524 | } 525 | } else |err| { 526 | warn("oauthback json parse err {}", .{err}); 527 | } 528 | } else { 529 | warn("*oauthback net err {}", .{http.response_code}); 530 | } 531 | } 532 | -------------------------------------------------------------------------------- /src/net.zig: -------------------------------------------------------------------------------- 1 | // net.zig 2 | const std = @import("std"); 3 | const thread = @import("./thread.zig"); 4 | 5 | const config = @import("./config.zig"); 6 | const util = @import("./util.zig"); 7 | 8 | const warn = util.log; 9 | 10 | const c = @cImport({ 11 | @cInclude("unistd.h"); 12 | @cInclude("pthread.h"); 13 | @cInclude("curl/curl.h"); 14 | }); 15 | 16 | const NetError = error{ JSONparse, Curl, CurlInit, DNS }; 17 | 18 | pub fn go(data: ?*anyopaque) callconv(.C) ?*anyopaque { 19 | var actor = @as(*thread.Actor, @ptrCast(@alignCast(data))); 20 | //warn("net thread start {*} {}\n", actor, actor); 21 | 22 | // setup for the callback 23 | var command = actor.allocator.create(thread.Command) catch unreachable; 24 | command.id = 1; 25 | command.verb = actor.payload; 26 | 27 | if (httpget(actor.allocator, actor.payload.http)) |body| { 28 | actor.payload.http.body = body; 29 | } else |err| { 30 | warn("net.go http {!} #{}", .{ err, actor.payload.http.response_code }); 31 | } 32 | thread.signal(actor, command); 33 | return null; 34 | } 35 | 36 | pub fn httpget(allocator: std.mem.Allocator, req: *config.HttpInfo) ![]const u8 { 37 | _ = c.curl_global_init(0); 38 | const curl = c.curl_easy_init(); 39 | if (curl != null) { 40 | const cstr = util.sliceAddNull(allocator, req.url); 41 | _ = c.curl_easy_setopt(curl, c.CURLOPT_URL, cstr.ptr); 42 | 43 | const zero: c_long = 0; 44 | const seconds: c_long = 30; 45 | _ = c.curl_easy_setopt(curl, c.CURLOPT_CONNECTTIMEOUT, seconds); 46 | _ = c.curl_easy_setopt(curl, c.CURLOPT_SSL_VERIFYPEER, zero); 47 | _ = c.curl_easy_setopt(curl, c.CURLOPT_SSL_VERIFYHOST, zero); 48 | _ = c.curl_easy_setopt(curl, c.CURLOPT_WRITEFUNCTION, curl_write); 49 | var body_buffer = std.ArrayList(u8).init(allocator); 50 | _ = c.curl_easy_setopt(curl, c.CURLOPT_WRITEDATA, &body_buffer); 51 | 52 | //var slist: ?[*c]c.curl_slist = null; 53 | var slist = @as([*c]c.curl_slist, @ptrFromInt(0)); // 0= new list 54 | slist = c.curl_slist_append(slist, "Accept: application/json"); 55 | if (req.token) |_| { 56 | //warn("Authorization: {s}\n", .{token}); 57 | } 58 | _ = c.curl_easy_setopt(curl, c.CURLOPT_HTTPHEADER, slist); 59 | 60 | switch (req.verb) { 61 | .get => _ = c.curl_easy_setopt(curl, c.CURLOPT_HTTPGET, @as(c_long, 1)), 62 | .post => { 63 | _ = c.curl_easy_setopt(curl, c.CURLOPT_POST, @as(c_long, 1)); 64 | const post_body_c: [*c]const u8 = util.sliceToCstr(allocator, req.post_body); 65 | _ = c.curl_easy_setopt(curl, c.CURLOPT_POSTFIELDS, post_body_c); 66 | //warn("post body: {s}\n", .{req.post_body}); 67 | }, 68 | } 69 | 70 | const res = c.curl_easy_perform(curl); 71 | defer c.curl_easy_cleanup(curl); 72 | if (res == c.CURLE_OK) { 73 | _ = c.curl_easy_getinfo(curl, c.CURLINFO_RESPONSE_CODE, &req.response_code); 74 | var ccontent_type: [*c]const u8 = undefined; 75 | _ = c.curl_easy_getinfo(curl, c.CURLINFO_CONTENT_TYPE, &ccontent_type); 76 | req.content_type = util.cstrToSliceCopy(allocator, ccontent_type); 77 | util.log("# {s} {s} {} bytes", .{ 78 | req.url, 79 | req.content_type, 80 | body_buffer.items.len, 81 | }); 82 | return body_buffer.toOwnedSliceSentinel(0); 83 | } else if (res == c.CURLE_OPERATION_TIMEDOUT) { 84 | req.response_code = 2200; 85 | return NetError.Curl; 86 | } else { 87 | //const err_cstr = c.curl_easy_strerror(res); 88 | //warn("curl ERR {!} {s}\n", .{ res, util.cstrToSliceCopy(allocator, err_cstr) }); 89 | if (res == c.CURLE_COULDNT_RESOLVE_HOST) { 90 | req.response_code = 2100; 91 | return NetError.DNS; 92 | } else { 93 | req.response_code = 2000; 94 | warn("net.go unknown curl result code {}", .{res}); 95 | return NetError.Curl; 96 | } 97 | } 98 | } else { 99 | //warn("net curl easy init fail\n", .{}); 100 | return NetError.CurlInit; 101 | } 102 | } 103 | 104 | pub fn curl_write(ptr: [*c]const u8, _: usize, nmemb: usize, userdata: *anyopaque) callconv(.C) usize { 105 | var buf = @as(*std.ArrayList(u8), @ptrCast(@alignCast(userdata))); 106 | const body_part: []const u8 = ptr[0..nmemb]; 107 | buf.appendSlice(body_part) catch {}; 108 | return nmemb; 109 | } 110 | -------------------------------------------------------------------------------- /src/oauth.zig: -------------------------------------------------------------------------------- 1 | // auth.zig 2 | const std = @import("std"); 3 | const Allocator = std.mem.Allocator; 4 | const SimpleBuffer = @import("./simple_buffer.zig"); 5 | const config = @import("./config.zig"); 6 | 7 | pub fn clientRegisterUrl(allocator: Allocator, httpInfo: *config.HttpInfo, url: []const u8) void { 8 | var urlBuf = SimpleBuffer.SimpleU8.initSize(allocator, 0) catch unreachable; 9 | urlBuf.append("https://") catch unreachable; 10 | urlBuf.append(url) catch unreachable; 11 | urlBuf.append("/api/v1/apps") catch unreachable; 12 | httpInfo.url = urlBuf.toSliceConst(); 13 | var postBodyBuf = SimpleBuffer.SimpleU8.initSize(allocator, 0) catch unreachable; 14 | postBodyBuf.append("client_name=zootdeck") catch unreachable; 15 | postBodyBuf.append("&scopes=read+write") catch unreachable; 16 | postBodyBuf.append("&redirect_uris=urn:ietf:wg:oauth:2.0:oob") catch unreachable; 17 | httpInfo.post_body = postBodyBuf.toSliceConst(); 18 | } 19 | 20 | pub fn tokenUpgradeUrl(allocator: Allocator, httpInfo: *config.HttpInfo, url: []const u8, code: []const u8, clientId: []const u8, clientSecret: []const u8) void { 21 | var urlBuf = SimpleBuffer.SimpleU8.initSize(allocator, 0) catch unreachable; 22 | urlBuf.append("https://") catch unreachable; 23 | urlBuf.append(url) catch unreachable; 24 | urlBuf.append("/oauth/token") catch unreachable; 25 | httpInfo.url = urlBuf.toSliceConst(); 26 | var postBodyBuf = SimpleBuffer.SimpleU8.initSize(allocator, 0) catch unreachable; 27 | postBodyBuf.append("client_id=") catch unreachable; 28 | postBodyBuf.append(clientId) catch unreachable; 29 | postBodyBuf.append("&client_secret=") catch unreachable; 30 | postBodyBuf.append(clientSecret) catch unreachable; 31 | postBodyBuf.append("&grant_type=authorization_code") catch unreachable; 32 | postBodyBuf.append("&code=") catch unreachable; 33 | postBodyBuf.append(code) catch unreachable; 34 | postBodyBuf.append("&redirect_uri=urn:ietf:wg:oauth:2.0:oob") catch unreachable; 35 | httpInfo.post_body = postBodyBuf.toSliceConst(); 36 | } 37 | -------------------------------------------------------------------------------- /src/simple_buffer.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const SimpleU8 = struct { 4 | list: std.ArrayList(u8), 5 | 6 | pub fn initSize(allocator: std.mem.Allocator, size: usize) !SimpleU8 { 7 | var self = SimpleU8{ .list = std.ArrayList(u8).init(allocator) }; 8 | try self.resize(size); 9 | return self; 10 | } 11 | 12 | pub fn len(self: *const SimpleU8) usize { 13 | return self.list.items.len; 14 | } 15 | 16 | pub fn resize(self: *SimpleU8, new_len: usize) !void { 17 | try self.list.resize(new_len); 18 | } 19 | 20 | pub fn toSliceConst(self: *const SimpleU8) []const u8 { 21 | return self.list.items[0..self.len()]; 22 | } 23 | 24 | pub fn append(self: *SimpleU8, m: []const u8) !void { 25 | const old_len = self.len(); 26 | try self.resize(old_len + m.len); 27 | std.mem.copyForwards(u8, self.list.items[old_len..], m); 28 | } 29 | 30 | pub fn appendByte(self: *SimpleU8, byte: u8) !void { 31 | const old_len = self.len(); 32 | try self.resize(old_len + 1); 33 | self.list.items[old_len] = byte; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/statemachine.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const util = @import("./util.zig"); 3 | const warn = util.log; 4 | const Allocator = std.mem.Allocator; 5 | var allocator: Allocator = undefined; 6 | 7 | pub const States = enum { 8 | Init, 9 | Setup, 10 | Running, 11 | }; 12 | 13 | pub var state: States = undefined; 14 | 15 | pub fn init() !void { 16 | setState(States.Init); 17 | if (state != States.Init) return error.StatemachineSetupFail; 18 | } 19 | 20 | pub fn needNetRefresh() bool { 21 | if (state == States.Setup) { 22 | setState(States.Running); 23 | return true; 24 | } else { 25 | return false; 26 | } 27 | } 28 | 29 | pub fn setState(new_state: States) void { 30 | state = new_state; 31 | warn("STATE: {s}", .{@tagName(state)}); 32 | } 33 | -------------------------------------------------------------------------------- /src/tests.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const util = @import("util.zig"); 3 | const lmdb = @import("db/lmdb.zig"); 4 | 5 | test "modules" { 6 | _ = lmdb; 7 | // std.testing.refAllDecls(@This); 8 | } 9 | -------------------------------------------------------------------------------- /src/thread.zig: -------------------------------------------------------------------------------- 1 | // thread.zig 2 | const std = @import("std"); 3 | const warn = std.debug.print; 4 | const Allocator = std.mem.Allocator; 5 | var allocator: Allocator = undefined; 6 | const ipc = @import("./ipc/epoll.zig"); 7 | const config = @import("./config.zig"); 8 | 9 | const c = @cImport({ 10 | @cInclude("unistd.h"); 11 | @cInclude("pthread.h"); 12 | @cInclude("sys/epoll.h"); 13 | }); 14 | 15 | pub const Actor = struct { thread_id: c.pthread_t, client: *ipc.Client, payload: *CommandVerb, recvback: *const fn (*Command) void, name: []const u8, allocator: std.mem.Allocator }; 16 | 17 | pub const Command = packed struct { id: u16, verb: *const CommandVerb, actor: *Actor }; 18 | 19 | pub const CommandVerb = packed union { login: *config.LoginInfo, http: *config.HttpInfo, column: *config.ColumnInfo, auth: *config.ColumnAuth, idle: u16 }; 20 | 21 | pub const ActorList = std.AutoArrayHashMap(u64, *Actor); 22 | var actors: ActorList = undefined; 23 | 24 | pub fn init(myAllocator: Allocator) !void { 25 | allocator = myAllocator; 26 | actors = ActorList.init(allocator); 27 | register_main_tid(self()) catch unreachable; 28 | try ipc.init(); 29 | } 30 | 31 | pub fn register_main_tid(mtid: u64) !void { 32 | var actor = try allocator.create(Actor); 33 | actor.name = "main"; 34 | try actors.put(mtid, actor); 35 | } 36 | 37 | pub fn name(tid: u64) []const u8 { 38 | return if (actors.get(tid)) |actor| actor.name else "-unregistered-thread-"; 39 | } 40 | 41 | pub fn create( 42 | actor_name: []const u8, 43 | startFn: *const fn (?*anyopaque) callconv(.C) ?*anyopaque, 44 | startParams: *CommandVerb, 45 | recvback: *const fn (*Command) void, 46 | ) !*Actor { 47 | var actor = try allocator.create(Actor); 48 | actor.client = ipc.newClient(allocator); 49 | actor.payload = startParams; //unused 50 | ipc.dial(actor.client, ""); 51 | actor.recvback = recvback; 52 | actor.name = actor_name; 53 | actor.allocator = allocator; 54 | //ipc.register(actor.client, recvback); 55 | const pthread_result = c.pthread_create(&actor.thread_id, null, startFn, actor); 56 | if (pthread_result == 0) { 57 | try actors.putNoClobber(actor.thread_id, actor); 58 | return actor; 59 | } else { 60 | warn("ERROR thread pthread_create err: {!} {*}", .{ pthread_result, actor }); 61 | } 62 | return error.BadValue; 63 | } 64 | 65 | pub fn signal(actor: *Actor, command: *Command) void { 66 | command.actor = actor; // fill in the command 67 | //const command_address_bytes: *const [8]u8 = @ptrCast([*]const u8, command)[0..8]; // not OK 68 | const command_address_bytes: *align(8) const [8]u8 = std.mem.asBytes(&command); // OK 69 | //const command_address_bytes = std.mem.asBytes(&@as(usize, @ptrToInt(command))); // OK 70 | //warn("tid {} is signaling command {*} id {} {*} to thread.wait() \n", .{ actor.thread_id, command, command.id, command.verb }); 71 | ipc.send(actor.client, command_address_bytes); 72 | } 73 | 74 | pub fn destroy(actor: *Actor) void { 75 | ipc.close(actor.client); 76 | _ = actors.swapRemove(actor.thread_id); 77 | allocator.destroy(actor); 78 | } 79 | 80 | pub fn self() c.pthread_t { 81 | return c.pthread_self(); 82 | } 83 | 84 | pub fn wait() void { 85 | const client = ipc.wait(); 86 | 87 | var bufArray = [_]u8{0} ** 16; // arbitrary receive buffer 88 | const buf: []u8 = ipc.read(client, bufArray[0..]); 89 | if (buf.len == 0) { 90 | // todo: skip read() and pass ptr with event_data 91 | warn("thread.wait ipc.read no socket payload! DEFLECTED!\n", .{}); 92 | } else { 93 | const b8: *[@sizeOf(usize)]u8 = @as(*[@sizeOf(usize)]u8, @ptrCast(buf.ptr)); 94 | const command: *Command = std.mem.bytesAsValue(*Command, b8).*; 95 | var iter = actors.iterator(); 96 | while (iter.next()) |entry| { 97 | const actor = entry.value_ptr.*; 98 | if (actor.client == client) { 99 | actor.recvback(command); 100 | break; 101 | } 102 | } 103 | } 104 | } 105 | 106 | pub fn join(jthread: c.pthread_t, joinret: *?*anyopaque) c_int { 107 | //pub extern fn pthread_join(__th: pthread_t, __thread_return: ?[*](?*c_void)) c_int; 108 | return c.pthread_join(jthread, @as(?[*]?*anyopaque, @ptrCast(joinret))); //expected type '?[*]?*c_void' / void **value_ptr 109 | } 110 | -------------------------------------------------------------------------------- /src/toot.zig: -------------------------------------------------------------------------------- 1 | // toot.zig 2 | const std = @import("std"); 3 | const Allocator = std.mem.Allocator; 4 | const testing = std.testing; 5 | const util = @import("util.zig"); 6 | const warn = util.log; 7 | 8 | pub const Type = Toot(); 9 | 10 | pub const Img = struct { 11 | bytes: []const u8, 12 | url: []const u8, 13 | id: []const u8, 14 | }; 15 | 16 | pub fn Toot() type { 17 | return struct { 18 | hashmap: std.json.Value, 19 | tagList: TagList, 20 | imgList: ImgList, 21 | 22 | const Self = @This(); 23 | const TagType = []const u8; 24 | pub const TagList = std.ArrayList(TagType); 25 | const ImgBytes = []const u8; 26 | const ImgList = std.ArrayList(Img); 27 | const K = []const u8; 28 | const V = std.json.Value; 29 | pub fn init(hash: std.json.Value, allocator: Allocator) *Self { 30 | var toot = allocator.create(Self) catch unreachable; 31 | toot.hashmap = hash; 32 | toot.tagList = TagList.init(allocator); 33 | toot.imgList = ImgList.init(allocator); 34 | toot.parseTags(allocator); 35 | warn("toot init #{s}", .{toot.id()}); 36 | return toot; 37 | } 38 | 39 | pub fn get(self: *const Self, key: K) ?V { 40 | return self.hashmap.object.get(key); 41 | } 42 | 43 | pub fn id(self: *const Self) []const u8 { 44 | if (self.hashmap.object.get("id")) |kv| { 45 | return kv.string; 46 | } else { 47 | unreachable; 48 | } 49 | } 50 | 51 | pub fn acct(self: *const Self) ![]const u8 { 52 | if (self.hashmap.object.get("account")) |kv| { 53 | if (kv.object.get("acct")) |akv| { 54 | return akv.string; 55 | } else { 56 | return error.NoAcct; 57 | } 58 | } else { 59 | return error.NoAccount; 60 | } 61 | } 62 | 63 | pub fn content(self: *const Self) []const u8 { 64 | return self.hashmap.object.get("content").?.string; 65 | } 66 | 67 | pub fn parseTags(self: *Self, allocator: Allocator) void { 68 | const hDecode = util.htmlEntityDecode(self.content(), allocator) catch unreachable; 69 | const html_trim = util.htmlTagStrip(hDecode, allocator) catch unreachable; 70 | 71 | var wordParts = std.mem.tokenizeSequence(u8, html_trim, " "); 72 | while (wordParts.next()) |word| { 73 | if (std.mem.startsWith(u8, word, "#")) { 74 | self.tagList.append(word) catch unreachable; 75 | } 76 | } 77 | } 78 | 79 | pub fn containsImg(self: *const @This(), img_id: []const u8) bool { 80 | var contains_img = false; 81 | for (self.imgList.items) |img| { 82 | if (std.mem.eql(u8, img.id, img_id)) { 83 | contains_img = true; 84 | } 85 | } 86 | return contains_img; 87 | } 88 | 89 | pub fn addImg(self: *Self, img: Img) void { 90 | warn("addImg toot {s}", .{self.id()}); 91 | self.imgList.append(img) catch unreachable; 92 | } 93 | 94 | pub fn imgCount(self: *Self) usize { 95 | const images = self.hashmap.object.get("media_attachments").?.array; 96 | return images.items.len; 97 | } 98 | }; 99 | } 100 | 101 | test "Toot" { 102 | const allocator = std.testing.allocator; 103 | var tootHash = std.json.Value{ .object = std.json.ObjectMap.init(allocator) }; 104 | 105 | const id_value = std.json.Value{ .string = "1234" }; 106 | _ = tootHash.object.put("id", id_value) catch unreachable; 107 | const content_value = std.json.Value{ .string = "I am a post." }; 108 | _ = tootHash.object.put("content", content_value) catch unreachable; 109 | 110 | const toot = Type.init(tootHash, allocator); 111 | try testing.expect(toot.tagList.items.len == 0); 112 | 113 | const content_with_tag_value = std.json.Value{ .string = "Kirk or Picard? #startrek" }; 114 | _ = tootHash.object.put("content", content_with_tag_value) catch unreachable; 115 | const toot2 = Type.init(tootHash, allocator); 116 | try testing.expect(toot2.tagList.items.len == 1); 117 | try testing.expect(std.mem.order(u8, toot2.tagList.items[0], "#startrek") == std.math.Order.eq); 118 | } 119 | -------------------------------------------------------------------------------- /src/toot_list.zig: -------------------------------------------------------------------------------- 1 | // toot_list.zig 2 | const std = @import("std"); 3 | const Allocator = std.mem.Allocator; 4 | 5 | const toot_lib = @import("./toot.zig"); 6 | const util = @import("./util.zig"); 7 | const warn = util.log; 8 | 9 | pub const TootList = SomeList(*toot_lib.Type); 10 | 11 | pub fn SomeList(comptime T: type) type { 12 | return struct { 13 | list: ListType, 14 | 15 | const Self = @This(); 16 | const ListType = std.DoublyLinkedList(T); 17 | 18 | pub fn init() Self { 19 | return Self{ 20 | .list = ListType{}, 21 | }; 22 | } 23 | 24 | pub fn len(self: *Self) usize { 25 | return self.list.len; 26 | } 27 | 28 | pub fn first(self: *Self) ?*ListType.Node { 29 | return self.list.first; 30 | } 31 | 32 | pub fn contains(self: *Self, item: T) bool { 33 | var ptr = self.list.first; 34 | while (ptr) |listItem| { 35 | if (util.hashIdSame(T, listItem.data, item)) { 36 | return true; 37 | } 38 | ptr = listItem.next; 39 | } 40 | return false; 41 | } 42 | 43 | pub fn author(self: *Self, acct: []const u8, allocator: Allocator) []T { 44 | var winners = std.ArrayList(T).init(allocator); 45 | var ptr = self.list.first; 46 | while (ptr) |listItem| { 47 | const toot = listItem.data; 48 | if (toot.acct()) |toot_acct| { 49 | if (std.mem.order(u8, acct, toot_acct) == std.math.Order.eq) { 50 | winners.append(toot) catch unreachable; 51 | } 52 | } else |_| {} 53 | ptr = listItem.next; 54 | } 55 | return winners.items; 56 | } 57 | 58 | pub fn sortedInsert(self: *Self, item: T, allocator: Allocator) void { 59 | const itemDate = item.get("created_at").?.string; 60 | const node = allocator.create(ListType.Node) catch unreachable; 61 | node.data = item; 62 | var current = self.list.first; 63 | while (current) |listItem| { 64 | const listItemDate = listItem.data.get("created_at").?.string; 65 | if (std.mem.order(u8, itemDate, listItemDate) == std.math.Order.gt) { 66 | self.list.insertBefore(listItem, node); 67 | return; 68 | } else {} 69 | current = listItem.next; 70 | } 71 | self.list.append(node); 72 | } 73 | 74 | pub fn count(self: *Self) usize { 75 | var counter: usize = 0; 76 | var current = self.list.first; 77 | while (current) |item| { 78 | counter = counter + 1; 79 | current = item.next; 80 | } 81 | return counter; 82 | } 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /src/util.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const thread = @import("./thread.zig"); 4 | const warn = std.debug.print; 5 | const Allocator = std.mem.Allocator; 6 | var GPAllocator = std.heap.GeneralPurposeAllocator(.{}){}; 7 | const alloc = GPAllocator.allocator(); 8 | 9 | const SimpleBuffer = @import("./simple_buffer.zig"); 10 | 11 | pub fn sliceAddNull(allocator: Allocator, str: []const u8) []const u8 { 12 | return allocator.dupeZ(u8, str) catch unreachable; 13 | } 14 | 15 | pub fn sliceToCstr(allocator: Allocator, str: []const u8) [*]u8 { 16 | var str_null: []u8 = allocator.alloc(u8, str.len + 1) catch unreachable; 17 | std.mem.copyForwards(u8, str_null[0..], str); 18 | str_null[str.len] = 0; 19 | return str_null.ptr; 20 | } 21 | 22 | pub fn cstrToSliceCopy(allocator: Allocator, cstr: [*c]const u8) []const u8 { 23 | const i: usize = std.mem.len(cstr); 24 | const ram = allocator.alloc(u8, i) catch unreachable; 25 | std.mem.copyForwards(u8, ram, cstr[0..i]); 26 | return ram; 27 | } 28 | 29 | pub fn json_stringify(value: anytype) []u8 { 30 | return std.json.stringifyAlloc(alloc, value, .{}) catch unreachable; 31 | } 32 | 33 | pub fn strings_join_separator(parts: []const []const u8, separator: u8, allocator: Allocator) []const u8 { 34 | var buf = std.ArrayList(u8).init(allocator); 35 | for (parts, 0..) |part, idx| { 36 | // todo abort if part contains separator 37 | 38 | buf.appendSlice(part) catch unreachable; 39 | if (idx != parts.len - 1) { 40 | buf.append(separator) catch unreachable; 41 | } 42 | } 43 | return buf.toOwnedSlice() catch unreachable; 44 | } 45 | 46 | test strings_join_separator { 47 | const joined = strings_join_separator(&.{ "a", "b" }, ':', std.testing.allocator); 48 | try std.testing.expectEqualSlices(u8, "a:b", joined); 49 | std.testing.allocator.free(joined); 50 | } 51 | 52 | pub fn log(comptime msg: []const u8, args: anytype) void { 53 | const tid = thread.self(); 54 | const tid_name = thread.name(tid); 55 | //const tz = std.os.timezone.tz_minuteswest; 56 | var tz = std.posix.timezone{ .minuteswest = 0, .dsttime = 0 }; 57 | std.posix.gettimeofday(null, &tz); // does not set tz 58 | const now_ms = std.time.milliTimestamp() + tz.minuteswest * std.time.ms_per_hour; 59 | const ms_leftover = @abs(now_ms) % std.time.ms_per_s; 60 | const esec = std.time.epoch.EpochSeconds{ .secs = @as(u64, @intCast(@divTrunc(now_ms, std.time.ms_per_s))) }; 61 | const eday = esec.getEpochDay(); 62 | const yday = eday.calculateYearDay(); 63 | const mday = yday.calculateMonthDay(); 64 | const dsec = esec.getDaySeconds(); 65 | 66 | const time_str = std.fmt.allocPrint(alloc, "{d}-{d:0>2}-{d:0>2} {d:0>2}:{d:0>2}:{d:0>2}.{d:0>3.3}", .{ yday.year, mday.month.numeric(), mday.day_index + 1, dsec.getHoursIntoDay(), dsec.getMinutesIntoHour(), dsec.getSecondsIntoMinute(), ms_leftover }) catch unreachable; 67 | std.debug.print("{s} [{s:9}] " ++ msg ++ "\n", .{ time_str, tid_name } ++ args); 68 | } 69 | 70 | pub fn hashIdSame(comptime T: type, a: T, b: T) bool { 71 | const a_id = a.get("id").?.string; 72 | const b_id = b.get("id").?.string; 73 | return std.mem.eql(u8, a_id, b_id); 74 | } 75 | 76 | pub fn mastodonExpandUrl(host: []const u8, home: bool, allocator: Allocator) []const u8 { 77 | var url = SimpleBuffer.SimpleU8.initSize(allocator, 0) catch unreachable; 78 | var filteredHost = host; 79 | if (filteredHost.len > 0) { 80 | if (filteredHost[filteredHost.len - 1] == '/') { 81 | filteredHost = filteredHost[0 .. filteredHost.len - 1]; 82 | } 83 | if (std.mem.order(u8, filteredHost[0..6], "https:") != std.math.Order.eq) { 84 | url.append("https://") catch unreachable; 85 | } 86 | url.append(filteredHost) catch unreachable; 87 | if (home) { 88 | url.append("/api/v1/timelines/home") catch unreachable; 89 | } else { 90 | url.append("/api/v1/timelines/public") catch unreachable; 91 | } 92 | return url.toSliceConst(); 93 | } else { 94 | //warn("mastodonExpandUrl given empty host", .{}); 95 | return ""; 96 | } 97 | } 98 | 99 | test "mastodonExpandUrl" { 100 | const url = mastodonExpandUrl("some.masto", true, alloc); 101 | try std.testing.expectEqualSlices(u8, url, "https://some.masto/api/v1/timelines/home"); 102 | } 103 | 104 | pub fn htmlTagStrip(str: []const u8, allocator: Allocator) ![]const u8 { 105 | var newStr = try SimpleBuffer.SimpleU8.initSize(allocator, 0); 106 | const States = enum { Looking, TagBegin }; 107 | var state = States.Looking; 108 | var tagEndPlusOne: usize = 0; 109 | for (str, 0..) |char, idx| { 110 | if (state == States.Looking and char == '<') { 111 | state = States.TagBegin; 112 | try newStr.append(str[tagEndPlusOne..idx]); 113 | } else if (state == States.TagBegin and char == '>') { 114 | tagEndPlusOne = idx + 1; 115 | state = States.Looking; 116 | } 117 | } 118 | if (tagEndPlusOne <= str.len) { 119 | try newStr.append(str[tagEndPlusOne..]); 120 | } 121 | return newStr.toSliceConst(); 122 | } 123 | 124 | test "htmlTagStrip" { 125 | var stripped = htmlTagStrip("a

    b

    ", alloc) catch unreachable; 126 | try std.testing.expect(std.mem.eql(u8, stripped, "ab")); 127 | stripped = htmlTagStrip("a

    b

    c", alloc) catch unreachable; 128 | try std.testing.expect(std.mem.eql(u8, stripped, "abc")); 129 | stripped = htmlTagStrip("abc", alloc) catch unreachable; 130 | try std.testing.expect(std.mem.eql(u8, stripped, "abc")); 131 | } 132 | 133 | pub fn htmlEntityDecode(str: []const u8, allocator: Allocator) ![]const u8 { 134 | var newStr = try SimpleBuffer.SimpleU8.initSize(allocator, 0); 135 | var previousStrEndMark: usize = 0; 136 | const States = enum { Looking, EntityBegin, EntityFound }; 137 | var state = States.Looking; 138 | var escStart: usize = undefined; 139 | for (str, 0..) |char, idx| { 140 | if (state == States.Looking and char == '&') { 141 | state = States.EntityBegin; 142 | escStart = idx; 143 | } else if (state == States.EntityBegin) { 144 | if (char == ';') { 145 | const snip = str[previousStrEndMark..escStart]; 146 | previousStrEndMark = idx + 1; 147 | try newStr.append(snip); 148 | const sigil = str[escStart + 1 .. idx]; 149 | var newChar: u8 = undefined; 150 | if (std.mem.order(u8, sigil, "amp") == std.math.Order.eq) { 151 | newChar = '&'; 152 | } 153 | try newStr.appendByte(newChar); 154 | state = States.Looking; 155 | } else if (idx - escStart > 4) { 156 | state = States.Looking; 157 | } 158 | } 159 | } 160 | if (previousStrEndMark <= str.len) { 161 | try newStr.append(str[previousStrEndMark..]); 162 | } 163 | return newStr.toSliceConst(); 164 | } 165 | 166 | test "htmlEntityParse" { 167 | const stripped = htmlEntityDecode("amp&pam", alloc) catch unreachable; 168 | try std.testing.expect(std.mem.eql(u8, stripped, "amp&pam")); 169 | } 170 | --------------------------------------------------------------------------------