this repo has no description
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

ui: add text field to networks

+145 -12
+10 -6
src/app.zig
··· 230 230 } 231 231 if (self.fg != null and self.bg != null) { 232 232 for (self.clients.items) |client| { 233 + client.text_field.style.bg = self.blendBg(10); 233 234 for (client.channels.items) |channel| { 234 235 channel.text_field.style.bg = self.blendBg(10); 235 236 } ··· 304 305 try ctx.queryColor(.fg); 305 306 try ctx.queryColor(.bg); 306 307 try ctx.queryColor(.{ .index = 3 }); 308 + if (self.clients.items.len > 0) { 309 + try ctx.requestFocus(self.clients.items[0].text_field.widget()); 310 + } 307 311 }, 308 312 .tick => { 309 313 for (self.clients.items) |client| { ··· 374 378 375 379 pub fn connect(self: *App, cfg: irc.Client.Config) !void { 376 380 const client = try self.alloc.create(irc.Client); 377 - client.* = try irc.Client.init(self.alloc, self, &self.write_queue, cfg); 381 + try client.init(self.alloc, self, &self.write_queue, cfg); 378 382 try self.clients.append(client); 379 383 } 380 384 ··· 383 387 self.buffer_list.nextItem(ctx); 384 388 if (self.selectedBuffer()) |buffer| { 385 389 switch (buffer) { 386 - .client => { 387 - ctx.requestFocus(self.widget()) catch {}; 390 + .client => |client| { 391 + ctx.requestFocus(client.text_field.widget()) catch {}; 388 392 }, 389 393 .channel => |channel| { 390 394 ctx.requestFocus(channel.text_field.widget()) catch {}; ··· 399 403 self.buffer_list.prevItem(ctx); 400 404 if (self.selectedBuffer()) |buffer| { 401 405 switch (buffer) { 402 - .client => { 403 - ctx.requestFocus(self.widget()) catch {}; 406 + .client => |client| { 407 + ctx.requestFocus(client.text_field.widget()) catch {}; 404 408 }, 405 409 .channel => |channel| { 406 410 ctx.requestFocus(channel.text_field.widget()) catch {}; ··· 659 663 for (self.clients.items) |client| { 660 664 if (client == target) { 661 665 if (self.ctx) |ctx| { 662 - ctx.requestFocus(self.widget()) catch {}; 666 + ctx.requestFocus(client.text_field.widget()) catch {}; 663 667 } 664 668 self.buffer_list.cursor = i; 665 669 self.buffer_list.ensureScroll();
+87 -6
src/irc.zig
··· 1656 1656 has_mouse: bool, 1657 1657 retry_delay_s: u8, 1658 1658 1659 + text_field: vxfw.TextField, 1660 + completer_shown: bool, 1661 + 1659 1662 pub fn init( 1663 + self: *Client, 1660 1664 alloc: std.mem.Allocator, 1661 1665 app: *comlink.App, 1662 1666 wq: *comlink.WriteQueue, 1663 1667 cfg: Config, 1664 - ) !Client { 1665 - return .{ 1668 + ) !void { 1669 + self.* = .{ 1666 1670 .alloc = alloc, 1667 1671 .app = app, 1668 1672 .client = undefined, ··· 1678 1682 .read_buf = std.ArrayList(u8).init(alloc), 1679 1683 .has_mouse = false, 1680 1684 .retry_delay_s = 0, 1685 + .text_field = .init(alloc, app.unicode), 1686 + .completer_shown = false, 1681 1687 }; 1688 + self.text_field.style = .{ .bg = self.app.blendBg(10) }; 1689 + self.text_field.userdata = self; 1690 + self.text_field.onSubmit = Client.onSubmit; 1691 + } 1692 + 1693 + fn onSubmit(ptr: ?*anyopaque, ctx: *vxfw.EventContext, input: []const u8) anyerror!void { 1694 + const self: *Client = @ptrCast(@alignCast(ptr orelse unreachable)); 1695 + 1696 + // Copy the input into a temporary buffer 1697 + var buf: [1024]u8 = undefined; 1698 + @memcpy(buf[0..input.len], input); 1699 + const local = buf[0..input.len]; 1700 + // Free the text field. We do this here because the command may destroy our channel 1701 + self.text_field.clearAndFree(); 1702 + self.completer_shown = false; 1703 + 1704 + if (std.mem.startsWith(u8, local, "/")) { 1705 + try self.app.handleCommand(.{ .client = self }, local); 1706 + } 1707 + ctx.redraw = true; 1682 1708 } 1683 1709 1684 1710 /// Closes the connection ··· 1779 1805 1780 1806 fn typeErasedViewDraw(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 1781 1807 const self: *Client = @ptrCast(@alignCast(ptr)); 1782 - const text: vxfw.Text = .{ .text = "content" }; 1783 - var surface = try text.draw(ctx); 1784 - surface.widget = self.view(); 1785 - return surface; 1808 + const max = ctx.max.size(); 1809 + 1810 + var children = std.ArrayList(vxfw.SubSurface).init(ctx.arena); 1811 + { 1812 + // Draw the character limit. 14 is length of message overhead "PRIVMSG :\r\n" 1813 + const max_limit = 510; 1814 + const limit = try std.fmt.allocPrint( 1815 + ctx.arena, 1816 + " {d}/{d}", 1817 + .{ self.text_field.buf.realLength(), max_limit }, 1818 + ); 1819 + const style: vaxis.Style = if (self.text_field.buf.realLength() > max_limit) 1820 + .{ .fg = .{ .index = 1 }, .reverse = true } 1821 + else 1822 + .{ .bg = self.app.blendBg(30) }; 1823 + const limit_text: vxfw.Text = .{ .text = limit, .style = style }; 1824 + const limit_ctx = ctx.withConstraints(.{ .width = @intCast(limit.len) }, ctx.max); 1825 + const limit_s = try limit_text.draw(limit_ctx); 1826 + 1827 + try children.append(.{ 1828 + .origin = .{ .col = max.width -| limit_s.size.width, .row = max.height - 1 }, 1829 + .surface = limit_s, 1830 + }); 1831 + 1832 + const text_field_ctx = ctx.withConstraints( 1833 + ctx.min, 1834 + .{ .height = 1, .width = max.width -| limit_s.size.width }, 1835 + ); 1836 + 1837 + // Draw the text field 1838 + try children.append(.{ 1839 + .origin = .{ .col = 0, .row = max.height - 1 }, 1840 + .surface = try self.text_field.draw(text_field_ctx), 1841 + }); 1842 + // Write some placeholder text if we don't have anything in the text field 1843 + if (self.text_field.buf.realLength() == 0) { 1844 + const text = try std.fmt.allocPrint(ctx.arena, "Message {s}", .{self.serverName()}); 1845 + var text_style = self.text_field.style; 1846 + text_style.italic = true; 1847 + text_style.dim = true; 1848 + var ghost_text_ctx = text_field_ctx; 1849 + ghost_text_ctx.max.width = text_field_ctx.max.width.? -| 2; 1850 + const ghost_text: vxfw.Text = .{ .text = text, .style = text_style }; 1851 + try children.append(.{ 1852 + .origin = .{ .col = 2, .row = max.height - 1 }, 1853 + .surface = try ghost_text.draw(ghost_text_ctx), 1854 + }); 1855 + } 1856 + } 1857 + return .{ 1858 + .widget = self.view(), 1859 + .size = max, 1860 + .buffer = &.{}, 1861 + .children = children.items, 1862 + }; 1863 + } 1864 + 1865 + pub fn serverName(self: *Client) []const u8 { 1866 + return self.config.name orelse self.config.server; 1786 1867 } 1787 1868 1788 1869 pub fn nameWidget(self: *Client, selected: bool) vxfw.Widget {
+48
src/ui.zig
··· 1 + const std = @import("std"); 2 + const vaxis = @import("vaxis"); 3 + 4 + const vxfw = vaxis.vxfw; 5 + 6 + const Allocator = std.mem.Allocator; 7 + const App = @import("app.zig").App; 8 + const ChildList = std.ArrayListUnmanaged(SubSurface); 9 + const Surface = vxfw.Surface; 10 + const SubSurface = vxfw.SubSurface; 11 + 12 + const default_rhs: vxfw.Text = .{ .text = "TODO: update this text" }; 13 + 14 + pub fn drawMain(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!Surface { 15 + const self: *App = @ptrCast(@alignCast(ptr)); 16 + const max = ctx.max.size(); 17 + self.last_height = max.height; 18 + if (self.selectedBuffer()) |buffer| { 19 + switch (buffer) { 20 + .client => |client| self.view.rhs = client.view(), 21 + .channel => |channel| self.view.rhs = channel.view.widget(), 22 + } 23 + } else self.view.rhs = default_rhs.widget(); 24 + 25 + var children: ChildList = .empty; 26 + 27 + // UI is a tree of splits 28 + // │ │ │ │ 29 + // │ │ │ │ 30 + // │ buffers │ buffer content │ members │ 31 + // │ │ │ │ 32 + // │ │ │ │ 33 + // │ │ │ │ 34 + // │ │ │ │ 35 + 36 + const sub: vxfw.SubSurface = .{ 37 + .origin = .{ .col = 0, .row = 0 }, 38 + .surface = try self.view.widget().draw(ctx), 39 + }; 40 + try children.append(ctx.arena, sub); 41 + 42 + return .{ 43 + .size = ctx.max.size(), 44 + .widget = self.widget(), 45 + .buffer = &.{}, 46 + .children = children.items, 47 + }; 48 + }